From bed2372f19eae5968e11c45f8036fd1efa762eb0 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 12 Sep 2026 21:43:10 +0200 Subject: [PATCH 01/10] Serve the Reflex chart data plane as a channel on the app websocket, dropping socket.io --- CLAUDE.md | 9 +- docs/app/pyproject.toml | 13 +- docs/app/uv.lock | 146 +--- examples/reflex/README.md | 2 +- news/505.breaking.md | 8 + news/505.feature.md | 9 + pyproject.toml | 35 +- python/reflex_xy/__init__.py | 12 +- python/reflex_xy/app.py | 65 +- python/reflex_xy/assets/XYChart.jsx | 127 ++- .../reflex_xy/{namespace.py => data_plane.py} | 240 ++++-- python/reflex_xy/registry.py | 12 +- python/reflex_xy/state_bridge.py | 4 +- python/xy/interaction.py | 2 +- scripts/reflex_ws_smoke.py | 6 +- scripts/verify_sdist.py | 2 +- scripts/verify_wheel.py | 2 +- spec/README.md | 4 +- spec/api/chart-roadmap.md | 2 +- .../reflex-component-api-implementation.md | 6 +- spec/design/reflex-component-api-options.md | 2 +- spec/design/reflex-integration.md | 201 +++-- spec/design/renderer-architecture.md | 2 +- spec/design/view-state.md | 2 +- spec/design/wire-protocol.md | 12 +- tests/reflex_adapter/test_assets.py | 80 +- tests/reflex_adapter/test_channel_browser.py | 169 ++++ ...ocket_data_plane.py => test_data_plane.py} | 793 +++++++++--------- .../test_page_plan_registration.py | 9 +- tests/test_dependencies.py | 13 +- tests/test_tailwind_root_customization.py | 2 +- uv.lock | 639 ++++++-------- 32 files changed, 1402 insertions(+), 1228 deletions(-) create mode 100644 news/505.breaking.md create mode 100644 news/505.feature.md rename python/reflex_xy/{namespace.py => data_plane.py} (76%) create mode 100644 tests/reflex_adapter/test_channel_browser.py rename tests/reflex_adapter/{test_socket_data_plane.py => test_data_plane.py} (72%) diff --git a/CLAUDE.md b/CLAUDE.md index dd21739f2..98320e545 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,10 +67,11 @@ control. `tests/pyplot/corpus/` + `spec/matplotlib/compat.md`. - `python/reflex_xy/` — the bundled Reflex integration (import namespace `reflex_xy`; design: `spec/design/reflex-integration.md`). Chart - data rides the app's own websocket as a second socket.io namespace; - figures live in a per-process registry rebuilt from Reflex state on miss. - The source ships in every `xy` artifact; the `xy[reflex]` extra selects the - supported Reflex floor while plain `xy` keeps no Reflex runtime dependency. + data rides the app's own websocket as a dedicated Reflex channel (`/_xy`, + `data_plane.py`); figures live in a per-process registry rebuilt from Reflex + state on miss. The source ships in every `xy` artifact; the `xy[reflex]` + extra selects the supported Reflex while plain `xy` keeps no Reflex runtime + dependency. The core `python/xy` package must never import Reflex. The render client is linked out of that package at app compile (no second copy to drift). Tests: `tests/reflex_adapter/` (skip unless Reflex is installed). diff --git a/docs/app/pyproject.toml b/docs/app/pyproject.toml index b5d21c1b2..adb1f7a8f 100644 --- a/docs/app/pyproject.toml +++ b/docs/app/pyproject.toml @@ -3,13 +3,18 @@ name = "xy-docs-app" version = "0.1.0" description = "Documentation site for XY." requires-python = ">=3.11" +# TEMPORARY: these four normally track reflex-dev/reflex@main, but `xy[reflex]` +# requires the unreleased channel transport (reflex-dev/reflex#6932) and uv +# refuses two URLs for one package. They therefore point at the same branch the +# root pyproject does — all from one repo, so the framework is never half +# branch and half main. Restore `@main` when channels land there. dependencies = [ "numpy", "pillow", - "reflex @ git+https://github.com/reflex-dev/reflex@main", - "reflex-components-internal @ git+https://github.com/reflex-dev/reflex@main#subdirectory=packages/reflex-components-internal", - "reflex-docgen @ git+https://github.com/reflex-dev/reflex@main#subdirectory=packages/reflex-docgen", - "reflex-site-shared @ git+https://github.com/reflex-dev/reflex@main#subdirectory=packages/reflex-site-shared", + "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@make-sio-optional", + "reflex-components-internal @ git+https://github.com/benedikt-bartscher/reflex.git@make-sio-optional#subdirectory=packages/reflex-components-internal", + "reflex-docgen @ git+https://github.com/benedikt-bartscher/reflex.git@make-sio-optional#subdirectory=packages/reflex-docgen", + "reflex-site-shared @ git+https://github.com/benedikt-bartscher/reflex.git@make-sio-optional#subdirectory=packages/reflex-site-shared", "xy[reflex]", ] diff --git a/docs/app/uv.lock b/docs/app/uv.lock index 53b08dd30..16bf81cf8 100644 --- a/docs/app/uv.lock +++ b/docs/app/uv.lock @@ -51,15 +51,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, ] -[[package]] -name = "bidict" -version = "0.23.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, -] - [[package]] name = "certifi" version = "2026.7.22" @@ -994,18 +985,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/45/689603d04b3bb8d7faa00f25c24acef993aab7813b3dbbfc472a459ab0b5/python_discovery-1.5.2-py3-none-any.whl", hash = "sha256:3e338c2d0f15dfaeea57493f4c2c6caebe0e998ea815c30ae8bf8ee21f1112d3", size = 38350, upload-time = "2026-08-12T14:05:25.113Z" }, ] -[[package]] -name = "python-engineio" -version = "4.13.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "simple-websocket" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/d8/65cc479ab697a2e7fdee83a9bd8a06b61ec68bf763a58a302cf161bf38bb/python_engineio-4.13.5.tar.gz", hash = "sha256:b5764d62243e3ffbc4c76dda3d7897c329dc52294c80c27105f9faa054e76897", size = 80035, upload-time = "2026-08-12T22:52:23.78Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/01/f804208061b504894546fddc479f6075e0f00dfe88cb1703dafaa8c3c67e/python_engineio-4.13.5-py3-none-any.whl", hash = "sha256:05c9f4951d242ad33d613b4245299562e5f64e4199f00e5390f9888505831704", size = 59963, upload-time = "2026-08-12T22:52:22.5Z" }, -] - [[package]] name = "python-multipart" version = "0.0.32" @@ -1015,19 +994,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, ] -[[package]] -name = "python-socketio" -version = "5.16.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bidict" }, - { name = "python-engineio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/5e/87d6b547c87c6d64f4a05f5bfaf6f42e9b786561216434290fdaa83f8667/python_socketio-5.16.4.tar.gz", hash = "sha256:f7fa4a43cc8e687930b5c6e44d6e2efc2071eca4bef49b8bb3dc0827f7f92235", size = 128140, upload-time = "2026-08-06T23:11:21.346Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/d9/463feca73ec119a135d90c9f40c0172b4758150b5ed442f0ca1e8fed807a/python_socketio-5.16.4-py3-none-any.whl", hash = "sha256:0eb9c7687e7fbf59e60d714fd62afba77dfaf8ef8a06a0bff05a86c351accc2f", size = 82098, upload-time = "2026-08-06T23:11:19.851Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -1097,8 +1063,8 @@ wheels = [ [[package]] name = "reflex" -version = "0.9.8.post23.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, @@ -1106,7 +1072,6 @@ dependencies = [ { name = "packaging" }, { name = "psutil", marker = "sys_platform == 'win32'" }, { name = "python-multipart" }, - { name = "python-socketio" }, { name = "redis" }, { name = "reflex-base" }, { name = "reflex-components-code" }, @@ -1130,8 +1095,8 @@ dependencies = [ [[package]] name = "reflex-base" -version = "0.9.8.post23.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-base&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "packaging" }, { name = "platformdirs" }, @@ -1141,8 +1106,8 @@ dependencies = [ [[package]] name = "reflex-components-code" -version = "0.9.3.post23.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-code&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1152,8 +1117,8 @@ dependencies = [ [[package]] name = "reflex-components-core" -version = "0.9.8.post23.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-core&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "python-multipart" }, { name = "reflex-base" }, @@ -1165,8 +1130,8 @@ dependencies = [ [[package]] name = "reflex-components-dataeditor" -version = "0.9.1.post307.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-dataeditor&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1174,32 +1139,32 @@ dependencies = [ [[package]] name = "reflex-components-gridjs" -version = "0.9.1.post307.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-gridjs&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-internal" -version = "0.0.23.post105.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-internal&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex" }, ] [[package]] name = "reflex-components-lucide" -version = "1.0.3.post23.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-lucide&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-markdown" -version = "0.9.3.post175.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-markdown&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-code" }, @@ -1209,16 +1174,16 @@ dependencies = [ [[package]] name = "reflex-components-moment" -version = "0.9.3.post23.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-moment&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-plotly" -version = "0.9.4.post23.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-plotly&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1226,8 +1191,8 @@ dependencies = [ [[package]] name = "reflex-components-radix" -version = "0.9.7.post23.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-radix&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1236,8 +1201,8 @@ dependencies = [ [[package]] name = "reflex-components-react-player" -version = "0.9.1.post307.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-react-player&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1245,16 +1210,16 @@ dependencies = [ [[package]] name = "reflex-components-recharts" -version = "0.9.2.post23.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-recharts&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-sonner" -version = "0.9.1.post307.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-sonner&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-lucide" }, @@ -1262,8 +1227,8 @@ dependencies = [ [[package]] name = "reflex-docgen" -version = "0.9.4.post23.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-docgen&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "griffelib" }, { name = "mistletoe" }, @@ -1274,8 +1239,8 @@ dependencies = [ [[package]] name = "reflex-hosting-cli" -version = "0.1.68.post31.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-hosting-cli&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "click" }, { name = "httpx" }, @@ -1286,13 +1251,13 @@ dependencies = [ [[package]] name = "reflex-integrations-docs" -version = "0.0.0.post3313.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Fintegrations-docs&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Fintegrations-docs&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } [[package]] name = "reflex-site-shared" -version = "0.0.41.post53.dev0+ed9acd35" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-site-shared&rev=main#ed9acd35df9708cef819337904854cf4f13abd22" } +version = "0.0.3.post248.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "email-validator" }, { name = "httpx" }, @@ -1380,18 +1345,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d6/92/477780b98169e0dc3d37fac015f3f9ef3ced1dfcd2ec3a44686c8e07f8d8/ruff_format-0.5.4-cp38-abi3-win_arm64.whl", hash = "sha256:b0dfb4ef7c7f0da7b17c018dc48103907375f06c7b9e3dac3bc396722a725a65", size = 1988587, upload-time = "2026-08-10T18:06:51.397Z" }, ] -[[package]] -name = "simple-websocket" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wsproto" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, -] - [[package]] name = "stack-data" version = "0.6.3" @@ -1661,18 +1614,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, ] -[[package]] -name = "wsproto" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, -] - [[package]] name = "xy" source = { editable = "../../" } @@ -1691,7 +1632,7 @@ reflex = [ requires-dist = [ { name = "anywidget", specifier = ">=0.9" }, { name = "numpy", specifier = ">=1.24" }, - { name = "reflex", marker = "extra == 'reflex'", specifier = ">=0.9.6" }, + { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=make-sio-optional" }, ] provides-extras = ["reflex"] @@ -1708,6 +1649,7 @@ dev = [ { name = "starlette", specifier = ">=0.36" }, { name = "ty" }, { name = "uvicorn", specifier = ">=0.23" }, + { name = "websockets", specifier = ">=12" }, ] [[package]] @@ -1738,10 +1680,10 @@ dev = [ requires-dist = [ { name = "numpy" }, { name = "pillow" }, - { name = "reflex", git = "https://github.com/reflex-dev/reflex?rev=main" }, - { name = "reflex-components-internal", git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-internal&rev=main" }, - { name = "reflex-docgen", git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-docgen&rev=main" }, - { name = "reflex-site-shared", git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-site-shared&rev=main" }, + { name = "reflex", git = "https://github.com/benedikt-bartscher/reflex.git?rev=make-sio-optional" }, + { name = "reflex-components-internal", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=make-sio-optional" }, + { name = "reflex-docgen", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=make-sio-optional" }, + { name = "reflex-site-shared", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=make-sio-optional" }, { name = "xy", extras = ["reflex"], editable = "../../" }, ] diff --git a/examples/reflex/README.md b/examples/reflex/README.md index 226553b34..4d625523b 100644 --- a/examples/reflex/README.md +++ b/examples/reflex/README.md @@ -5,7 +5,7 @@ One page walks through the ways to link chart data into a Reflex app, and each section carries a **Code** accordion showing its source via `inspect.getsource`. -Chart data rides the app's own websocket as a second socket.io namespace of +Chart data rides the app's own websocket as a dedicated Reflex channel of binary columns; Reflex state holds only a tiny handle per chart. Charts use the data-bound component API — structure declared in the page, compiled to a validated plan at `reflex run`, columns supplied by `@reflex_xy.data` — diff --git a/news/505.breaking.md b/news/505.breaking.md new file mode 100644 index 000000000..3947b91a1 --- /dev/null +++ b/news/505.breaking.md @@ -0,0 +1,8 @@ +The Reflex integration's chart data plane no longer uses socket.io. `/_xy` is +now a Reflex channel on the app's own event websocket, which means `xy[reflex]` +requires a Reflex that ships the channel transport and an app on the default +`transport="websocket"`; `transport="socketio"` and `transport="polling"` apps +can no longer serve charts. `reflex_xy.XYNamespace` and `reflex_xy.XY_NAMESPACE` +are replaced by `reflex_xy.XYChannel` and `reflex_xy.XY_PLANE`, and +`reflex_xy.namespace` by `reflex_xy.data_plane`. Application code that only +uses `XYPlugin`/`setup()` and the chart components is unaffected. diff --git a/news/505.feature.md b/news/505.feature.md new file mode 100644 index 000000000..1d7e32dca --- /dev/null +++ b/news/505.feature.md @@ -0,0 +1,9 @@ +The Reflex integration's chart data plane rides the app's own websocket as a +Reflex channel, with no socket.io anywhere. Binary columns now arrive beside +the message as frame attachments — aligned typed-array views over the received +frame rather than copies — and the per-message attachment budget rises from +socket.io-parser's hard limit of 10 to the channel frame's 64, so far more +figures ship their columns split instead of paying for a joined blob. The chart +component opens no connection of its own and mirrors none of Reflex's socket +options: it asks Reflex for the `/_xy` channel and rides whatever the app +already has. diff --git a/pyproject.toml b/pyproject.toml index 352fdb86b..14dae9655 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,22 @@ dependencies = [ # The adapter source ships in every xy wheel so the framework integration and # render client can never drift. The extra only selects the supported Reflex # floor; importing plain `xy` remains framework-free. +# +# TEMPORARY, and deliberately unmergeable: the data plane is a Reflex *channel* +# (spec/design/reflex-integration.md §2), which no released Reflex ships yet — +# it exists only on reflex-dev/reflex#6932. There is no socket.io fallback any +# more, so a version specifier here would be a lie in either direction: +# `>=0.9.6` admits versions where every chart stays blank, and a guessed +# `>=0.10` names a release that does not exist. The git requirement is the +# honest statement of what this integration needs, it keeps `uv sync` working +# for contributors, and uv resolves the rest of that repo's workspace +# (reflex-base, the component packages) from the same branch, so the framework +# is never half branch and half release. `docs/app/pyproject.toml` points its +# own Reflex requirements at the same branch for the same reason. Replace all +# of it with the real floor the moment channels are released (see the Status +# note in the design doc). reflex = [ - "reflex>=0.9.6", + "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@make-sio-optional", ] [project.urls] @@ -45,10 +59,15 @@ dev = [ # drilldown pan-alignment browser test importorskips it, so it lives in dev # where CI exercises that test. "starlette>=0.36", - # tests/reflex_adapter exercises the real socket data plane when the - # `reflex` extra is installed in the main CI job. + # tests/reflex_adapter exercises the real websocket data plane when the + # `reflex` extra is installed in the main CI job: uvicorn serves the app, + # aiohttp is the client. Bare uvicorn has no websocket protocol of its own + # and answers an upgrade request with 404, so the implementation is an + # explicit dev dependency rather than something inherited from a transport + # package. "aiohttp>=3.14.3", "uvicorn>=0.23", + "websockets>=12", # Parses .binder/environment.yml in tests/test_binder_config.py so the # Binder contract is asserted on structure, not string transcription. "pyyaml>=6", @@ -71,6 +90,16 @@ codspeed = [ [tool.hatch.version] source = "uv-dynamic-versioning" +# TEMPORARY, and paired with the git requirement in the `reflex` extra. Direct +# references are refused by default because PyPI refuses them on upload — which +# is the point here: while the channel transport is unreleased there is no +# version specifier that honestly describes what `xy[reflex]` needs, and a +# release built from this tree *should* fail rather than ship a floor that +# installs a Reflex where every chart stays blank. Remove both when channels +# are released. +[tool.hatch.metadata] +allow-direct-references = true + [tool.uv-dynamic-versioning] vcs = "git" style = "pep440" diff --git a/python/reflex_xy/__init__.py b/python/reflex_xy/__init__.py index befa9d395..ec1082bc6 100644 --- a/python/reflex_xy/__init__.py +++ b/python/reflex_xy/__init__.py @@ -2,7 +2,7 @@ The integration in one paragraph (full design: spec/design/reflex-integration.md in the xy repo): chart data rides -the app's *existing* websocket as a second socket.io namespace — binary +the app's *existing* websocket as a dedicated Reflex channel — binary columns, no JSON numbers, no extra endpoints to proxy. Figures and columns live in a per-process registry keyed by tokens; Reflex state holds only small typed handles. State methods are both the definition and the @@ -106,8 +106,8 @@ def index() -> rx.Component: "SelectEndEvent": ".events", "SelectionPayload": ".events", "ViewChangeEvent": ".events", - "XY_NAMESPACE": ".namespace", - "XYNamespace": ".namespace", + "XY_PLANE": ".data_plane", + "XYChannel": ".data_plane", "FigureRegistry": ".registry", "registry": ".registry", "resolve_selection": ".selections", @@ -179,7 +179,7 @@ def index() -> rx.Component: ) __all__ = [ - "XY_NAMESPACE", + "XY_PLANE", "AsyncDataVar", "AsyncFigureVar", "CanonicalRowIdGroup", @@ -197,7 +197,7 @@ def index() -> rx.Component: "SelectEndEvent", "SelectionPayload", "ViewChangeEvent", - "XYNamespace", + "XYChannel", "XYPlugin", "animation", "append", @@ -455,6 +455,7 @@ def release(token: "str | FigureHandle") -> None: ) from .app import XYPlugin, append, clear_selection, reset_view, select, set_view, setup + from .data_plane import XY_PLANE, XYChannel from .data_vars import AsyncDataVar, DataVar, data from .events import ( CanonicalRowIdGroup, @@ -492,7 +493,6 @@ def release(token: "str | FigureHandle") -> None: violin_chart, ) from .handles import DataHandle, FigureHandle - from .namespace import XY_NAMESPACE, XYNamespace from .registry import FigureRegistry, registry from .selections import resolve_selection from .vars import AsyncFigureVar, FigureVar, figure diff --git a/python/reflex_xy/app.py b/python/reflex_xy/app.py index 2fd276d86..16bbcb5c0 100644 --- a/python/reflex_xy/app.py +++ b/python/reflex_xy/app.py @@ -10,9 +10,9 @@ `setup` is idempotent; using both costs nothing. -What setup does: registers the `/_xy` socket.io namespace on the app's -existing AsyncServer (same physical websocket as the app plane — see -namespace.py), wires publish fan-out, and adds a lifespan task that +What setup does: registers the `/_xy` channel on the app's existing event +websocket (data_plane.py — the same physical connection as the app plane), +wires publish fan-out, and adds a lifespan task that registers this worker's chart plans (`_ensure_page_plans`, fail-closed), captures the event loop (for thread-safe broadcasts from sync handlers), and runs the registry TTL sweep. @@ -29,8 +29,8 @@ from reflex.plugins import Plugin +from .data_plane import XYChannel from .handles import FigureHandle, token_of -from .namespace import XYNamespace from .registry import _figure_of, registry from .state_bridge import make_rebuild_hook from .tokens import BUILDER_ATTR, PROBE_ATTR @@ -48,24 +48,27 @@ "setup", ] -_namespace: Optional[XYNamespace] = None +_channel: Optional[XYChannel] = None +# The app the current channel is attached to. Idempotency is per app, not +# per process: a hot reload (and AppHarness) re-imports the app module, and +# the new App gets its own websocket that must be attached to. +_setup_app: Any = None -def setup(app: Any) -> XYNamespace: - """Attach the xy data plane to a Reflex app (idempotent).""" - global _namespace - if _namespace is not None: - return _namespace - sio = getattr(app, "sio", None) - if sio is None: - msg = ( - "reflex_xy.setup(app) needs the app's socket server; it exists " - "only when state is enabled (rx.App(enable_state=True), the default)." - ) - raise RuntimeError(msg) - namespace = XYNamespace(registry, rebuild=make_rebuild_hook(app)) - sio.register_namespace(namespace) - wire(namespace) +def setup(app: Any) -> XYChannel: + """Attach the xy data plane to a Reflex app (idempotent). + + The plane is a Reflex channel on the app's own event websocket. Reflex's + `register_channel` refuses an app that cannot carry one — state disabled, + or a non-WebSocket transport — so a misconfiguration fails here at startup + rather than as a blank chart in the browser. + """ + global _channel, _setup_app + if _channel is not None and _setup_app is app: + return _channel + channel = XYChannel(registry, rebuild=make_rebuild_hook(app)) + app.register_channel(channel) + wire(channel) def _lifespan() -> Coroutine[Any, Any, None]: # Deliberately a *sync* function returning the sweep coroutine, not an @@ -80,8 +83,9 @@ def _lifespan() -> Coroutine[Any, Any, None]: return _xy_lifespan() app.register_lifespan_task(_lifespan) - _namespace = namespace - return namespace + _channel = channel + _setup_app = app + return channel def _ensure_page_plans(app: Any) -> None: @@ -129,11 +133,11 @@ def _ensure_page_plans(app: Any) -> None: raise RuntimeError(msg) -def wire(namespace: XYNamespace) -> None: - """Point the registry's fan-out seams at a namespace (setup and tests).""" - registry.on_publish(namespace.broadcast_payload) - registry.on_push(namespace.broadcast_message) - registry.on_error(namespace.broadcast_error) +def wire(channel: XYChannel) -> None: + """Point the registry's fan-out seams at the data plane (setup and tests).""" + registry.on_publish(channel.broadcast_payload) + registry.on_push(channel.broadcast_message) + registry.on_error(channel.broadcast_error) async def _xy_lifespan() -> None: @@ -331,6 +335,7 @@ def clear_selection(token: "str | FigureHandle") -> None: def reset_setup_for_tests() -> None: - """Forget the wired namespace (test isolation only).""" - global _namespace - _namespace = None + """Forget the wired data plane (test isolation only).""" + global _channel, _setup_app + _channel = None + _setup_app = None diff --git a/python/reflex_xy/assets/XYChart.jsx b/python/reflex_xy/assets/XYChart.jsx index 9bab7c54c..7aebe4bad 100644 --- a/python/reflex_xy/assets/XYChart.jsx +++ b/python/reflex_xy/assets/XYChart.jsx @@ -6,18 +6,18 @@ // `plan` digest + `data` ({token} DataHandle), composed client-side as // `xyp1||` once the data handle hydrates. // -// Live — this component does NOT open its own connection. -// socket.io multiplexing reuses the app's engine.io websocket when the -// manager options match, so `xySocket()` below constructs its `/_xy` -// namespace socket with exactly the options Reflex's own `connect()` uses -// (`$/utils/state`). Whichever side runs first creates the shared manager; -// the other rides it. One TCP connection carries app state and chart data — -// same lifecycle, same auth surface, same proxy config. +// Live — this component does NOT open its own connection. `getChannel("/_xy")` +// multiplexes the data plane onto the app's own event websocket as a Reflex +// channel (data_plane.py), so one TCP connection carries app state and chart +// data: same lifecycle, same auth surface, same proxy config. Columns arrive +// beside the metadata as frame attachments — aligned typed-array views over the +// received frame, never copies and never JSON numbers. // -// Live data protocol (namespace.py): +// Live data protocol (data_plane.py); `buffers` is the handler's second +// argument, not a field of the envelope: // out: sub {fig, px, mid} | unsub {fig, mid} | msg {fig, v?, mid, m} -// in: payload {fig, version, spec, buffers, mid?} — direct replies carry mid -// msg {fig, version?, mid?, message, buffers} — replies carry our mid +// in: payload {fig, version, spec, mid?} — direct replies carry mid +// msg {fig, version?, mid?, message} — replies carry our mid // err {fig, error, resync?} // A subscribe/reconnect starts a version epoch: no msg is applied until its // authoritative payload arrives. Replies and room-wide pushes carry the @@ -40,10 +40,7 @@ import { // emotion's jsx() (a guaranteed app dependency) honors it without relying // on any jsxImportSource configuration in the app's build. import { jsx } from "@emotion/react"; -import io from "socket.io-client"; -import env from "$/env.json"; -import reflexEnvironment from "$/reflex.json"; -import { getBackendURL, getToken } from "$/utils/state"; +import { getChannel } from "$/utils/state"; import { ChartView, decodeFrame, renderStandalone } from "./xy_client.js"; // Opt-in console tracing: localStorage.setItem("xy_debug", "1") @@ -59,31 +56,23 @@ const dbg = (...args) => ); dbg("XYChart module loaded"); -let sharedSocket = null; +let sharedPlane = null; // fig token -> number of mounted charts using it (unsub only at zero, since // room membership is per-connection, not per-mount). const subCounts = new Map(); -function xySocket() { - if (sharedSocket) return sharedSocket; - const endpoint = getBackendURL(env.EVENT); - const nsUrl = new URL(endpoint.href); - // The URI pathname selects the socket.io *namespace*; the engine.io mount - // path stays the app's (`endpoint.pathname`), which is what keys the - // manager cache — same key as Reflex's socket, hence one physical ws. - nsUrl.pathname = "/_xy"; - nsUrl.search = ""; - sharedSocket = io(nsUrl.href, { - path: endpoint.pathname, - transports: [env.TRANSPORT], - protocols: [reflexEnvironment.version], - autoUnref: false, - query: { token: getToken() }, - reconnection: false, // the app plane owns manager reconnects - }); - sharedSocket.on("connect", () => dbg("xy namespace connected")); - sharedSocket.on("connect_error", (e) => dbg("xy connect_error", String(e))); - return sharedSocket; +// The data plane's channel name, matching XY_PLANE in data_plane.py. +const XY_PLANE = "/_xy"; + +/** + * The shared xy data plane: one Reflex channel on the app's own websocket. + * @returns The data plane handle: connected, emit(), on(), off(). + */ +function xyDataPlane() { + if (sharedPlane) return sharedPlane; + sharedPlane = getChannel(XY_PLANE); + sharedPlane.on("error", (error) => dbg("xy channel error", error)); + return sharedPlane; } let nextMountId = 1; @@ -429,12 +418,12 @@ export function XYChart(props) { }; }, [src]); - // Live mode: subscribe on the shared websocket. + // Live mode: subscribe on the shared data plane. useEffect(() => { const el = elRef.current; dbg("effect run", { token: liveToken && liveToken.slice(0, 24), hasEl: !!el }); if (!liveToken || src || !el) return undefined; - const socket = xySocket(); + const plane = xyDataPlane(); const mid = `m${nextMountId++}`; let view = null; let destroyed = false; @@ -484,16 +473,16 @@ export function XYChart(props) { // A reconnect can land on a fresh worker whose rebuilt figure starts at // version 1. Versions are monotonic only within this subscription epoch. resetEpoch(); - socket.emit("sub", { fig: liveToken, px: el.clientWidth || null, mid }); + plane.emit("sub", { fig: liveToken, px: el.clientWidth || null, mid }); }; const emitMessage = (m) => { - // socket.io flushes its sendBuffer before firing `connect`; never queue - // an old-epoch request while the namespace is disconnected. - if (awaitingPayload || !socket.connected) return; + // Both transports buffer sends made while disconnected; never queue an + // old-epoch request that would arrive after a reconnect. + if (awaitingPayload || !plane.connected) return; const envelope = { fig: liveToken, mid, m }; if (payloadVersion !== null) envelope.v = payloadVersion; - socket.emit("msg", envelope); + plane.emit("msg", envelope); }; const withSelectionSeq = (m) => ({ @@ -577,7 +566,7 @@ export function XYChart(props) { // preceding epoch arm a new trailing timer after resetEpoch cleared it. if ( awaitingPayload - || !socket.connected + || !plane.connected || !cbRef.current.onViewChange || m.source === "linked" || m.source === "republish" @@ -599,7 +588,7 @@ export function XYChart(props) { dispatchView(m); return; } - if (awaitingPayload || !socket.connected) return; + if (awaitingPayload || !plane.connected) return; if (m.type === "select" || m.type === "select_polygon" || m.type === "select_clear") { m = withSelectionSeq(m); pendingSelectionSeqs.add(m.seq); @@ -637,7 +626,9 @@ export function XYChart(props) { }; const toSpans = (spec, buffers) => { - const spans = (buffers || []).map((b) => new Uint8Array(b)); + const spans = (buffers || []).map( + (b) => (b instanceof Uint8Array ? b : new Uint8Array(b)), + ); return spec.buffer_layout === "split" ? spans : spans[0]; }; @@ -684,13 +675,13 @@ export function XYChart(props) { invalidateSelectionReplies(); }; - const deferStatePush = (data, message) => { + const deferStatePush = (data, message, buffers) => { // Programmatic view/selection pushes are not part of the authoritative // figure payload. Preserve only generation-stamped room events; replies // and appends remain unsafe to defer across payload mounts. const generation = statePushGeneration(data, message); if (generation !== null) { - pendingStatePushes.push({ generation, message, buffers: data.buffers || [] }); + pendingStatePushes.push({ generation, message, buffers }); return true; } return false; @@ -711,7 +702,7 @@ export function XYChart(props) { generation === payloadGeneration && selectionPushReplacesSelection(message) ); - const onPayload = (data) => { + const onPayload = (data, buffers) => { if (destroyed || !data || data.fig !== liveToken) return; // Direct subscription replies are mount-addressed; room-wide rebuild // broadcasts intentionally omit mid and remain visible to every mount. @@ -776,7 +767,7 @@ export function XYChart(props) { : selectionRequest(selectionToRestore); payloadVersion = nextPayloadVersion; const spec = withHoverFlag(eventSpec(data.spec, cbRef.current)); - const nextBuffers = toSpans(data.spec, data.buffers); + const nextBuffers = toSpans(data.spec, buffers); const chromeChanged = Boolean(view && !sameMountedChromeSpec(view.spec, spec)); if (!chromeChanged && view?.updatePayload?.(spec, nextBuffers)) { // updatePayload re-homes the viewport and rebuilds trace state, so pin @@ -820,14 +811,14 @@ export function XYChart(props) { replayPendingStatePushes(nextPayloadVersion); }; - const onMsg = (data) => { + const onMsg = (data, buffers) => { if (destroyed || !data || data.fig !== liveToken) return; // Replies are mount-addressed; pushes (append) carry no mid. if (data.mid !== undefined && data.mid !== null && data.mid !== mid) return; const message = data.message; if (!message) return; if (awaitingPayload) { - if (!deferStatePush(data, message)) discardPendingReply(message); + if (!deferStatePush(data, message, buffers)) discardPendingReply(message); return; } const wireVersion = Number.isInteger(data.version) ? data.version : null; @@ -841,7 +832,7 @@ export function XYChart(props) { // or a replacement payload. Keep it until that generation mounts; // unlike a reply, it remains authoritative room state. if (selectionPushReplacesSelection(message)) invalidateSelectionReplies(); - deferStatePush(data, message); + deferStatePush(data, message, buffers); return; } if (wireVersion !== null && payloadVersion !== null) { @@ -854,7 +845,7 @@ export function XYChart(props) { // A forward gap means at least one append was not applied (for // example, an oversized binary push). Re-prime from a full payload // instead of rejecting every subsequent append forever. - if (isAppendPush && wireVersion > expected && socket.connected) subscribe(); + if (isAppendPush && wireVersion > expected && plane.connected) subscribe(); return; } } @@ -922,17 +913,17 @@ export function XYChart(props) { // The append establishes the generation whose state writes may have // reached this mount first. Apply the data delta before replaying // those writes so row-backed selections see the new trace lengths. - dispatchToView(clientMessage, data.buffers || []); + dispatchToView(clientMessage, buffers); replayPendingStatePushes(wireVersion); return; } - dispatchToView(clientMessage, data.buffers || []); + dispatchToView(clientMessage, buffers); }; const onErr = (data) => { if (destroyed || !data || data.fig !== liveToken) return; console.warn(`xy: ${data.error} (fig ${data.fig})`); - if (data.resync === true && socket.connected && errResyncs < 5) { + if (data.resync === true && plane.connected && errResyncs < 5) { errResyncs += 1; subscribe(); } @@ -942,10 +933,10 @@ export function XYChart(props) { resetEpoch(); }; - socket.on("payload", onPayload); - socket.on("msg", onMsg); - socket.on("err", onErr); - socket.on("disconnect", onDisconnect); + plane.on("payload", onPayload); + plane.on("msg", onMsg); + plane.on("err", onErr); + plane.on("disconnect", onDisconnect); // Resubscribe on every (re)connect: after the app plane reconnects the // shared manager, rooms are gone and — on another backend node — the // figure itself may need a state-driven rebuild. `sub` triggers both. @@ -956,9 +947,9 @@ export function XYChart(props) { errResyncs = 0; subscribe(); }; - socket.on("connect", onConnect); + plane.on("connect", onConnect); subCounts.set(liveToken, (subCounts.get(liveToken) || 0) + 1); - if (socket.connected) subscribe(); + if (plane.connected) subscribe(); const rememberClick = (event) => { if (!cbRef.current.onPointClick) return; @@ -983,15 +974,15 @@ export function XYChart(props) { destroyed = true; if (tracksClickInput) el.removeEventListener("click", rememberClick, true); resetEpoch(); - socket.off("payload", onPayload); - socket.off("msg", onMsg); - socket.off("err", onErr); - socket.off("disconnect", onDisconnect); - socket.off("connect", onConnect); + plane.off("payload", onPayload); + plane.off("msg", onMsg); + plane.off("err", onErr); + plane.off("disconnect", onDisconnect); + plane.off("connect", onConnect); const remaining = (subCounts.get(liveToken) || 1) - 1; if (remaining <= 0) { subCounts.delete(liveToken); - if (socket.connected) socket.emit("unsub", { fig: liveToken, mid }); + if (plane.connected) plane.emit("unsub", { fig: liveToken, mid }); } else { subCounts.set(liveToken, remaining); } diff --git a/python/reflex_xy/namespace.py b/python/reflex_xy/data_plane.py similarity index 76% rename from python/reflex_xy/namespace.py rename to python/reflex_xy/data_plane.py index 8a2732432..889ca0b46 100644 --- a/python/reflex_xy/namespace.py +++ b/python/reflex_xy/data_plane.py @@ -1,28 +1,29 @@ -"""The xy data plane as a second socket.io namespace on Reflex's server. +"""The xy data plane as a Reflex channel on the app's websocket. Transport decision (spec/design/reflex-integration.md): instead of new HTTP -endpoints, the data plane multiplexes onto the app's existing engine.io -websocket as its own namespace (`/_xy`). socket.io multiplexing means the -browser keeps ONE physical connection for app state and chart data; this -namespace inherits the connection's lifecycle, origin checks, and query -token — anything the app plane gains (auth on connect, proxy config, TLS) -the data plane gets for free, because it *is* the same connection. - -Wire shape: metadata is one small JSON object per event; every data column -rides as a native socket.io binary attachment (`bytes` values below), which -the browser receives as `ArrayBuffer`s in-place. No JSON numbers for data, -no base64 (§29) — and no custom length-prefix framing needed, because the -socket.io protocol already delimits attachments. +endpoints or a second connection, the data plane multiplexes onto the app's +existing event websocket as its own *channel* (`/_xy`). The browser keeps ONE +physical connection for app state and chart data; this channel inherits the +connection's lifecycle, origin checks, and resolved client token — anything the +app plane gains (auth on connect, proxy config, TLS) the data plane gets for +free, because it *is* the same connection. + +Wire shape: metadata is one small JSON object per message, and every data +column rides *beside* it as a binary frame attachment. Reflex pads every +attachment to an 8-byte boundary, so the browser reads each column as an +aligned typed-array view over the received frame. No JSON numbers for data, no +base64 (§29) — and no custom length-prefix framing needed, because a channel +frame already delimits its attachments. Events, client -> server: sub {fig, px?, mid?} subscribe; joins the figure room, replies `payload` unsub {fig} leave the figure room msg {fig, v?, mid?, m} one channel.handle_message dispatch, reply `msg` -Events, server -> client: - payload {fig, version, spec, buffers, mid?} first paint / full refresh - msg {fig, version?, mid?, message, buffers} reply or room-wide push - err {fig, error, resync?} failure; resync requests a new `sub` +Events, server -> client (metadata; the columns are the frame's attachments): + payload {fig, version, spec, mid?} first paint / full refresh + msg {fig, version?, mid?, message} reply or room-wide push + err {fig, error, resync?} failure; resync requests a new `sub` Each successful `sub` begins an authoritative client comparison epoch. Replies and room-wide pushes carry the FigureEntry version whose data produced them. @@ -37,12 +38,12 @@ from __future__ import annotations import asyncio -import urllib.parse from collections.abc import Awaitable, Callable, Sequence from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Optional -from socketio import AsyncNamespace +import reflex as rx +from reflex.channels import MAX_MESSAGE_BUFFERS from xy.channel import handle_message @@ -53,25 +54,25 @@ if TYPE_CHECKING: from xy._figure import Figure -__all__ = ["XY_NAMESPACE", "XYNamespace"] +__all__ = ["XY_PLANE", "XYChannel"] -#: socket.io namespace for the chart data plane. A namespace name is part of -#: the socket.io protocol, not a URL: it needs no route, no mount, and no +#: Channel name for the chart data plane. A channel name is part of Reflex's +#: websocket protocol, not a URL: it needs no route, no mount, and no #: reverse-proxy entry beyond what the app's websocket already has. -XY_NAMESPACE = "/_xy" +XY_PLANE = "/_xy" # One payload/message is screen-bounded by construction (§29); these caps are # the transport's fail-closed backstop, not a tuning knob. _MAX_PX_HINT = 8192 _MIN_PX_HINT = 16 -# socket.io-parser's Decoder ships with `maxAttachments: 10`; a binary packet -# with more attachments makes the BROWSER close the whole shared websocket as -# a parse error — which the app plane then re-opens, re-subscribing every -# chart into an infinite reconnect loop. Figures stay under the limit or fall -# back to the joined single-blob payload (`buffer_layout` != "split", which -# the client's `toSpans` already handles). -_MAX_WIRE_ATTACHMENTS = 10 +# A channel frame declares its attachment count in the header, and Reflex +# refuses to encode or decode more than `MAX_MESSAGE_BUFFERS` of them on either +# side. Taking the framework's constant rather than a local copy means the two +# cannot drift into emitting a frame the client will reject. Figures stay under +# the limit or fall back to the joined single-blob payload +# (`buffer_layout` != "split", which the client's `toSpans` already handles). +_MAX_WIRE_ATTACHMENTS = MAX_MESSAGE_BUFFERS def _build_wire_payload( @@ -83,7 +84,7 @@ def _build_wire_payload( return spec, raw # Rebuild joined rather than concatenating `raw`: the split spec addresses # columns by `buf` index and carries `buffer_layout: "split"`, which the - # client rejects for a one-buffer packet. Running the emitters twice is + # client rejects for a one-buffer frame. Running the emitters twice is # safe — they only assign shipped_sel/drill state, and the caller holds # the figure lock across both builds. spec, blob = figure.build_payload(px) @@ -163,29 +164,36 @@ def _plain(value: Any) -> Any: def _buffer_bytes( buffers: Optional[Sequence[bytes | bytearray | memoryview]], -) -> list[bytes | bytearray]: - """socket.io attaches `bytes`/`bytearray` only; memoryviews must convert. +) -> list[bytes]: + """Frame attachments are `bytes`; anything else converts. This is the single wire copy of each column (the join copy the split payload layout avoids does not come back — each column converts alone). + The emitters hand over `memoryview`s, so in practice this is exactly one + copy per column and never two. """ - return [b if isinstance(b, (bytes, bytearray)) else bytes(b) for b in (buffers or [])] + return [b if isinstance(b, bytes) else bytes(b) for b in (buffers or [])] -class XYNamespace(AsyncNamespace): - """Serve registry figures to browser clients over the shared socket.""" +class XYChannel(rx.channels.Channel): + """Serve registry figures to browser clients over the app's websocket.""" + + name = XY_PLANE + + # Clients send JSON only; every column travels server -> client. + accepts_binary = False def __init__( self, registry: FigureRegistry, *, - namespace: str = XY_NAMESPACE, rebuild: Optional[RebuildHook] = None, ) -> None: - super().__init__(namespace) + super().__init__() self.registry = registry self._rebuild = rebuild - # Socket.IO may dispatch events for one SID concurrently. Serialize + self._sessions_by_sid: dict[str, rx.channels.ChannelSession] = {} + # Reflex may dispatch messages for one session concurrently. Serialize # subscribe/unsubscribe for one mount token so a slow rebuild cannot # re-add membership after a later unsubscribe. Locks are reference- # counted so hostile/abandoned token strings do not create a second @@ -202,27 +210,43 @@ def __init__( # -- connection lifecycle ------------------------------------------------ - async def on_connect(self, sid: str, environ: dict) -> None: - """Record the Reflex client token presented by this connection. + async def on_open(self, session: rx.channels.ChannelSession) -> None: + """Record the session and the Reflex client token it carries. - The engine.io connection is shared with Reflex's `/_event` namespace, - so the same `?token=` query string reaches us. Reflex treats this as a - bearer session identifier (its TokenManager links rather than - authenticates it); engine-level origin/auth policy is inherited from - the shared connection. + The websocket is shared with Reflex's own event plane, so Reflex has + already resolved the connection's `?token=`. It treats that as a bearer + session identifier (its TokenManager links rather than authenticates + it); connection-level origin/auth policy is inherited from the shared + connection. """ - query = urllib.parse.parse_qs(environ.get("QUERY_STRING", "")) - token_list = query.get("token", []) - await self.save_session(sid, {"client_token": token_list[0] if token_list else None}) + self._sessions_by_sid[session.sid] = session + session.data["client_token"] = session.client_token - async def on_disconnect(self, sid: str) -> None: - """Rooms are cleaned up by socket.io; figures outlive the socket. + async def on_close(self, session: rx.channels.ChannelSession) -> None: + """Release the session; figures outlive the socket. Deliberate: a dropped connection (reload, laptop lid, transient network) must not destroy server-side figures — the client resubscribes with the same tokens on reconnect. """ - self.registry.disconnect(sid) + self._sessions_by_sid.pop(session.sid, None) + self.registry.disconnect(session.sid) + + async def on_message( + self, + session: rx.channels.ChannelSession, + event: str, + data: Any, + buffers: list[bytes], + ) -> None: + """Dispatch one inbound message to the data plane handlers.""" + handler = { + "sub": self.on_sub, + "unsub": self.on_unsub, + "msg": self.on_msg, + }.get(event) + if handler is not None: + await handler(session.sid, data) # -- subscription ---------------------------------------------------------- @@ -239,7 +263,7 @@ async def on_sub(self, sid: str, data: Any) -> None: ) if token is None or entry is None or not self._is_connected(sid): return - await self.enter_room(sid, self._room(token)) + self._enter_room(sid, self._room(token)) if not self._is_connected(sid): return # Mirror membership only after the await-heavy join/rebuild @@ -293,15 +317,10 @@ async def _send_payload( "fig": token, "version": entry.version, "spec": spec, - "buffers": _buffer_bytes(raw), } if mid is not None: envelope["mid"] = mid - await self.emit( - "payload", - envelope, - to=sid, - ) + await self._send("payload", envelope, _buffer_bytes(raw), to=sid) async def on_unsub(self, sid: str, data: Any) -> None: token = self._token_of(data) @@ -311,7 +330,7 @@ async def on_unsub(self, sid: str, data: Any) -> None: try: async with lock: try: - await self.leave_room(sid, self._room(token)) + self._leave_room(sid, self._room(token)) finally: self.registry.unsubscribe(token, sid) finally: @@ -354,25 +373,25 @@ async def on_msg(self, sid: str, data: Any) -> None: message, buffers = reply wire_buffers = _buffer_bytes(buffers) if len(wire_buffers) > _MAX_WIRE_ATTACHMENTS: - # Never exceed the browser parser's attachment limit: one oversized - # packet closes the shared websocket for the whole app. Channel - # replies are bounded by construction, so this is a contract check. - await self.emit( - "err", {"fig": token, "error": "reply exceeds wire attachment limit"}, to=sid + # Never exceed the frame's attachment limit: Reflex refuses to + # encode it, so the reply would be lost rather than answered. + # Channel replies are bounded by construction, so this is a + # contract check. + await self._send( + "err", {"fig": token, "error": "reply exceeds wire attachment limit"}, [], to=sid ) return envelope: dict[str, Any] = { "fig": token, "version": operation_version, "message": _plain(message), - "buffers": wire_buffers, } # Replies are mount-addressed: several charts on one page share one # socket, so the client tags requests with a mount id and we echo it. mid = self._mid_of(data) if mid is not None: envelope["mid"] = mid - await self.emit("msg", envelope, to=sid) + await self._send("msg", envelope, wire_buffers, to=sid) # -- server-side pushes (append/refresh fan-out) --------------------------- @@ -386,29 +405,29 @@ async def broadcast_message( """Push one channel message to every subscriber of a figure.""" wire_buffers = _buffer_bytes(buffers) if len(wire_buffers) > _MAX_WIRE_ATTACHMENTS: - # This packet goes to a room: one oversized push would close every - # subscriber's shared websocket at once (see _MAX_WIRE_ATTACHMENTS). - # Fail loud instead of emitting it. Append pushes also ask clients - # to resubscribe because the figure advanced; view-state pushes - # carry a generation stamp but do not mutate the figure payload. - await self.emit( + # This frame goes to a room: an unencodable push would silently + # drop the update for every subscriber at once. Fail loud instead. + # Append pushes also ask clients to resubscribe because the figure + # advanced; view-state pushes carry a generation stamp but do not + # mutate the figure payload. + await self._send( "err", { "fig": token, "error": "push exceeds wire attachment limit", "resync": message.get("type") == "append" and version is not None, }, + [], room=self._room(token), ) return - envelope = { + envelope: dict[str, Any] = { "fig": token, "message": _plain(message), - "buffers": wire_buffers, } if version is not None: envelope["version"] = version - await self.emit("msg", envelope, room=self._room(token)) + await self._send("msg", envelope, wire_buffers, room=self._room(token)) async def broadcast_payload(self, token: str, entry: FigureEntry) -> None: """Push a full refreshed payload (figure rebuilt) to subscribers.""" @@ -416,17 +435,59 @@ async def broadcast_payload(self, token: str, entry: FigureEntry) -> None: spec, raw = await asyncio.to_thread(_build_entry_payload, entry) if not self.registry.is_current(token, entry): return - await self.emit( + await self._send( "payload", { "fig": token, "version": entry.version, "spec": spec, - "buffers": _buffer_bytes(raw), }, + _buffer_bytes(raw), room=self._room(token), ) + # -- transport seams ------------------------------------------------------- + + async def _send( + self, + event: str, + data: dict[str, Any], + buffers: list[bytes], + *, + to: Optional[str] = None, + room: Optional[str] = None, + ) -> None: + """Send one envelope, with its columns as the frame's attachments.""" + if to is not None: + session = self._sessions_by_sid.get(to) + if session is not None: + await session.send(event, data, buffers) + elif room is not None: + await self.send_to_room(room, event, data, buffers) + + def _enter_room(self, sid: str, room: str) -> None: + """Join a fan-out room (never suspends — see ``on_sub``'s liveness + checks, which bracket this call).""" + session = self._sessions_by_sid.get(sid) + if session is not None: + session.join(room) + + def _leave_room(self, sid: str, room: str) -> None: + """Leave a fan-out room.""" + session = self._sessions_by_sid.get(sid) + if session is not None: + session.leave(room) + + def _session_data(self, sid: str) -> dict[str, Any]: + """Read the per-connection store.""" + held = self._sessions_by_sid.get(sid) + return held.data if held is not None else {} + + def _is_connected(self, sid: str) -> bool: + """Whether the channel session is still open.""" + session = self._sessions_by_sid.get(sid) + return session is not None and session.open + # -- internals --------------------------------------------------------------- @staticmethod @@ -460,12 +521,6 @@ def _mid_of(data: Any) -> Optional[str]: return None return mid - def _is_connected(self, sid: str) -> bool: - """Whether Socket.IO still owns ``sid`` in this namespace.""" - server = getattr(self, "server", None) - manager = getattr(server, "manager", None) - return manager is not None and manager.is_connected(sid, self.namespace) - def _retain_subscription_lock(self, key: tuple[str, str]) -> asyncio.Lock: lock = self._subscription_locks.setdefault(key, asyncio.Lock()) self._subscription_lock_users[key] = self._subscription_lock_users.get(key, 0) + 1 @@ -544,7 +599,7 @@ async def _run_rebuild_attempt( try: await self.broadcast_payload(token, entry) - except Exception: # noqa: BLE001 - payload build/emit is a protocol boundary + except Exception: # noqa: BLE001 - payload build/send is a protocol boundary # A failed generation must not poison later cache hits. Remove # only the generation this attempt inserted; if a normal # replacement won meanwhile, preserve and return that entry. @@ -581,11 +636,12 @@ async def _entry_for( if token is None: return None, None, False identity = _token_identity(token) - if identity.affinity_client is not None: - session = await self.get_session(sid) - if session.get("client_token") != identity.affinity_client: - await self._err(sid, token, "figure belongs to another session") - return token, None, False + if ( + identity.affinity_client is not None + and self._session_data(sid).get("client_token") != identity.affinity_client + ): + await self._err(sid, token, "figure belongs to another session") + return token, None, False entry, rebuild_guarded = self.registry.get_with_rebuild_guard(token) attempt = self._rebuild_attempts.get(token) # A normal publish invalidates the old attempt's guard atomically with @@ -626,7 +682,7 @@ async def _err( envelope: dict[str, Any] = {"fig": token, "error": error} if resync: envelope["resync"] = True - await self.emit("err", envelope, to=sid) + await self._send("err", envelope, [], to=sid) async def broadcast_error(self, token: str, error: str, resync: bool = False) -> None: """Room-wide err frame for server-side failures with no request to @@ -634,4 +690,4 @@ async def broadcast_error(self, token: str, error: str, resync: bool = False) -> envelope: dict[str, Any] = {"fig": token, "error": error} if resync: envelope["resync"] = True - await self.emit("err", envelope, room=self._room(token)) + await self._send("err", envelope, [], room=self._room(token)) diff --git a/python/reflex_xy/registry.py b/python/reflex_xy/registry.py index 375b9e556..37f379d2d 100644 --- a/python/reflex_xy/registry.py +++ b/python/reflex_xy/registry.py @@ -69,7 +69,7 @@ class FigureEntry: # parallelize (the kernels release the GIL on the Rust side). lock: asyncio.Lock = field(default_factory=asyncio.Lock) # The unwired/headless path cannot use the asyncio lock. This lock also - # makes synchronous message construction atomic with namespace payload/ + # makes synchronous message construction atomic with data-plane payload/ # interaction kernels and append mutation/version bump. Code needing both # locks always takes ``lock`` then ``sync_lock``; sync callers take only # this lock. It remains entry-local, so unrelated figures proceed. @@ -125,7 +125,7 @@ def __init__(self, ttl_seconds: float = DEFAULT_TTL_SECONDS) -> None: # and the data-token -> {plan digest} index that lets a column # republish rebuild + broadcast every mounted dependent figure. The # index is bounded by mounted plans: entries are added when a - # composite figure binds (namespace sub) and dropped by + # composite figure binds (data-plane sub) and dropped by # _unbind_plan_if_unmounted_locked on every transition that can end # the mount — unsubscribe, disconnect, release, failed-rebuild # cleanup, TTL sweep, and a republish that finds it unmounted. @@ -138,7 +138,7 @@ def __init__(self, ttl_seconds: float = DEFAULT_TTL_SECONDS) -> None: # tokens with no subscribers leave no unbounded tombstones behind. self._evicted_versions: dict[str, int] = {} # Only rebuildable subscriptions need registry lifecycle state; - # socket.io owns rooms for both token families. The inverse indexes + # the channel owns rooms for both token families. The inverse indexes # make subscribe/unsubscribe/disconnect idempotent and bounded by live # connections. self._rebuildable_subscribers: dict[str, set[str]] = {} @@ -160,7 +160,7 @@ def __init__(self, ttl_seconds: float = DEFAULT_TTL_SECONDS) -> None: # Captured by setup(); lets sync-handler threads schedule async # broadcasts safely. None until the data plane is attached. self._loop: Optional[asyncio.AbstractEventLoop] = None - # async callback(token, entry) -> None wired by the namespace so + # async callback(token, entry) -> None wired by the data plane so # publishes reach subscribed clients without a module cycle. self._on_publish: Optional[Callable[[str, FigureEntry], Awaitable[None]]] = None # async callback(token, message, buffers, version) -> None for append @@ -293,7 +293,7 @@ def get(self, token: str) -> Optional[FigureEntry]: def get_with_rebuild_guard(self, token: str) -> tuple[Optional[FigureEntry], bool]: """Return the current entry and whether a rebuild guard remains valid. - The snapshot is atomic so namespace requests can distinguish an entry + The snapshot is atomic so data-plane requests can distinguish an entry provisionally inserted by the active rebuild from one authorized by a concurrent normal publish, which invalidates every older guard. """ @@ -695,7 +695,7 @@ def schedule_broadcast(self, token: str) -> None: """Fan a publish out to subscribers from any thread. Safe no-op before setup() (no loop yet: nobody can be subscribed - either, because the namespace is what wires the loop). + either, because the data plane is what wires the loop). """ callback = self._on_publish loop = self._loop diff --git a/python/reflex_xy/state_bridge.py b/python/reflex_xy/state_bridge.py index a451f93cc..775999b14 100644 --- a/python/reflex_xy/state_bridge.py +++ b/python/reflex_xy/state_bridge.py @@ -97,7 +97,7 @@ async def rebuild_data(app: Any, parsed: ParsedToken) -> Optional[dict[str, Any] async def rebuild_plan_figure(app: Any, composite: ParsedPlanToken) -> Optional["Figure"]: """Recover a data-bound figure: plan (local map) + columns (registry or state) + bind. Raises PlanMissError / PlanBindError for spec-aware `err` - frames; anything else fails closed in the namespace.""" + frames; anything else fails closed in the data plane.""" plan = require_plan(composite.digest) entry = registry.get_columns(composite.data_token) if entry is not None: @@ -116,7 +116,7 @@ async def rebuild_plan_figure(app: Any, composite: ParsedPlanToken) -> Optional[ def make_rebuild_hook(app: Any) -> Any: - """The namespace's RebuildHook, bound to one app instance.""" + """The data plane's RebuildHook, bound to one app instance.""" async def _rebuild(token_str: str) -> Optional["Figure"]: composite = parse_plan_token(token_str) diff --git a/python/xy/interaction.py b/python/xy/interaction.py index 77fc39a6b..f680b727a 100644 --- a/python/xy/interaction.py +++ b/python/xy/interaction.py @@ -1447,7 +1447,7 @@ def _style_tail(name: str, values: Any) -> Optional[np.ndarray]: # join copy. The spec itself names the append — `append.seq` is the apply # signal for the widget host, where the refresh rides the spec+buffers # trait update as one comm message that doubles as notebook-reopen state. - # The socket.io host wraps the same spec in an `append` message push. + # The Reflex host wraps the same spec in an `append` message push. fig._append_seq += 1 seq = fig._append_seq spec, buffers = fig.build_payload_split() diff --git a/scripts/reflex_ws_smoke.py b/scripts/reflex_ws_smoke.py index 2777b2bd5..1a7da1d2c 100644 --- a/scripts/reflex_ws_smoke.py +++ b/scripts/reflex_ws_smoke.py @@ -5,7 +5,7 @@ design: 1. ONE physical websocket to the backend carries both the app plane and the - chart data plane (socket.io namespace multiplexing) — counted via CDP. + chart data plane (Reflex channel multiplexing) — counted via CDP. 2. The charts paint real pixels from binary socket payloads (screenshot evidence; there are no HTTP data endpoints to fall back on) — including the §6 fastapi-parity drilldown chart's density surface. @@ -191,7 +191,9 @@ def backend_websockets(self) -> list[str]: for (sid, method), events in self.s._events.items(): if sid == self.sid and method == "Network.webSocketCreated": urls.extend(e.get("url", "") for e in events) - return [u for u in urls if "/_event" in u or "/_xy" in u] + # The data plane is a channel *inside* this socket, so there is one + # URL to match — a second one would mean the chart opened its own. + return [u for u in urls if "/_event" in u] def sent_ws_frames(self, needle: str) -> list[str]: """Payloads of sent websocket frames containing `needle`.""" diff --git a/scripts/verify_sdist.py b/scripts/verify_sdist.py index 168893ef4..41e75a2f8 100644 --- a/scripts/verify_sdist.py +++ b/scripts/verify_sdist.py @@ -63,7 +63,7 @@ "python/reflex_xy/assets/__init__.py", "python/reflex_xy/component.py", "python/reflex_xy/events.py", - "python/reflex_xy/namespace.py", + "python/reflex_xy/data_plane.py", "python/reflex_xy/payload_asset.py", "python/reflex_xy/py.typed", "python/reflex_xy/registry.py", diff --git a/scripts/verify_wheel.py b/scripts/verify_wheel.py index 609bc45cd..e42c27413 100644 --- a/scripts/verify_wheel.py +++ b/scripts/verify_wheel.py @@ -39,7 +39,7 @@ "reflex_xy/assets/__init__.py", "reflex_xy/component.py", "reflex_xy/events.py", - "reflex_xy/namespace.py", + "reflex_xy/data_plane.py", "reflex_xy/payload_asset.py", "reflex_xy/py.typed", "reflex_xy/registry.py", diff --git a/spec/README.md b/spec/README.md index ff69b4c78..bb292800a 100644 --- a/spec/README.md +++ b/spec/README.md @@ -52,8 +52,8 @@ Internal architecture: how the engine is built and why. flat per-axis viewport-navigation contract: capability/action/axis/source switches, `zoom_limits`, reset, and the semantic `ranges` view events. - [`reflex-integration.md`](design/reflex-integration.md) — the bundled - `xy[reflex]` integration design: figures as first-class Reflex components over a second - socket.io namespace. + `xy[reflex]` integration design: figures as first-class Reflex components + over a dedicated channel on the app's own websocket. - [`reflex-shaped-api.md`](design/reflex-shaped-api.md) — how the core package feels Reflex-shaped while keeping no Reflex dependency. - [`renderer-architecture.md`](design/renderer-architecture.md) — audit of the diff --git a/spec/api/chart-roadmap.md b/spec/api/chart-roadmap.md index df1a7764d..933b134eb 100644 --- a/spec/api/chart-roadmap.md +++ b/spec/api/chart-roadmap.md @@ -62,7 +62,7 @@ Beyond the mark set, four capability layers now ship on `main`: thread), so a kernel-less page refines instead of stretching the overview. - **Reflex-first reactive API (`python/reflex_xy/`):** xy figures as first-class Reflex components (PR #55). Chart data multiplexes onto the - app's existing websocket as a second socket.io namespace (`/_xy`) with + app's existing websocket as a dedicated Reflex channel (`/_xy`) with binary column attachments (§29 — no JSON numbers, no sidecar HTTP endpoints); the only chart state in Reflex is a token string. The `@reflex_xy.figure` computed-var decorator binds figure builders to Reflex diff --git a/spec/design/reflex-component-api-implementation.md b/spec/design/reflex-component-api-implementation.md index bfc8baaa9..ac2d422f2 100644 --- a/spec/design/reflex-component-api-implementation.md +++ b/spec/design/reflex-component-api-implementation.md @@ -202,13 +202,13 @@ Composite figure identity, minimal wire change: - The wrapper subscribes with `fig = "xyp1||"`, assembled client-side once `data.token` is non-empty. Rooms, `mid` addressing, versioning, and the attachment-cap logic in - `namespace.py` are reused unchanged — one new token prefix to parse. + `data_plane.py` are reused unchanged — one new token prefix to parse. - **`registry.py`**: column entries (token → columns + version) beside figure entries; figure cache keyed by the composite token, plus an index `data_token → {digests}` so a data republish rebuilds and broadcasts every dependent figure. Figure entries stay derived caches: TTL-sweepable, rebuildable from (plan map, data rebuild). -- **`namespace.py`**: `sub`/`msg` on `xyp1|…` → plan lookup; data resolve +- **`data_plane.py`**: `sub`/`msg` on `xyp1|…` → plan lookup; data resolve (registry hit, else rebuild); bind → publish → serve. Affinity check uses the client token embedded in the `xyd1` half. - **`state_bridge.py`**: `rebuild_data(app, parsed)` mirroring @@ -228,7 +228,7 @@ Composite figure identity, minimal wire change: - `test_factories.py` — partition + collision table, did-you-mean, TypedDict column errors (including through a foreach item var), untyped fallback, static-tier routing. -- Extend `test_socket_data_plane.py` — composite `sub`, payload, pick; +- Extend `test_data_plane.py` — composite `sub`, payload, pick; data republish fans out to all dependent plans; rebuild-on-miss for both halves; affinity refusal; plan-miss `err {resync}`. - Demo app: add one data-bound chart to `examples/reflex/` beside the diff --git a/spec/design/reflex-component-api-options.md b/spec/design/reflex-component-api-options.md index 9444091bf..f321be35e 100644 --- a/spec/design/reflex-component-api-options.md +++ b/spec/design/reflex-component-api-options.md @@ -142,7 +142,7 @@ planes: props, and small semantic events (`on_point_click`, `on_select_end`, …) — ordinary Reflex state and event handlers. Never data buffers. - **Data plane** (xy-native): binary payloads, drilldown round-trips, and - streaming appends on a second socket.io namespace (`/_xy`) **multiplexed + streaming appends on a dedicated Reflex channel (`/_xy`) **multiplexed onto the app's existing websocket**. Reflex state never sees a data byte. The pieces, end to end: diff --git a/spec/design/reflex-integration.md b/spec/design/reflex-integration.md index c18d2bfe6..0fac3bafc 100644 --- a/spec/design/reflex-integration.md +++ b/spec/design/reflex-integration.md @@ -1,20 +1,30 @@ # Reflex integration — design Status: **implementation landed** (`python/reflex_xy`, tests under -`tests/reflex_adapter/`). This document is the authoritative design; the -prototype implements it end to end over Reflex 0.9.6. The deliverable is an -integration bundled in the `xy` distribution and installed with -`xy[reflex]`. It makes a xy figure a first-class Reflex component with the +`tests/reflex_adapter/`). This document is the authoritative design. The +deliverable is an integration bundled in the `xy` distribution and installed +with `xy[reflex]`. It makes a xy figure a first-class Reflex component with the same performance contract as the notebook path: screen-bounded binary wire (§29), kernel-side canonical data (§27), stale-while-revalidate interaction (§17). +> **Unreleased dependency (temporary).** The data plane is a Reflex *channel* +> (§2), which no released Reflex ships yet — it exists on +> reflex-dev/reflex#6932. Until that lands, the `xy[reflex]` extra carries a +> git requirement for the branch and `[tool.uv.sources]` pins the whole Reflex +> workspace to it, with `[tool.hatch.metadata] allow-direct-references` to make +> the build accept that. This is deliberately unpublishable: PyPI refuses a +> direct reference, so a release cut from this tree fails at upload rather than +> shipping a version specifier that would install a Reflex where every chart +> stays blank. Replace all three with the real floor when channels are +> released. + Two decisions define this revision (superseding the HTTP-routes draft — see §8 for the audit trail): -1. **The data plane rides the app's existing websocket.** No new endpoints; - a second socket.io namespace multiplexes onto the engine.io connection - Reflex already maintains. +1. **The data plane rides the app's existing websocket.** No new endpoints and + no second connection: the `/_xy` plane is a Reflex *channel* multiplexed + onto the event websocket the app already maintains (§2). 2. **There is no figure server and no chart data in Redis.** Figures are per-process *rebuildable caches*; Reflex state (already durable and already distributed) is the only source of truth. The figure token is the @@ -33,8 +43,8 @@ state, wrong for data buffers. The integration splits every chart into: usual way. Rows and summaries are small by construction — never buffers. - **Data plane (xy-native, high-frequency, binary).** First paint, `view`/`density_view`/`pick`/`select` round-trips, streaming `append` - pushes, and full-payload refreshes — on a dedicated socket.io namespace - (`/_xy`) **carried by the same physical websocket** as the control plane. + pushes, and full-payload refreshes — on a dedicated plane named `/_xy` + **carried by the same physical websocket** as the control plane. Reflex state never sees a data byte; state diffing cost is independent of data size. @@ -54,10 +64,10 @@ proxies must be taught each route; every `/msg` pays request setup + headers; SSE is a second long-lived connection per chart with its own reconnect logic; and none of it inherits app-plane auth. The XYBF binary frame format (`python/xy/_framing.py`; versioning in [wire-protocol.md](wire-protocol.md) -§7) exists because HTTP bodies need framing — socket.io attachments -already carry length-delimited binary, so on this transport the framing +§7) exists because HTTP bodies need framing — a Reflex channel frame already +carries length-delimited binary attachments, so on this transport the framing layer disappears too. XYBF remains in `python/xy/_framing.py` (re-exported -from `xy.channel`) for HTTP/export hosts; the namespace does not use it. +from `xy.channel`) for HTTP/export hosts; the data plane does not use it. ### The cost we accept (recorded, §28 spirit) @@ -67,72 +77,67 @@ from `xy.channel`) for HTTP/export hosts; the namespace does not use it. is single-digit MB; if it ever matters, chunked payload emission (bounded frames interleaved with other traffic) fits behind the same events without protocol change. -- **Version coupling.** The wrapper mirrors Reflex's socket options - (`transports`, ws subprotocol, `?token=` query) so the manager cache - merges the connections. Those names are pinned by - `tests/reflex_adapter/test_assets.py` — a Reflex upgrade that renames - them fails loudly in CI, not silently in prod. -- **One engine.io connection per tab** stays the invariant. If a chart page +- **Version coupling.** The wrapper depends on exactly one Reflex export, + `getChannel` from `$/utils/state`, and the backend on one method, + `app.register_channel`. Both are pinned by + `tests/reflex_adapter/test_assets.py` and the adapter suite — a Reflex + upgrade that renames either fails loudly in CI, not silently in prod. This + is a deliberate narrowing: the previous socket.io carrier had to mirror + Reflex's whole `io()` option set (engine.io mount path, `transports`, ws + subprotocol, `?token=` query) just to make socket.io-client's manager cache + merge the two connections, and every one of those was a way to silently end + up with a second socket or none at all. +- **One websocket per tab** stays the invariant, now structurally rather than + by option-matching: the chart never constructs a connection. If a chart page somehow loads without state enabled there is no socket at all — but figure tokens come from state, so that page has no charts either. -## 2. Transport: a second namespace on the app's socket +## 2. Transport: the `/_xy` channel -**Backend.** Reflex builds a python-socketio `AsyncServer` at app -construction and registers its `/_event` namespace on it. The adapter -registers one more namespace on the same server: +**Backend.** Reflex serves its own events on a plain websocket and +multiplexes additional *channels* onto it. The adapter registers one: ```python -app.sio.register_namespace(XYNamespace(registry, rebuild=...)) # "/_xy" +app.register_channel(XYChannel(registry, rebuild=...)) # name "/_xy" ``` -A namespace is a socket.io protocol concept, not a URL: no route, no mount, -no proxy entry. Wiring is one line in `rxconfig.py` — -`plugins=[reflex_xy.XYPlugin()]` — whose `post_compile` hook receives the -live `App` at backend-worker startup (after the socket server exists, before -any client connects), or an explicit `reflex_xy.setup(app)` for people who -prefer it in `app.py`. A lifespan task captures the serving loop (for -thread-safe fan-out from sync handlers) and runs the registry TTL sweep. - -**Frontend.** socket.io-client caches managers by -`(protocol, host, port, engine.io path)`. The wrapper connects to namespace -`/_xy` with the *same* URL and options Reflex's `connect()` uses -(`getBackendURL(env.EVENT)`, `path: endpoint.pathname`, -`transports: [env.TRANSPORT]`, `protocols: [version]`, -`query: {token: getToken()}`) — so whichever side connects first creates the -manager and the other multiplexes onto it. One websocket in the browser's -network tab, two namespaces inside it. React effect ordering means the chart -often connects first; mirroring the options exactly is what makes that safe -(the backend sees an identical connection either way). - -Reflex owns reconnection: its `reconnect()` reopens the shared manager, our -namespace socket re-CONNECTs automatically, and every mounted chart re-`sub`s -on the `connect` event. - -**Wire shape.** Metadata is one small JSON object per event; every data -column is a `bytes` value inside it, which python-socketio hoists into -binary attachments and the browser receives as `ArrayBuffer`s *in place* — -aligned, zero-copy into `Float32Array`s. No JSON numbers for data, no -base64, no custom framing (§29 preserved; the socket.io protocol already -length-prefixes attachments). - -**Attachment cap (hard browser limit).** socket.io-parser's `Decoder` ships -with `maxAttachments: 10`; one binary packet with more attachments makes the -browser throw `"too many attachments"`, which `Manager.ondata` converts into -closing the *entire shared websocket* as a parse error — the app plane then -reconnects, every chart re-`sub`s, the oversized payload is re-sent, and the -connection loops forever with no console error. The namespace therefore never -emits more than 10 attachments per packet (`_MAX_WIRE_ATTACHMENTS`): -payloads whose split layout would exceed the cap fall back to the joined -single-blob `build_payload()` form (no `buffer_layout: "split"` in the spec; -the wrapper's `toSpans` already dispatches on that flag), trading the join -copy for staying inside the parser's budget. `msg` replies are bounded by -channel construction; the namespace enforces the same cap as a contract -check and answers `err` instead of emitting an unparseable packet — on both -the reply path and the room-wide push path (`broadcast_message`, the -`reflex_xy.append` fan-out, where one oversized packet would close every -subscriber's connection at once). Regression: -`tests/reflex_adapter/test_socket_data_plane.py::` +A channel is a Reflex protocol concept, not a URL: no route, no mount, no +proxy entry. `register_channel` raises if the app has no event websocket +(state disabled) or is not on the plain-WebSocket transport, so a +misconfiguration fails at startup rather than at first paint. The channel +declares `accepts_binary = False`: every column travels server → client, and +an inbound binary frame is refused by Reflex before it reaches a handler. + +**Frontend.** `getChannel("/_xy")` (from `$/utils/state`) returns the shared +plane handle — `connected`, `emit`, `on`, `off` — riding the app's own +websocket. No second connection exists to key, cache or race, and Reflex owns +reconnection: when it reopens the socket the channel reopens with it and every +mounted chart re-`sub`s on the `connect` event. + +**Wire shape.** Metadata is one small JSON object per message and the data +columns are its binary attachments, carried *beside* the metadata in the +frame rather than inside it. Reflex pads every attachment to an 8-byte +boundary, so each column arrives as a `Uint8Array` view over the received +frame at a 4-byte-aligned offset — zero-copy into `Float32Array`s, and the +client's `toSpans`/LOD paths already honor `byteOffset`. No JSON numbers for +data, no base64, no custom framing (§29 preserved). + +**Attachment cap.** A channel frame declares its attachment count in the +header, and Reflex refuses to encode or decode more than +`MAX_MESSAGE_BUFFERS` (64) of them — an oversized payload would not be +truncated, it would simply never arrive. `_MAX_WIRE_ATTACHMENTS` takes that +number from `reflex.channels` rather than copying it, so the two cannot drift +into emitting a frame the client will reject. Payloads whose split layout +would exceed the cap fall back to the joined single-blob `build_payload()` +form (no `buffer_layout: "split"` in the spec; the wrapper's `toSpans` already +dispatches on that flag), trading the join copy for staying inside the frame's +budget — a recorded tier decision, never silent (§28). `msg` replies are +bounded by construction; the data plane enforces the same cap as a contract +check and answers `err` instead of dropping the update, on both the reply path +and the room-wide push path (`broadcast_message`, the `reflex_xy.append` +fan-out, where one oversized frame would silently lose the update for every +subscriber at once). Regression: +`tests/reflex_adapter/test_data_plane.py::` `test_sub_over_attachment_limit_ships_single_blob`, `::test_msg_reply_over_attachment_limit_answers_err_not_msg`, and `::test_broadcast_over_attachment_limit_answers_err_not_msg`. @@ -141,17 +146,21 @@ The envelope is below; the `m` payload it carries is specified field by field in `spec/design/wire-protocol.md`. ``` -client -> server (namespace /_xy) +client -> server (channel /_xy) sub {fig, px?, mid?} subscribe; join figure room; reply `payload` unsub {fig} leave the room msg {fig, v?, mid?, m} one xy.channel.handle_message dispatch -server -> client - payload {fig, version, spec, buffers, mid?} first paint / full refresh - msg {fig, version?, mid?, message, buffers} reply or push (no mid) +server -> client (metadata; the columns are the frame's attachments) + payload {fig, version, spec, mid?} first paint / full refresh + msg {fig, version?, mid?, message} reply or push (no mid) err {fig, error, resync?} failure; resync requests a new `sub` ``` +The attachments are not a field of the envelope: `XYChannel._send` passes them +as the frame's attachment list, and on the client every handler takes +`(data, buffers)`. + `mid` is a validated optional per-mount id: several charts on a page share the socket, so direct interaction replies and direct subscription payloads echo it and other mounts ignore the addressed envelope. Pushes and full-payload @@ -160,7 +169,7 @@ from each applying all M direct subscription responses after a resync. The kernel dispatch is byte-for-byte the notebook dispatch — `xy.channel.handle_message` (§3.1 of the old draft, now shipped), run off the event loop via a worker thread (the Rust kernels release the GIL) under a -per-figure lock. Namespace payload builds and interaction dispatches take the +per-figure lock. Data plane payload builds and interaction dispatches take the generation's async lock and then its synchronous figure lock, the same order as wired append. Caller-thread view-state writes take only the synchronous lock; there is no reverse acquisition. Thus payload emitter updates such as @@ -210,7 +219,7 @@ type crosses the socket unchanged and is dispatched by the shared `lod.screen_shape`, which rejects non-finite values and clamps the rest to `[16, MAX_SCREEN_DIM]` (`MAX_SCREEN_DIM = 4096`, `xy/config.py`) — a hostile client cannot make the kernel allocate an arbitrary density texture. The bound -matters more here than in the notebook: the namespace is reachable by anyone +matters more here than in the notebook: the data plane is reachable by anyone who can reach the app, so the clamp is an access-control boundary rather than a sanity check (§3.3). @@ -979,9 +988,10 @@ different worker may safely begin again at version 1. Once a room payload has mounted, a duplicate room broadcast at that same generation is ignored; this lets canonical rebuild recovery retry delivery without clearing a rows mask that was already replayed for the generation. -While disconnected, the wrapper does not enqueue kernel messages: socket.io -flushes its send buffer before its `connect` callback, which would otherwise -send old-epoch requests ahead of the resetting `sub`. +While disconnected, the wrapper does not enqueue kernel messages: the channel +buffers sends made while it is down and flushes them before the `connect` +callback runs, which would otherwise send old-epoch requests ahead of the +resetting `sub`. The guard is `plane.connected`, read off the channel handle. ## 6. Latency budget @@ -1065,10 +1075,12 @@ python/reflex_xy/ partition, schema checks, plan/data/static mount state_bridge.py token -> state_manager -> builder/data/plan rebuild hooks - namespace.py XYNamespace: sub/unsub/msg, payload/msg/err, - affinity (incl. composite), rebuild-on-miss, - binary attachments - app.py setup(app), XYPlugin (post_compile), lifespan + data_plane.py XYChannel(rx.channels.Channel): sub/unsub/msg, + payload/msg/err, affinity (incl. composite), + rebuild-on-miss, binary attachments — the whole + protocol and its one carrier (§2) + app.py setup(app) -> app.register_channel(...), + XYPlugin (post_compile), lifespan component.py chart(figure=...) -> rx.Component (local-JSX library); typed figure/data props; static tier payload_asset.py static tier: Chart -> content-addressed XYBF @@ -1100,24 +1112,29 @@ examples/fastapi/ (repo root) the same charts + a live 100M drilldown served tests/reflex_adapter/ token/registry/var/data-var/plan/factory/bridge/ payload-asset units, component compile, framework contract pins (R1/R7/R8), and a real-websocket - integration suite (uvicorn + socketio client) - covering payload/pick/select/affinity/rebuild/ - publish-broadcast/append/unsub + the composite - plan tier (fan-out, plan-miss resync, bind errs) + integration suite in test_data_plane.py (a real + rx.App under uvicorn, aiohttp speaking the channel + frame protocol) covering payload/pick/select/ + affinity/rebuild/publish-broadcast/append/unsub + + the composite plan tier (fan-out, plan-miss + resync, bind errs); test_channel_browser.py + asserts in a real browser that it shares the app's + one websocket ``` `inline()` (content-addressed pinned tokens, §3.4) lives in the package root beside `register()`/`release()`. The core `python/xy` package itself stays Reflex-free (CLAUDE.md rule). -`xy[reflex]` adds full `reflex>=0.9.6` for now — the `reflex-base` split covers -components/vars but not yet App/state-manager access; revisit when a smaller -supported surface exists. +`xy[reflex]` adds full `reflex` for now — the `reflex-base` split covers +components/vars but not yet App/state-manager access (nor channels); revisit +when a smaller supported surface exists. The extra currently names the +unreleased channel branch rather than a version floor; see § Status. **Versioning & releases.** The integration ships in every `xy` wheel and sdist, so it shares the core's version and bare `vX.Y.Z` release tags. The published -extra is dependency metadata only: it adds the supported Reflex floor without -creating another distribution or release pipeline. +extra is dependency metadata only: it adds the supported Reflex requirement +without creating another distribution or release pipeline. ## 8. Superseded: the HTTP-routes draft diff --git a/spec/design/renderer-architecture.md b/spec/design/renderer-architecture.md index 79336f1d5..b4d0d7b5a 100644 --- a/spec/design/renderer-architecture.md +++ b/spec/design/renderer-architecture.md @@ -81,7 +81,7 @@ relative mass, not as a budget (see §3 on why a line count failed as a metric). buffer list — this skips the join copy, the largest allocation of a direct-tier build. Split is what both shipping hosts use (`build_payload_split` in `python/xy/widget.py` and - `python/reflex_xy/namespace.py`); packed remains for standalone + `python/reflex_xy/data_plane.py`); packed remains for standalone export and for streaming `append`, which always re-ships packed (`python/xy/interaction.py`). `payloadBuffers` (`60_entries.ts`) and `_columnView` (`50_chartview.ts`) treat any spec/transport disagreement as a diff --git a/spec/design/view-state.md b/spec/design/view-state.md index e19de5149..cb8e90d46 100644 --- a/spec/design/view-state.md +++ b/spec/design/view-state.md @@ -359,7 +359,7 @@ are client-local (§4, §5.2). `view_nav` carries only `reset`, which is well-defined for every receiver because home ranges are client-known. All three reuse the existing `msg` envelope in both transports (anywidget -comm and the `/_xy` socket.io namespace), so Reflex room broadcast and +comm and the `/_xy` Reflex channel), so Reflex room broadcast and notebook delivery need no new plumbing. Hover payloads (§7) ride the existing hover/pick messages — no new message, larger detail. diff --git a/spec/design/wire-protocol.md b/spec/design/wire-protocol.md index c44fd4066..b6dd6def9 100644 --- a/spec/design/wire-protocol.md +++ b/spec/design/wire-protocol.md @@ -4,7 +4,7 @@ Status: **shipped**. This document specifies the message catalog dispatched by `xy.channel.handle_message` (`python/xy/channel.py`) and consumed by `js/src/54_kernel.ts`, plus the first-paint buffer layouts and the version handshake. The transport envelopes that carry these messages are separate: -the anywidget comm (`python/xy/widget.py`), the `/_xy` socket.io namespace +the anywidget comm (`python/xy/widget.py`), the `/_xy` Reflex channel ([reflex-integration.md](reflex-integration.md) §2), and the `XYBF` binary frame (`python/xy/_framing.py`, versioned in §7 below). @@ -342,7 +342,7 @@ omitted from the message clear; never pushed to history, reported by (view-state.md §5.1). All three ride the existing `msg` envelope in both transports (anywidget -comm and the `/_xy` socket.io namespace) behind the version handshake. +comm and the `/_xy` Reflex channel) behind the version handshake. ## 5. First-paint buffer layout: packed vs split @@ -361,8 +361,8 @@ spec's `columns` table is the addressing scheme, and it comes in two layouts: a `u8` column is folded into that column's own buffer, and `len` still counts only real values, so split is a byte-identical repack of packed. This is what both live hosts ship at first paint — `FigureWidget` - (`python/xy/widget.py`) and the `/_xy` namespace - (`python/reflex_xy/namespace.py`) — and on streaming append (§4), + (`python/xy/widget.py`) and the `/_xy` channel + (`python/reflex_xy/data_plane.py`) — and on streaming append (§4), with no join copy anywhere on a live path. The browser accepts genuine `ArrayBuffer` objects across JavaScript realm @@ -449,8 +449,8 @@ that progress and starts no animation clock. ## 6. Chunked base64 (standalone export only) -The comm and socket.io transports carry binary attachments natively and never -base64. Standalone HTML export has no binary channel, so `xy.export` embeds +The comm and Reflex channel transports carry binary attachments natively and +never base64. Standalone HTML export has no binary channel, so `xy.export` embeds the packed blob as chunked base64: - The blob is sliced into `_B64_CHUNK_BYTES` = 48 MiB pieces. That size is diff --git a/tests/reflex_adapter/test_assets.py b/tests/reflex_adapter/test_assets.py index e372222b1..e3ac83803 100644 --- a/tests/reflex_adapter/test_assets.py +++ b/tests/reflex_adapter/test_assets.py @@ -53,18 +53,22 @@ def test_link_client_creates_and_repairs(tmp_path): assert dst.resolve() == _client_source() -def test_wrapper_speaks_the_namespace_protocol(): - """The JSX wrapper and namespace.py must agree on event names and shapes.""" +def test_wrapper_speaks_the_data_plane_protocol(): + """The JSX wrapper and data_plane.py must agree on event names and shapes.""" jsx = (ADAPTER_ASSETS / "XYChart.jsx").read_text(encoding="utf-8") - # transport identity: same engine.io path as the app socket, /_xy namespace - assert 'nsUrl.pathname = "/_xy"' in jsx - assert "path: endpoint.pathname" in jsx + # transport identity: one channel name, matching XY_PLANE in data_plane.py, + # on the app's own websocket — no second connection to open or configure + assert 'const XY_PLANE = "/_xy"' in jsx + assert "getChannel(XY_PLANE)" in jsx + # socket.io is gone: no client, no namespace URL, no engine.io mount path + for gone in ("socket.io-client", "nsUrl", "endpoint.pathname", "env.TRANSPORT", "io("): + assert gone not in jsx, f"wrapper still carries socket.io machinery: {gone}" # client -> server events for needle in ('"sub"', '"unsub"', '"msg"'): - assert f"socket.emit({needle}" in jsx or f"emit({needle}" in jsx + assert f"plane.emit({needle}" in jsx # server -> client events for needle in ('"payload"', '"msg"', '"err"'): - assert f"socket.on({needle}" in jsx + assert f"plane.on({needle}" in jsx # binary columns go straight into typed arrays — never through JSON numbers assert "new Uint8Array(b)" in jsx assert "data.version < payloadVersion" in jsx @@ -93,11 +97,11 @@ def test_wrapper_speaks_the_namespace_protocol(): subscribe = jsx.split("const subscribe = () => {", 1)[1].split("};", 1)[0] assert "resetEpoch()" in subscribe assert jsx.count("resetEpoch();") == 3 # subscribe, disconnect, cleanup - assert "awaitingPayload || !socket.connected" in jsx + assert "awaitingPayload || !plane.connected" in jsx assert "awaitingPayload = false" in jsx - assert 'socket.on("disconnect", onDisconnect)' in jsx - assert "wireVersion > expected && socket.connected" in jsx - assert "data.resync === true && socket.connected" in jsx + assert 'plane.on("disconnect", onDisconnect)' in jsx + assert "wireVersion > expected && plane.connected" in jsx + assert "data.resync === true && plane.connected" in jsx # Rejected replies and accepted generation advances both reclaim pending # synthetic click/selection bookkeeping. assert "discardPendingReply(message)" in jsx @@ -119,8 +123,8 @@ def test_wrapper_speaks_the_namespace_protocol(): assert "data.mid == null" in jsx assert "Number.isInteger(data.version)" in jsx assert "DEFERRED_STATE_PUSH_TYPES.has(message.type)" in jsx - assert "pendingStatePushes.push({ generation, message, buffers: data.buffers || [] })" in jsx - assert "if (!deferStatePush(data, message)) discardPendingReply(message)" in jsx + assert "pendingStatePushes.push({ generation, message, buffers })" in jsx + assert "if (!deferStatePush(data, message, buffers)) discardPendingReply(message)" in jsx assert "const queued = pendingStatePushes.splice(0)" in jsx assert "for (const { generation, message, buffers } of queued)" in jsx assert "if (generation === payloadGeneration) dispatchToView(message, buffers)" in jsx @@ -135,8 +139,8 @@ def test_wrapper_speaks_the_namespace_protocol(): < replay_pushes.index("pendingStatePushes.push({ generation, message, buffers })") ) assert jsx.count("replayPendingStatePushes(nextPayloadVersion);") == 2 - on_payload = jsx.split("const onPayload = (data) => {", 1)[1].split( - "const onMsg = (data) => {", 1 + on_payload = jsx.split("const onPayload = (data, buffers) => {", 1)[1].split( + "const onMsg = (data, buffers) => {", 1 )[0] # Rebuild recovery may deliver both a room payload and a same-generation # addressed reply. Generic duplicates are ignored; addressed px-specific @@ -182,12 +186,14 @@ def test_wrapper_speaks_the_namespace_protocol(): # every older in-flight selection request (user gestures and payload # restores). Any eventual addressed reply is consumed before either the # Reflex callback or ChartView sees it. - on_msg = jsx.split("const onMsg = (data) => {", 1)[1].split("const onErr = (data) => {", 1)[0] + on_msg = jsx.split("const onMsg = (data, buffers) => {", 1)[1].split( + "const onErr = (data) => {", 1 + )[0] # A state write may overtake the append/full payload that establishes its # generation. Queue that write before the ordinary version mismatch path. assert ( on_msg.index("deferredPushGeneration > payloadVersion") - < on_msg.index("deferStatePush(data, message);") + < on_msg.index("deferStatePush(data, message, buffers);") < on_msg.index("if (wireVersion !== null && payloadVersion !== null)") ) assert ( @@ -203,7 +209,7 @@ def test_wrapper_speaks_the_namespace_protocol(): < on_msg.index("if (selectionWasInvalidated || !selectionWasPending) return;") < on_msg.index("const isRestore = restoreSelectionSeqs.delete(message.seq);") < on_msg.index("cbRef.current.onSelectEnd({") - < on_msg.index("dispatchToView(clientMessage, data.buffers || []);") + < on_msg.index("dispatchToView(clientMessage, buffers);") ) invalidate_replies = jsx.split("const invalidateSelectionReplies = () => {", 1)[1].split( "};", 1 @@ -220,7 +226,7 @@ def test_wrapper_speaks_the_namespace_protocol(): )[1] assert ( append_advance.index("payloadVersion = wireVersion;") - < append_advance.index("dispatchToView(clientMessage, data.buffers || []);") + < append_advance.index("dispatchToView(clientMessage, buffers);") < append_advance.index("replayPendingStatePushes(wireVersion);") < append_advance.index("return;") ) @@ -228,7 +234,7 @@ def test_wrapper_speaks_the_namespace_protocol(): # it must not arm new semantic view callbacks in that reset epoch. dispatch_view = jsx.split("const dispatchView = (m) => {", 1)[1].split("};", 1)[0] assert "awaitingPayload" in dispatch_view - assert "!socket.connected" in dispatch_view + assert "!plane.connected" in dispatch_view # the wrapper imports the sibling client copy, not a CDN or npm package assert 'from "./xy_client.js"' in jsx # static tier: fetch the payload asset, decode the XYBF frame, render @@ -246,10 +252,12 @@ def test_wrapper_speaks_the_namespace_protocol(): def test_same_generation_addressed_payload_invalidates_in_flight_replies(): """A px-specific payload can change shipped indices without a version bump.""" jsx = (ADAPTER_ASSETS / "XYChart.jsx").read_text(encoding="utf-8") - on_payload = jsx.split("const onPayload = (data) => {", 1)[1].split( - "const onMsg = (data) => {", 1 + on_payload = jsx.split("const onPayload = (data, buffers) => {", 1)[1].split( + "const onMsg = (data, buffers) => {", 1 + )[0] + on_msg = jsx.split("const onMsg = (data, buffers) => {", 1)[1].split( + "const onErr = (data) => {", 1 )[0] - on_msg = jsx.split("const onMsg = (data) => {", 1)[1].split("const onErr = (data) => {", 1)[0] invalidate = jsx.split("const invalidatePayloadReplies = () => {", 1)[1].split("};", 1)[0] for needle in ( @@ -294,7 +302,7 @@ def test_same_generation_addressed_payload_invalidates_in_flight_replies(): < on_msg.index("const selectionWasPending = pendingSelectionSeqs.delete(message.seq);") < on_msg.index("if (selectionWasInvalidated || !selectionWasPending) return;") < on_msg.index("cbRef.current.onSelectEnd({") - < on_msg.index("dispatchToView(clientMessage, data.buffers || []);") + < on_msg.index("dispatchToView(clientMessage, buffers);") ) @@ -333,17 +341,15 @@ def test_wrapper_discards_tailwind_scan_manifest_before_dom_props(): assert "...divProps" in jsx -def test_wrapper_mirrors_reflex_connection_options(): - """The shared-manager trick only works if our io() options match reflex's - connect() (utils/state.js). These names are the coupling surface — if - reflex renames them, this test is the early warning.""" +def test_wrapper_rides_the_app_socket_through_one_reflex_export(): + """`getChannel` is the whole coupling surface to Reflex's transport now. + + The chart opens no connection of its own: it asks Reflex for the `/_xy` + channel on the app's own websocket. That one import is what a Reflex + upgrade could rename, so this test is the early warning.""" jsx = (ADAPTER_ASSETS / "XYChart.jsx").read_text(encoding="utf-8") - for needle in ( - "getBackendURL(env.EVENT)", - "transports: [env.TRANSPORT]", - "protocols: [reflexEnvironment.version]", - "query: { token: getToken() }", - "autoUnref: false", - "reconnection: false", - ): - assert needle in jsx, f"wrapper lost reflex connection option: {needle}" + assert 'import { getChannel } from "$/utils/state";' in jsx + assert "sharedPlane = getChannel(XY_PLANE)" in jsx + # every handler takes the frame's attachments as its second argument + for handler in ("onPayload", "onMsg"): + assert f"const {handler} = (data, buffers) =>" in jsx diff --git a/tests/reflex_adapter/test_channel_browser.py b/tests/reflex_adapter/test_channel_browser.py new file mode 100644 index 000000000..67581c27e --- /dev/null +++ b/tests/reflex_adapter/test_channel_browser.py @@ -0,0 +1,169 @@ +"""A chart rendering in a real browser over the Reflex channel transport. + +The rest of the adapter suite drives the data plane from Python. This one +proves the thing that only a browser can: the wrapper opens no connection of +its own, the app's single websocket carries the chart's binary columns, and +the WebGL view paints from them. +""" + +from __future__ import annotations + +import json +from collections.abc import Generator +from pathlib import Path + +import pytest + +pytest.importorskip("playwright") + +from PIL import Image # noqa: E402 +from playwright.sync_api import WebSocket, sync_playwright # noqa: E402 +from reflex.testing import AppHarness # noqa: E402 + +# The app's event websocket, the one the chart has to share. +EVENT_PATH = "/_event" + +# Headless chromium has no GPU; xy's own browser probes use the same pair. +CHROMIUM_ARGS = ("--use-angle=swiftshader", "--enable-unsafe-swiftshader") + + +def ChannelChartApp(): + """A Reflex app with one live xy chart served over a channel.""" + import numpy as np + import reflex as rx + + import reflex_xy + import xy + + def orbits() -> "xy.Chart": + rng = np.random.default_rng(3) + count = 20_000 + theta = rng.uniform(0.0, 2.0 * np.pi, count) + radius = rng.normal(1.0, 0.05, count) + return xy.scatter_chart( + xy.scatter(radius * np.cos(theta), radius * np.sin(theta), opacity=0.6), + xy.x_axis(label="x"), + xy.y_axis(label="y"), + width="100%", + height=240, + ) + + token = reflex_xy.inline(orbits()) + + @rx.page("/") + def index(): + return rx.box( + reflex_xy.chart(token, height="240px"), + rx.text("ready", id="ready"), + ) + + app = rx.App() + reflex_xy.setup(app) + + +@pytest.fixture +def _fresh_registry(): + """Keep the registry across this module's tests. + + The package-wide autouse fixture resets it between tests, but this app + registers its figure once, when the harness imports it — a reset would + leave the subscribe with no figure to serve. + + Yields: + None; this only shadows the resetting fixture. + """ + yield + + +@pytest.fixture(scope="module") +def chart_app( + tmp_path_factory: pytest.TempPathFactory, +) -> Generator[AppHarness, None, None]: + """Build and serve the chart app. + + Args: + tmp_path_factory: pytest fixture for creating temporary directories. + + Yields: + The running harness. + """ + with AppHarness.create( + root=tmp_path_factory.mktemp("channel_chart_app"), + app_source=ChannelChartApp, + ) as harness: + assert harness.app_instance is not None, "app is not running" + yield harness + + +def test_chart_paints_from_channel_binary(chart_app: AppHarness, tmp_path: Path): + """The chart paints, and its columns arrive on the app's own websocket. + + Args: + chart_app: The running harness. + tmp_path: Where to drop a screenshot of the painted chart. + """ + assert chart_app.frontend_url is not None + sockets: list[WebSocket] = [] + binary_frames: list[int] = [] + + with sync_playwright() as playwright: + browser = playwright.chromium.launch(args=list(CHROMIUM_ARGS)) + page = browser.new_page(viewport={"width": 1024, "height": 768}) + + def watch(socket: WebSocket) -> None: + sockets.append(socket) + if EVENT_PATH not in socket.url: + return # the dev server's hot-reload channel + socket.on( + "framereceived", + lambda payload: ( + binary_frames.append(len(payload)) if isinstance(payload, bytes) else None + ), + ) + + page.on("websocket", watch) + page.goto(chart_app.frontend_url) + page.wait_for_selector("#ready") + # The chart only reaches "ready" once a payload has been applied. + # A cold dev server still has xy's WebGL client bundle to transform, + # which outlasts the default wait by a wide margin. + chart = page.locator('[data-xy-slot="root"]') + chart.wait_for(state="visible", timeout=120_000) + page.wait_for_function( + "!!document.querySelector('[data-xy-context-state=\"ready\"]')", + timeout=120_000, + ) + # The accessible summary is computed from the decoded columns. + summary = page.locator('[id$="-summary"]').inner_text() + shot = tmp_path / "chart.png" + chart.screenshot(path=str(shot)) + browser.close() + + # One connection to the backend for the whole page: the chart multiplexes + # onto the app's event websocket instead of opening a data plane of its + # own. (The other socket is the dev server's hot-reload channel, which a + # built app does not have.) + backend_sockets = [socket.url for socket in sockets if EVENT_PATH in socket.url] + assert len(backend_sockets) == 1, backend_sockets + # Columns travelled as binary frames on that socket, not as JSON numbers + # or base64. + assert binary_frames, "no binary frame arrived on the app websocket" + assert max(binary_frames) > 10_000, ( + f"binary frames look too small for 20k points: {sorted(binary_frames)[-3:]}" + ) + # Axis ranges in the accessible summary are derived from the columns, so + # they only exist if the binary payload decoded into real data. + assert "ranges from" in summary, summary + # And the chart actually painted: a blank mount is one flat colour. + with Image.open(shot) as image: + colours = len(image.convert("RGB").getcolors(maxcolors=1 << 20) or []) + assert colours > 50, f"chart looks blank: {colours} distinct colours" + print( + json.dumps( + { + "summary": summary.replace("\n", " ")[:120], + "colours": colours, + "binary_frames": binary_frames, + } + ) + ) diff --git a/tests/reflex_adapter/test_socket_data_plane.py b/tests/reflex_adapter/test_data_plane.py similarity index 72% rename from tests/reflex_adapter/test_socket_data_plane.py rename to tests/reflex_adapter/test_data_plane.py index 3db9c720e..7a2910979 100644 --- a/tests/reflex_adapter/test_socket_data_plane.py +++ b/tests/reflex_adapter/test_data_plane.py @@ -1,12 +1,13 @@ """End-to-end data plane over a real websocket. -Boots the same server stack a Reflex backend uses — python-socketio -AsyncServer (with reflex's JSON config) + engine.io ASGI app mounted at -/_event under uvicorn — registers XYNamespace exactly like `setup(app)` -does, and drives it with the real socket.io client protocol. This is the -transport contract the browser wrapper (XYChart.jsx) relies on, minus the -browser: spec as JSON, columns as native binary attachments, replies -mount-addressed, tokens session-affine, registry misses rebuilt. +Boots the same server stack a Reflex backend uses — a real `rx.App()` served by +uvicorn, with the data plane registered through `app.register_channel(...)` +exactly as `setup(app)` does — and drives it with the real channel wire +protocol: JSON text frames `[event, data, channel]` outbound, and binary frames +carrying the columns as attachments inbound. This is the transport contract the +browser wrapper (XYChart.jsx) relies on, minus the browser: spec as JSON, +columns as native binary attachments beside it, replies mount-addressed, tokens +session-affine, registry misses rebuilt. """ from __future__ import annotations @@ -17,20 +18,21 @@ import socket import threading from types import SimpleNamespace -from typing import TypedDict +from typing import Any, TypedDict +import aiohttp import numpy as np import pytest import reflex as rx -import socketio import uvicorn +from reflex.event_namespace import decode_channel_frame from reflex.istate.manager.memory import StateManagerMemory -from reflex_base.utils import format as reflex_format +from reflex_base.registry import RegistrationContext import reflex_xy import xy from reflex_xy.app import wire -from reflex_xy.namespace import XYNamespace +from reflex_xy.data_plane import XY_PLANE, XYChannel from reflex_xy.plan import build_plan from reflex_xy.registry import registry from reflex_xy.state_bridge import make_rebuild_hook @@ -39,6 +41,10 @@ CLIENT_TOKEN = "11111111-2222-4333-8444-555566667777" OTHER_TOKEN = "99999999-8888-4777-8666-555544443333" +# Channel frames cap at this many attachments; the data plane's own guard is +# the same number (data_plane._MAX_WIRE_ATTACHMENTS takes it from reflex). +OVER_THE_CAP = 65 + def make_figure(n: int = 64): xs = np.linspace(0.0, 1.0, n) @@ -54,63 +60,156 @@ def free_port() -> int: @contextlib.asynccontextmanager async def data_plane_server(rebuild=None): - """AsyncServer configured like reflex's (app.py _setup_state) + XYNamespace.""" - sio = socketio.AsyncServer( - async_mode="asgi", - cors_allowed_origins="*", - json=SimpleNamespace( - dumps=staticmethod(reflex_format.json_dumps), loads=staticmethod(json.loads) - ), - transports=["websocket"], - allow_upgrades=False, - ) - namespace = XYNamespace(registry, rebuild=rebuild) - sio.register_namespace(namespace) - wire(namespace) + """A real Reflex app serving the xy channel on its own event websocket. + + An App claims the registration context it is built in, so this one is built + in a fork: the process context stays free for whatever app the rest of the + suite builds. + """ + with RegistrationContext.get().fork(): + app = rx.App() + app._state_manager = StateManagerMemory() + channel = XYChannel(registry, rebuild=rebuild) + app.register_channel(channel) + wire(channel) registry.attach_loop(asyncio.get_running_loop()) - asgi = socketio.ASGIApp(sio, socketio_path="/_event") port = free_port() - config = uvicorn.Config(asgi, host="127.0.0.1", port=port, log_level="error") + config = uvicorn.Config(app._api, host="127.0.0.1", port=port, log_level="error") server = uvicorn.Server(config) task = asyncio.create_task(server.serve()) while not server.started: await asyncio.sleep(0.01) try: - yield f"http://127.0.0.1:{port}", namespace + yield f"http://127.0.0.1:{port}", channel finally: server.should_exit = True await task -async def connect_client(base_url: str, client_token: str = CLIENT_TOKEN): - """Connect the way XYChart.jsx does: /_xy namespace, token in the query.""" - client = socketio.AsyncClient(reconnection=False) - await client.connect( - f"{base_url}?token={client_token}", - socketio_path="/_event", - namespaces=["/_xy"], - transports=["websocket"], - ) - return client - +class PlaneClient: + """One connected websocket with the `/_xy` channel open. -class Collector: - """Buffers events from one client for ordered assertions.""" + Mirrors what XYChart.jsx does through `getChannel("/_xy")`: the app's own + event websocket, one `_open` for the channel, and every data plane frame + arriving as `(metadata, attachments)`. + """ - def __init__(self, client: socketio.AsyncClient) -> None: + def __init__(self, session: aiohttp.ClientSession, ws: aiohttp.ClientWebSocketResponse): + self._session = session + self._ws = ws + self.connected = True self.payloads: asyncio.Queue = asyncio.Queue() self.messages: asyncio.Queue = asyncio.Queue() self.errors: asyncio.Queue = asyncio.Queue() - client.on("payload", self.payloads.put, namespace="/_xy") - client.on("msg", self.messages.put, namespace="/_xy") - client.on("err", self.errors.put, namespace="/_xy") + self._opened = asyncio.Event() + self._queues = { + "payload": self.payloads, + "msg": self.messages, + "err": self.errors, + } + self._reader = asyncio.create_task(self._read_forever()) + + async def _read_forever(self) -> None: + with contextlib.suppress(asyncio.CancelledError, ConnectionResetError): + async for frame in self._ws: + if frame.type == aiohttp.WSMsgType.BINARY: + event, data, channel, buffers = decode_channel_frame(frame.data) + elif frame.type == aiohttp.WSMsgType.TEXT: + decoded = json.loads(frame.data) + event = decoded[0] + data = decoded[1] if len(decoded) > 1 else None + channel = decoded[2] if len(decoded) > 2 else None + buffers = [] + else: + continue + if event == "_ping": + await self._ws.send_str(json.dumps(["_pong"])) + continue + if event == "_opened" and channel == XY_PLANE: + self._opened.set() + continue + queue = self._queues.get(event) if channel == XY_PLANE else None + if queue is not None: + queue.put_nowait((data, buffers)) + + async def emit(self, event: str, data: Any) -> None: + """Send one data plane message on the `/_xy` channel.""" + await self._ws.send_str(json.dumps([event, data, XY_PLANE])) + + async def open_plane(self) -> None: + await self.emit("_open", None) + await asyncio.wait_for(self._opened.wait(), 5.0) + + async def disconnect(self) -> None: + if not self.connected: + return + self.connected = False + self._reader.cancel() + with contextlib.suppress(asyncio.CancelledError): + await self._reader + await self._ws.close() + await self._session.close() + + +async def connect_client(base_url: str, client_token: str = CLIENT_TOKEN) -> PlaneClient: + """Connect the way XYChart.jsx does: the app socket, token in the query.""" + session = aiohttp.ClientSession() + ws = await session.ws_connect(f"{base_url.replace('http', 'ws')}/_event?token={client_token}") + client = PlaneClient(session, ws) + await client.open_plane() + return client + + +class Collector: + """Buffers events from one client for ordered assertions. + + Every queue holds `(metadata, attachments)`: the two halves of a channel + frame, kept apart the way the browser receives them. + """ + + def __init__(self, client: PlaneClient) -> None: + self.payloads = client.payloads + self.messages = client.messages + self.errors = client.errors @staticmethod async def next(queue: asyncio.Queue, timeout: float = 5.0): return await asyncio.wait_for(queue.get(), timeout) +class FakeSession: + """A channel session for the tests that drive handlers directly. + + Registering one through `on_open` means the real transport seams (rooms, + per-connection store, liveness) stay under test instead of being patched + out; only the send paths are recorded. + """ + + def __init__(self, sid: str, client_token: str = CLIENT_TOKEN) -> None: + self.sid = sid + self.client_token = client_token + self.data: dict[str, Any] = {} + self.open = True + self.rooms: set[str] = set() + self.sent: list[tuple[str, Any, list]] = [] + + def join(self, room: str) -> None: + self.rooms.add(room) + + def leave(self, room: str) -> None: + self.rooms.discard(room) + + async def send(self, event: str, data: Any, buffers: list) -> None: + self.sent.append((event, data, list(buffers))) + + +async def open_fake_session(channel: XYChannel, sid: str, **kwargs) -> FakeSession: + session = FakeSession(sid, **kwargs) + await channel.on_open(session) # type: ignore[arg-type] + return session + + def run(coro): return asyncio.run(asyncio.wait_for(coro, 60.0)) @@ -138,7 +237,7 @@ def __exit__(self, *_exc): def test_payload_build_serializes_with_view_push_per_figure(_fresh_registry, monkeypatch): """Payload emitter state and row-mask construction cannot overlap.""" - import reflex_xy.namespace as namespace_module + import reflex_xy.data_plane as data_plane_module primary = _fresh_registry.publish("primary", make_figure(8), broadcast=False) other = _fresh_registry.publish("other", make_figure(8), broadcast=False) @@ -161,17 +260,17 @@ def other_view(_figure): other_view_entered.set() return {"type": "state_patch"}, [] - namespace = XYNamespace(_fresh_registry) + channel = XYChannel(_fresh_registry) - async def emit(*_args, **_kwargs): + async def send(*_args, **_kwargs): return None - monkeypatch.setattr(namespace_module, "_build_wire_payload", blocked_payload) - monkeypatch.setattr(namespace, "emit", emit) + monkeypatch.setattr(data_plane_module, "_build_wire_payload", blocked_payload) + monkeypatch.setattr(channel, "_send", send) async def main(): tasks = [] - payload_task = asyncio.create_task(namespace._send_payload("sid", "primary", primary)) + payload_task = asyncio.create_task(channel._send_payload("sid", "primary", primary)) tasks.append(payload_task) try: assert await asyncio.to_thread(payload_started.wait, 1.0) @@ -206,7 +305,7 @@ async def main(): def test_interaction_serializes_with_view_push_per_figure(_fresh_registry, monkeypatch): """Interaction/drill state and caller-thread view writes cannot overlap.""" - import reflex_xy.namespace as namespace_module + import reflex_xy.data_plane as data_plane_module primary = _fresh_registry.publish("primary", make_figure(8), broadcast=False) other = _fresh_registry.publish("other", make_figure(8), broadcast=False) @@ -229,13 +328,13 @@ def other_view(_figure): other_view_entered.set() return {"type": "state_patch"}, [] - namespace = XYNamespace(_fresh_registry) - monkeypatch.setattr(namespace_module, "handle_message", blocked_interaction) + channel = XYChannel(_fresh_registry) + monkeypatch.setattr(data_plane_module, "handle_message", blocked_interaction) async def main(): tasks = [] interaction_task = asyncio.create_task( - namespace.on_msg( + channel.on_msg( "sid", {"fig": "primary", "v": primary.version, "m": {"type": "pick"}}, ) @@ -276,8 +375,8 @@ async def main(): async with data_plane_server() as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": token, "px": 640, "mid": "m1"}, namespace="/_xy") - payload = await collector.next(collector.payloads) + await client.emit("sub", {"fig": token, "px": 640, "mid": "m1"}) + payload, buffers = await collector.next(collector.payloads) await client.disconnect() assert payload["fig"] == token assert payload["version"] == 1 @@ -285,9 +384,9 @@ async def main(): spec = payload["spec"] assert spec["buffer_layout"] == "split" assert len(spec["traces"]) == 1 - buffers = payload["buffers"] - # Binary columns arrive as raw bytes (the JS client sees ArrayBuffers): - # no base64, no JSON numbers (§29 preserved across this transport). + # Binary columns arrive as the frame's attachments, never inside the + # envelope: no base64, no JSON numbers (§29 preserved on this transport). + assert "buffers" not in payload assert all(isinstance(b, (bytes, bytearray)) for b in buffers) xcol = np.frombuffer(buffers[0], dtype=np.float32) assert len(xcol) == 64 @@ -296,31 +395,31 @@ async def main(): def test_sub_over_attachment_limit_ships_single_blob(_fresh_registry): - """socket.io-parser's browser Decoder defaults to `maxAttachments: 10` and - closes the WHOLE shared websocket ("too many attachments" -> "parse - error") on any binary packet exceeding it — which then reconnect-loops - the app. Buffer-heavy figures must fall back to the joined single-blob - payload, which the wrapper's `toSpans` handles via `buffer_layout`.""" + """A channel frame declares its attachment count, and Reflex refuses to + encode or decode more than MAX_MESSAGE_BUFFERS of them. Buffer-heavy + figures must fall back to the joined single-blob payload, which the + wrapper's `toSpans` handles via `buffer_layout`.""" async def main(): xs = np.linspace(0.0, 1.0, 64) + # Four buffers per trace (x, y, color, size): 17 traces clears the cap. figure = xy.scatter_chart( - *[xy.scatter(xs, xs * k, color=xs, size=xs) for k in (1.0, 2.0, 3.0)], + *[xy.scatter(xs, xs * k, color=xs, size=xs) for k in range(1, 18)], width=640, height=400, ).figure() _, raw = figure.build_payload_split(640) - assert len(raw) > 10, "premise: this figure must exceed the parser cap" + assert len(raw) >= OVER_THE_CAP, "premise: this figure must exceed the frame cap" token = registry.register(figure) async with data_plane_server() as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": token, "px": 640}, namespace="/_xy") - payload = await collector.next(collector.payloads) + await client.emit("sub", {"fig": token, "px": 640}) + payload, buffers = await collector.next(collector.payloads) await client.disconnect() assert payload["fig"] == token assert payload["spec"].get("buffer_layout") != "split" - assert len(payload["buffers"]) == 1 + assert len(buffers) == 1 run(main()) @@ -330,7 +429,7 @@ async def main(): [("append", True), ("selection_rows", False)], ) def test_broadcast_over_attachment_limit_answers_err_not_msg(_fresh_registry, message_type, resync): - """Room pushes over the parser cap fail loud without an invalid packet. + """Room pushes over the frame cap fail loud without an unencodable frame. Only append needs a payload resync: a generation-stamped view-state push does not itself advance the figure. @@ -338,15 +437,15 @@ def test_broadcast_over_attachment_limit_answers_err_not_msg(_fresh_registry, me async def main(): token = registry.register(make_figure(16)) - async with data_plane_server() as (url, namespace): + async with data_plane_server() as (url, channel): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": token, "px": 640}, namespace="/_xy") + await client.emit("sub", {"fig": token, "px": 640}) await collector.next(collector.payloads) - await namespace.broadcast_message( - token, {"type": message_type}, [b"\x00" * 4] * 11, version=2 + await channel.broadcast_message( + token, {"type": message_type}, [b"\x00" * 4] * OVER_THE_CAP, version=2 ) - error = await collector.next(collector.errors) + error, _ = await collector.next(collector.errors) await client.disconnect() assert error["fig"] == token assert "attachment" in error["error"] @@ -358,28 +457,26 @@ async def main(): def test_msg_reply_over_attachment_limit_answers_err_not_msg(_fresh_registry, monkeypatch): """The `on_msg` reply guard: channel replies are bounded by construction, - so a reply over the parser's attachment cap is a contract violation — the - client must get an `err` envelope, never a `msg` whose packet the browser - parser would reject (closing the shared websocket).""" + so a reply over the frame's attachment cap is a contract violation — the + client must get an `err` envelope, never a `msg` frame Reflex would refuse + to encode (silently losing the reply).""" - from reflex_xy import namespace as namespace_module + from reflex_xy import data_plane as data_plane_module def oversized_reply(figure, message, buffers): - return {"kind": "pick"}, [b"\x00" * 4] * 11 + return {"kind": "pick"}, [b"\x00" * 4] * OVER_THE_CAP - monkeypatch.setattr(namespace_module, "handle_message", oversized_reply) + monkeypatch.setattr(data_plane_module, "handle_message", oversized_reply) async def main(): token = registry.register(make_figure(16)) async with data_plane_server() as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": token, "px": 640}, namespace="/_xy") + await client.emit("sub", {"fig": token, "px": 640}) await collector.next(collector.payloads) - await client.emit( - "msg", {"fig": token, "m": {"kind": "pick"}, "mid": "m1"}, namespace="/_xy" - ) - error = await collector.next(collector.errors) + await client.emit("msg", {"fig": token, "m": {"kind": "pick"}, "mid": "m1"}) + error, _ = await collector.next(collector.errors) await client.disconnect() assert error["fig"] == token assert "attachment" in error["error"] @@ -394,7 +491,7 @@ async def main(): async with data_plane_server() as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": token, "mid": "m1"}, namespace="/_xy") + await client.emit("sub", {"fig": token, "mid": "m1"}) await collector.next(collector.payloads) # pick -> exact f64 row readout, mid echoed for mount routing @@ -405,9 +502,8 @@ async def main(): "mid": "m1", "m": {"type": "pick", "trace": 0, "index": 3, "seq": 7}, }, - namespace="/_xy", ) - reply = await collector.next(collector.messages) + reply, _ = await collector.next(collector.messages) assert reply["mid"] == "m1" assert reply["message"]["type"] == "pick_result" assert reply["message"]["seq"] == 7 @@ -415,7 +511,7 @@ async def main(): assert row["x"] == pytest.approx(3 / 15) assert row["y"] == pytest.approx(3 / 15 * 3.0) - # select -> selection mask as binary buffers + # select -> selection mask as binary attachments await client.emit( "msg", { @@ -423,16 +519,15 @@ async def main(): "mid": "m1", "m": {"type": "select", "x0": 0.0, "x1": 0.5, "y0": 0.0, "y1": 3.0}, }, - namespace="/_xy", ) - sel = await collector.next(collector.messages) + sel, mask = await collector.next(collector.messages) assert sel["message"]["type"] == "selection" assert sel["message"]["total"] == 8 - assert len(sel["buffers"]) == 1 + assert len(mask) == 1 # malformed messages are dropped silently, never crash the server - await client.emit("msg", {"fig": token, "m": ["not", "a", "dict"]}, namespace="/_xy") - await client.emit("msg", "garbage", namespace="/_xy") + await client.emit("msg", {"fig": token, "m": ["not", "a", "dict"]}) + await client.emit("msg", "garbage") await client.emit( "msg", { @@ -440,9 +535,8 @@ async def main(): "mid": "m1", "m": {"type": "pick", "trace": 0, "index": 5, "seq": 8}, }, - namespace="/_xy", ) - after = await collector.next(collector.messages) + after, _ = await collector.next(collector.messages) assert after["message"]["seq"] == 8 await client.disconnect() @@ -455,7 +549,7 @@ async def main(): async with data_plane_server() as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": token, "mid": "m1"}, namespace="/_xy") + await client.emit("sub", {"fig": token, "mid": "m1"}) await collector.next(collector.payloads) await client.emit( "msg", @@ -472,9 +566,8 @@ async def main(): "include_rows": True, }, }, - namespace="/_xy", ) - reply = await collector.next(collector.messages) + reply, _ = await collector.next(collector.messages) await client.disconnect() message = reply["message"] assert message["version"] == 1 @@ -491,16 +584,14 @@ async def main(): async with data_plane_server() as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": token, "mid": "m1"}, namespace="/_xy") + await client.emit("sub", {"fig": token, "mid": "m1"}) await collector.next(collector.payloads) registry.publish(token, make_figure(16)) - payload = await collector.next(collector.payloads) + payload, _ = await collector.next(collector.payloads) assert payload["version"] == 2 message = {"type": "pick", "trace": 0, "index": 2, "seq": 21} - await client.emit( - "msg", {"fig": token, "mid": "m1", "v": 1, "m": message}, namespace="/_xy" - ) + await client.emit("msg", {"fig": token, "mid": "m1", "v": 1, "m": message}) with pytest.raises(asyncio.TimeoutError): await Collector.next(collector.messages, timeout=0.15) @@ -514,21 +605,18 @@ async def main(): "v": malformed_version, "m": message, }, - namespace="/_xy", ) with pytest.raises(asyncio.TimeoutError): await Collector.next(collector.messages, timeout=0.1) message["seq"] = 21 - await client.emit( - "msg", {"fig": token, "mid": "m1", "v": 2, "m": message}, namespace="/_xy" - ) - current = await collector.next(collector.messages) + await client.emit("msg", {"fig": token, "mid": "m1", "v": 2, "m": message}) + current, _ = await collector.next(collector.messages) assert current["message"]["seq"] == 21 message["seq"] = 22 - await client.emit("msg", {"fig": token, "mid": "m1", "m": message}, namespace="/_xy") - compatible = await collector.next(collector.messages) + await client.emit("msg", {"fig": token, "mid": "m1", "m": message}) + compatible, _ = await collector.next(collector.messages) assert compatible["message"]["seq"] == 22 await client.disconnect() @@ -544,14 +632,14 @@ def slow_handle_message(figure, content, callbacks): assert resume.wait(timeout=5) return {"type": "pick_result", "seq": 99, "row": None}, [] - monkeypatch.setattr("reflex_xy.namespace.handle_message", slow_handle_message) + monkeypatch.setattr("reflex_xy.data_plane.handle_message", slow_handle_message) async def main(): token = registry.register(make_figure(16)) async with data_plane_server() as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": token, "mid": "m1"}, namespace="/_xy") + await client.emit("sub", {"fig": token, "mid": "m1"}) await collector.next(collector.payloads) await client.emit( @@ -562,13 +650,12 @@ async def main(): "v": 1, "m": {"type": "pick", "trace": 0, "index": 2, "seq": 99}, }, - namespace="/_xy", ) assert await asyncio.to_thread(started.wait, 5) registry.publish(token, make_figure(32)) resume.set() - replacement = await collector.next(collector.payloads) + replacement, _ = await collector.next(collector.payloads) assert replacement["version"] == 2 with pytest.raises(asyncio.TimeoutError): await Collector.next(collector.messages, timeout=0.15) @@ -586,14 +673,14 @@ async def main(): # be able to subscribe to this figure. thief = await connect_client(url, client_token=OTHER_TOKEN) thief_collector = Collector(thief) - await thief.emit("sub", {"fig": state_token, "mid": "m1"}, namespace="/_xy") - err = await thief_collector.next(thief_collector.errors) + await thief.emit("sub", {"fig": state_token, "mid": "m1"}) + err, _ = await thief_collector.next(thief_collector.errors) assert "another session" in err["error"] owner = await connect_client(url, client_token=CLIENT_TOKEN) owner_collector = Collector(owner) - await owner.emit("sub", {"fig": state_token, "mid": "m1"}, namespace="/_xy") - payload = await owner_collector.next(owner_collector.payloads) + await owner.emit("sub", {"fig": state_token, "mid": "m1"}) + payload, _ = await owner_collector.next(owner_collector.payloads) assert payload["fig"] == state_token await thief.disconnect() await owner.disconnect() @@ -615,10 +702,10 @@ async def main(): async with data_plane_server(rebuild=rebuild) as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": state_token, "mid": "m1"}, namespace="/_xy") - payload = await collector.next(collector.payloads) + await client.emit("sub", {"fig": state_token, "mid": "m1"}) + payload, buffers = await collector.next(collector.payloads) assert payload["fig"] == state_token - assert len(payload["buffers"]) == 2 + assert len(buffers) == 2 await client.disconnect() assert rebuilt == [state_token] assert registry.get(state_token) is not None @@ -643,22 +730,20 @@ async def rebuild(token_str): await resume.wait() return make_figure(32) - namespace = XYNamespace(registry, rebuild=rebuild) - - async def get_session(sid): - return {"client_token": CLIENT_TOKEN} + channel = XYChannel(registry, rebuild=rebuild) async def broadcast(token, entry): broadcasts.append((token, entry)) - monkeypatch.setattr(namespace, "get_session", get_session) - monkeypatch.setattr(namespace, "broadcast_payload", broadcast) + monkeypatch.setattr(channel, "broadcast_payload", broadcast) async def main(): + await open_fake_session(channel, "sid-1") + await open_fake_session(channel, "sid-2") data = {"fig": state_token} - first = asyncio.create_task(namespace._entry_for("sid-1", data, allow_rebuild=True)) + first = asyncio.create_task(channel._entry_for("sid-1", data, allow_rebuild=True)) await started.wait() - second = asyncio.create_task(namespace._entry_for("sid-2", data, allow_rebuild=True)) + second = asyncio.create_task(channel._entry_for("sid-2", data, allow_rebuild=True)) await asyncio.sleep(0) resume.set() results = await asyncio.gather(first, second) @@ -669,7 +754,7 @@ async def main(): assert registry.is_current(state_token, entries[0]) assert all(result[2] for result in results) assert broadcasts == [(state_token, entries[0])] - assert namespace._rebuild_attempts == {} + assert channel._rebuild_attempts == {} run(main()) @@ -691,10 +776,7 @@ async def rebuild(token_str): await resume.wait() return make_figure(32) - namespace = XYNamespace(registry, rebuild=rebuild) - - async def get_session(sid): - return {"client_token": CLIENT_TOKEN} + channel = XYChannel(registry, rebuild=rebuild) async def broadcast(token, entry): assert token == state_token @@ -704,13 +786,14 @@ def handle(figure, message, buffers): handled.append((figure, message, buffers)) return None - monkeypatch.setattr(namespace, "get_session", get_session) - monkeypatch.setattr(namespace, "broadcast_payload", broadcast) - monkeypatch.setattr("reflex_xy.namespace.handle_message", handle) + monkeypatch.setattr(channel, "broadcast_payload", broadcast) + monkeypatch.setattr("reflex_xy.data_plane.handle_message", handle) async def main(): + await open_fake_session(channel, "sid-1") + await open_fake_session(channel, "sid-2") first = asyncio.create_task( - namespace.on_msg( + channel.on_msg( "sid-1", {"fig": state_token, "m": {"type": "pick", "trace": 0, "index": 1}}, ) @@ -719,7 +802,7 @@ async def main(): # Version 1 also matches a fresh worker's rebuilt version. The request # must still drop because it was sent before that worker's payload. second = asyncio.create_task( - namespace.on_msg( + channel.on_msg( "sid-2", { "fig": state_token, @@ -735,7 +818,7 @@ async def main(): assert rebuild_calls == 1 assert handled == [] - assert namespace._rebuild_attempts == {} + assert channel._rebuild_attempts == {} run(main()) @@ -759,10 +842,7 @@ async def rebuild(token_str): return None return make_figure(32) - namespace = XYNamespace(registry, rebuild=rebuild) - - async def get_session(sid): - return {"client_token": CLIENT_TOKEN} + channel = XYChannel(registry, rebuild=rebuild) async def send_error(sid, token, error, resync=False): errors.append((sid, token, error)) @@ -770,15 +850,16 @@ async def send_error(sid, token, error, resync=False): async def broadcast(token, entry): broadcasts.append((token, entry)) - monkeypatch.setattr(namespace, "get_session", get_session) - monkeypatch.setattr(namespace, "_err", send_error) - monkeypatch.setattr(namespace, "broadcast_payload", broadcast) + monkeypatch.setattr(channel, "_err", send_error) + monkeypatch.setattr(channel, "broadcast_payload", broadcast) async def main(): + for sid in ("sid-1", "sid-2", "sid-3"): + await open_fake_session(channel, sid) data = {"fig": state_token} - first = asyncio.create_task(namespace._entry_for("sid-1", data, allow_rebuild=True)) + first = asyncio.create_task(channel._entry_for("sid-1", data, allow_rebuild=True)) await started.wait() - second = asyncio.create_task(namespace._entry_for("sid-2", data, allow_rebuild=True)) + second = asyncio.create_task(channel._entry_for("sid-2", data, allow_rebuild=True)) await asyncio.sleep(0) resume.set() first_results = await asyncio.gather(first, second) @@ -789,9 +870,9 @@ async def main(): assert sorted(sid for sid, _, _ in errors) == ["sid-1", "sid-2"] assert all(token == state_token for _, token, _ in errors) assert all(error == "unknown figure token" for _, _, error in errors) - assert namespace._rebuild_attempts == {} + assert channel._rebuild_attempts == {} - retried = await namespace._entry_for("sid-3", data, allow_rebuild=True) + retried = await channel._entry_for("sid-3", data, allow_rebuild=True) await asyncio.sleep(0) assert rebuild_calls == 2 assert retried[0] == state_token @@ -799,7 +880,7 @@ async def main(): assert retried[2] assert registry.is_current(state_token, retried[1]) assert broadcasts == [(state_token, retried[1])] - assert namespace._rebuild_attempts == {} + assert channel._rebuild_attempts == {} run(main()) @@ -816,7 +897,6 @@ def test_rebuild_broadcast_failure_removes_generation_and_later_sub_retries( errors = [] broadcasts = [] - joined = [] sent = [] rebuild_calls = 0 @@ -826,13 +906,7 @@ async def rebuild(token_str): rebuild_calls += 1 return make_figure(32) - namespace = XYNamespace(registry, rebuild=rebuild) - namespace._set_server( - SimpleNamespace(manager=SimpleNamespace(is_connected=lambda sid, _namespace: True)) - ) - - async def get_session(sid): - return {"client_token": CLIENT_TOKEN} + channel = XYChannel(registry, rebuild=rebuild) async def broadcast(token, entry): assert token == state_token @@ -844,36 +918,33 @@ async def broadcast(token, entry): async def send_error(sid, token, error, resync=False): errors.append((sid, token, error)) - async def enter_room(sid, room): - joined.append((sid, room)) - async def send_payload(sid, token, entry, **kwargs): sent.append((sid, token, entry, kwargs)) - monkeypatch.setattr(namespace, "get_session", get_session) - monkeypatch.setattr(namespace, "broadcast_payload", broadcast) - monkeypatch.setattr(namespace, "_err", send_error) - monkeypatch.setattr(namespace, "enter_room", enter_room) - monkeypatch.setattr(namespace, "_send_payload", send_payload) + monkeypatch.setattr(channel, "broadcast_payload", broadcast) + monkeypatch.setattr(channel, "_err", send_error) + monkeypatch.setattr(channel, "_send_payload", send_payload) async def main(): - await namespace.on_sub("sid-1", {"fig": state_token, "mid": "m1"}) + first = await open_fake_session(channel, "sid-1") + second = await open_fake_session(channel, "sid-2") + await channel.on_sub("sid-1", {"fig": state_token, "mid": "m1"}) await asyncio.sleep(0) assert errors == [("sid-1", state_token, "rebuild failed")] assert registry.get(state_token) is None assert registry._evicted_versions == {state_token: 3} - assert joined == [] + assert first.rooms == set() assert sent == [] - assert namespace._rebuild_attempts == {} + assert channel._rebuild_attempts == {} assert registry._active_rebuild_guards == {} - await namespace.on_sub("sid-2", {"fig": state_token, "mid": "m2", "px": 321}) + await channel.on_sub("sid-2", {"fig": state_token, "mid": "m2", "px": 321}) await asyncio.sleep(0) assert rebuild_calls == 2 assert [entry.version for entry in broadcasts] == [3, 4] - assert joined == [("sid-2", namespace._room(state_token))] + assert second.rooms == {channel._room(state_token)} assert len(sent) == 1 sid, token, entry, kwargs = sent[0] assert (sid, token, entry.version, kwargs) == ( @@ -883,7 +954,7 @@ async def main(): {"px": 321, "mid": "m2"}, ) assert registry.is_current(state_token, entry) - assert namespace._rebuild_attempts == {} + assert channel._rebuild_attempts == {} assert registry._active_rebuild_guards == {} run(main()) @@ -902,10 +973,7 @@ async def rebuild(token_str): assert token_str == state_token return make_figure(16) - namespace = XYNamespace(registry, rebuild=rebuild) - - async def get_session(sid): - return {"client_token": CLIENT_TOKEN} + channel = XYChannel(registry, rebuild=rebuild) async def fail_after_replacement(token, entry): nonlocal replacement @@ -917,12 +985,12 @@ async def fail_after_replacement(token, entry): async def send_error(sid, token, error, resync=False): errors.append((sid, token, error)) - monkeypatch.setattr(namespace, "get_session", get_session) - monkeypatch.setattr(namespace, "broadcast_payload", fail_after_replacement) - monkeypatch.setattr(namespace, "_err", send_error) + monkeypatch.setattr(channel, "broadcast_payload", fail_after_replacement) + monkeypatch.setattr(channel, "_err", send_error) async def main(): - token, entry, initially_missing = await namespace._entry_for( + await open_fake_session(channel, "sid-1") + token, entry, initially_missing = await channel._entry_for( "sid-1", {"fig": state_token}, allow_rebuild=True ) await asyncio.sleep(0) @@ -935,7 +1003,7 @@ async def main(): assert entry.version == 2 assert registry.is_current(state_token, entry) assert errors == [] - assert namespace._rebuild_attempts == {} + assert channel._rebuild_attempts == {} assert registry._active_rebuild_guards == {} run(main()) @@ -957,10 +1025,7 @@ async def rebuild(token_str): assert token_str == state_token return rebuilt_figure - namespace = XYNamespace(registry, rebuild=rebuild) - - async def get_session(sid): - return {"client_token": CLIENT_TOKEN} + channel = XYChannel(registry, rebuild=rebuild) async def fail_then_deliver_from_publish(token, entry): nonlocal published @@ -980,14 +1045,14 @@ async def fail_then_deliver_from_publish(token, entry): async def send_error(sid, token, error, resync=False): errors.append((sid, token, error)) - monkeypatch.setattr(namespace, "get_session", get_session) - monkeypatch.setattr(namespace, "broadcast_payload", fail_then_deliver_from_publish) - monkeypatch.setattr(namespace, "_err", send_error) + monkeypatch.setattr(channel, "broadcast_payload", fail_then_deliver_from_publish) + monkeypatch.setattr(channel, "_err", send_error) async def main(): registry.attach_loop(asyncio.get_running_loop()) - registry.on_publish(namespace.broadcast_payload) - token, entry, initially_missing = await namespace._entry_for( + registry.on_publish(channel.broadcast_payload) + await open_fake_session(channel, "sid-1") + token, entry, initially_missing = await channel._entry_for( "sid-1", {"fig": state_token}, allow_rebuild=True ) await asyncio.wait_for(delivered.wait(), timeout=1.0) @@ -1002,7 +1067,7 @@ async def main(): assert errors == [] assert fanouts == [(state_token, entry), (state_token, entry)] assert registry._pending_broadcasts == set() - assert namespace._rebuild_attempts == {} + assert channel._rebuild_attempts == {} assert registry._active_rebuild_guards == {} run(main()) @@ -1026,20 +1091,17 @@ async def rebuild(token_str): await resume.wait() return rebuilt_figure - namespace = XYNamespace(registry, rebuild=rebuild) - - async def get_session(sid): - return {"client_token": CLIENT_TOKEN} + channel = XYChannel(registry, rebuild=rebuild) async def broadcast(token, entry): broadcasts.append((token, entry)) - monkeypatch.setattr(namespace, "get_session", get_session) - monkeypatch.setattr(namespace, "broadcast_payload", broadcast) + monkeypatch.setattr(channel, "broadcast_payload", broadcast) async def main(): + await open_fake_session(channel, "sid-1") pending = asyncio.create_task( - namespace._entry_for("sid-1", {"fig": state_token}, allow_rebuild=True) + channel._entry_for("sid-1", {"fig": state_token}, allow_rebuild=True) ) await started.wait() published = registry.publish(state_token, current_figure, broadcast=False) @@ -1052,7 +1114,7 @@ async def main(): assert entry.figure is current_figure assert registry.is_current(state_token, entry) assert broadcasts == [] - assert namespace._rebuild_attempts == {} + assert channel._rebuild_attempts == {} run(main()) @@ -1066,7 +1128,6 @@ def test_later_requests_bypass_slow_rebuild_after_authoritative_publish( resume = asyncio.Event() rebuild_calls = 0 current_figure = make_figure(48) - joined = [] sent = [] handled = [] @@ -1078,16 +1139,7 @@ async def rebuild(token_str): await resume.wait() return make_figure(16) - namespace = XYNamespace(registry, rebuild=rebuild) - namespace._set_server( - SimpleNamespace(manager=SimpleNamespace(is_connected=lambda sid, _namespace: True)) - ) - - async def get_session(sid): - return {"client_token": CLIENT_TOKEN} - - async def enter_room(sid, room): - joined.append((sid, room)) + channel = XYChannel(registry, rebuild=rebuild) async def send_payload(sid, token, entry, **kwargs): sent.append((sid, token, entry, kwargs)) @@ -1099,15 +1151,16 @@ def handle(figure, message, buffers): handled.append((figure, message, buffers)) return None - monkeypatch.setattr(namespace, "get_session", get_session) - monkeypatch.setattr(namespace, "enter_room", enter_room) - monkeypatch.setattr(namespace, "_send_payload", send_payload) - monkeypatch.setattr(namespace, "broadcast_payload", unexpected_broadcast) - monkeypatch.setattr("reflex_xy.namespace.handle_message", handle) + monkeypatch.setattr(channel, "_send_payload", send_payload) + monkeypatch.setattr(channel, "broadcast_payload", unexpected_broadcast) + monkeypatch.setattr("reflex_xy.data_plane.handle_message", handle) async def main(): + await open_fake_session(channel, "sid-original") + subscriber = await open_fake_session(channel, "sid-sub") + await open_fake_session(channel, "sid-msg") original = asyncio.create_task( - namespace._entry_for("sid-original", {"fig": state_token}, allow_rebuild=True) + channel._entry_for("sid-original", {"fig": state_token}, allow_rebuild=True) ) await started.wait() assert registry._active_rebuild_guards @@ -1118,10 +1171,10 @@ async def main(): message = {"type": "pick", "trace": 0, "index": 1} later_sub = asyncio.create_task( - namespace.on_sub("sid-sub", {"fig": state_token, "mid": "m1", "px": 321}) + channel.on_sub("sid-sub", {"fig": state_token, "mid": "m1", "px": 321}) ) later_msg = asyncio.create_task( - namespace.on_msg( + channel.on_msg( "sid-msg", {"fig": state_token, "v": published.version, "m": message}, ) @@ -1135,7 +1188,7 @@ async def main(): assert not blocked_at_deadline await asyncio.gather(*later_tasks) - assert joined == [("sid-sub", namespace._room(state_token))] + assert subscriber.rooms == {channel._room(state_token)} assert sent == [("sid-sub", state_token, published, {"px": 321, "mid": "m1"})] assert handled == [(current_figure, message, None)] assert not original.done() @@ -1147,7 +1200,7 @@ async def main(): assert original_result == (state_token, published, True) assert rebuild_calls == 1 assert registry.is_current(state_token, published) - assert namespace._rebuild_attempts == {} + assert channel._rebuild_attempts == {} assert registry._active_rebuild_guards == {} run(main()) @@ -1177,10 +1230,7 @@ async def rebuild(token_str): await resume.wait() return make_figure(32) - namespace = XYNamespace(registry, rebuild=rebuild) - - async def get_session(sid): - return {"client_token": CLIENT_TOKEN} + channel = XYChannel(registry, rebuild=rebuild) async def send_error(sid, token, error, resync=False): errors.append((sid, token, error)) @@ -1188,13 +1238,14 @@ async def send_error(sid, token, error, resync=False): async def broadcast(token, entry): broadcasts.append((token, entry)) - monkeypatch.setattr(namespace, "get_session", get_session) - monkeypatch.setattr(namespace, "_err", send_error) - monkeypatch.setattr(namespace, "broadcast_payload", broadcast) + monkeypatch.setattr(channel, "_err", send_error) + monkeypatch.setattr(channel, "broadcast_payload", broadcast) async def main(): + await open_fake_session(channel, "sid-1") + await open_fake_session(channel, "sid-2") data = {"fig": state_token} - pending = asyncio.create_task(namespace._entry_for("sid-1", data, allow_rebuild=True)) + pending = asyncio.create_task(channel._entry_for("sid-1", data, allow_rebuild=True)) await started.wait() # The canonical builder evaluated to None after this rebuild began. @@ -1210,10 +1261,10 @@ async def main(): assert registry.get(state_token) is None assert registry._evicted_versions == {state_token: 2} assert broadcasts == [] - assert namespace._rebuild_attempts == {} + assert channel._rebuild_attempts == {} assert registry._active_rebuild_guards == {} - retried = await namespace._entry_for("sid-2", data, allow_rebuild=True) + retried = await channel._entry_for("sid-2", data, allow_rebuild=True) await asyncio.sleep(0) assert rebuild_calls == 2 @@ -1223,7 +1274,7 @@ async def main(): assert retried[2] assert registry.is_current(state_token, retried[1]) assert broadcasts == [(state_token, retried[1])] - assert namespace._rebuild_attempts == {} + assert channel._rebuild_attempts == {} assert registry._active_rebuild_guards == {} run(main()) @@ -1247,26 +1298,24 @@ async def rebuild(token_str): await resume_old.wait() return make_figure(32) - namespace = XYNamespace(registry, rebuild=rebuild) - - async def get_session(_sid): - return {"client_token": CLIENT_TOKEN} + channel = XYChannel(registry, rebuild=rebuild) async def broadcast(_token, _entry): return None - monkeypatch.setattr(namespace, "get_session", get_session) - monkeypatch.setattr(namespace, "broadcast_payload", broadcast) + monkeypatch.setattr(channel, "broadcast_payload", broadcast) async def main(): + await open_fake_session(channel, "sid-old") + await open_fake_session(channel, "sid-new") data = {"fig": state_token} - old = asyncio.create_task(namespace._entry_for("sid-old", data, allow_rebuild=True)) + old = asyncio.create_task(channel._entry_for("sid-old", data, allow_rebuild=True)) await started.wait() registry.release(state_token) assert not registry._active_rebuild_guards newer = await asyncio.wait_for( - namespace._entry_for("sid-new", data, allow_rebuild=True), timeout=1.0 + channel._entry_for("sid-new", data, allow_rebuild=True), timeout=1.0 ) assert rebuild_calls == 2 assert newer[1] is not None @@ -1277,7 +1326,7 @@ async def main(): old_result = await old assert old_result[1] is newer[1] await asyncio.sleep(0) - assert namespace._rebuild_attempts == {} + assert channel._rebuild_attempts == {} assert registry._active_rebuild_guards == {} run(main()) @@ -1289,7 +1338,6 @@ def test_sub_sends_current_replacement_when_publish_lands_before_join(_fresh_reg state_token = build_state_token(CLIENT_TOKEN, "root.some_state", "chart") rebuilt_figure = make_figure(16) replacement_figure = make_figure(48) - joined = [] sent = [] replacement = None @@ -1297,13 +1345,7 @@ async def rebuild(token_str): assert token_str == state_token return rebuilt_figure - namespace = XYNamespace(registry, rebuild=rebuild) - namespace._set_server( - SimpleNamespace(manager=SimpleNamespace(is_connected=lambda sid, _namespace: True)) - ) - - async def get_session(sid): - return {"client_token": CLIENT_TOKEN} + channel = XYChannel(registry, rebuild=rebuild) async def broadcast(token, entry): nonlocal replacement @@ -1313,22 +1355,18 @@ async def broadcast(token, entry): # joins. The direct response must therefore re-read this generation. replacement = registry.publish(state_token, replacement_figure, broadcast=False) - async def enter_room(sid, room): - joined.append((sid, room)) - async def send_payload(sid, token, entry, **kwargs): sent.append((sid, token, entry, kwargs)) - monkeypatch.setattr(namespace, "get_session", get_session) - monkeypatch.setattr(namespace, "broadcast_payload", broadcast) - monkeypatch.setattr(namespace, "enter_room", enter_room) - monkeypatch.setattr(namespace, "_send_payload", send_payload) + monkeypatch.setattr(channel, "broadcast_payload", broadcast) + monkeypatch.setattr(channel, "_send_payload", send_payload) async def main(): - await namespace.on_sub("sid-1", {"fig": state_token, "mid": "m1", "px": 321}) + session = await open_fake_session(channel, "sid-1") + await channel.on_sub("sid-1", {"fig": state_token, "mid": "m1", "px": 321}) assert replacement is not None - assert joined == [("sid-1", namespace._room(state_token))] + assert session.rooms == {channel._room(state_token)} assert len(sent) == 1 sid, token, entry, kwargs = sent[0] assert (sid, token) == ("sid-1", state_token) @@ -1346,7 +1384,6 @@ def test_slow_sub_does_not_restore_membership_after_disconnect(_fresh_registry, state_token = build_state_token(CLIENT_TOKEN, "root.some_state", "chart") started = asyncio.Event() resume = asyncio.Event() - connected = {"sid-gone": True} sent_payloads = [] async def rebuild(token_str): @@ -1355,18 +1392,7 @@ async def rebuild(token_str): await resume.wait() return make_figure(32) - namespace = XYNamespace(registry, rebuild=rebuild) - namespace._set_server( - SimpleNamespace( - manager=SimpleNamespace(is_connected=lambda sid, _namespace: connected.get(sid, False)) - ) - ) - - async def get_session(sid): - return {"client_token": CLIENT_TOKEN} - - async def enter_room(sid, room): - raise AssertionError("disconnected SID must not re-enter a room") + channel = XYChannel(registry, rebuild=rebuild) async def send_payload(*args, **kwargs): sent_payloads.append((args, kwargs)) @@ -1374,29 +1400,27 @@ async def send_payload(*args, **kwargs): async def broadcast(*args, **kwargs): return None - monkeypatch.setattr(namespace, "get_session", get_session) - monkeypatch.setattr(namespace, "enter_room", enter_room) - monkeypatch.setattr(namespace, "_send_payload", send_payload) - monkeypatch.setattr(namespace, "broadcast_payload", broadcast) + monkeypatch.setattr(channel, "_send_payload", send_payload) + monkeypatch.setattr(channel, "broadcast_payload", broadcast) async def main(): - pending = asyncio.create_task( - namespace.on_sub("sid-gone", {"fig": state_token, "mid": "m1"}) - ) + session = await open_fake_session(channel, "sid-gone") + pending = asyncio.create_task(channel.on_sub("sid-gone", {"fig": state_token, "mid": "m1"})) await started.wait() - connected["sid-gone"] = False - await namespace.on_disconnect("sid-gone") + session.open = False + await channel.on_close(session) # type: ignore[arg-type] resume.set() await pending + assert session.rooms == set(), "a disconnected SID must not re-enter a room" assert sent_payloads == [] assert registry._rebuildable_subscribers == {} assert registry._rebuildable_tokens_by_sid == {} entry = registry.get(state_token) assert registry.sweep(now=entry.last_access + 1_000_000.0) == [state_token] assert registry._evicted_versions == {} - assert namespace._subscription_locks == {} - assert namespace._subscription_lock_users == {} + assert channel._subscription_locks == {} + assert channel._subscription_lock_users == {} run(main()) @@ -1408,15 +1432,15 @@ def test_sub_after_ttl_rebuild_fans_existing_room_before_px_reply(_fresh_registr joining mount receives exactly one addressed payload built for its px. """ - from reflex_xy import namespace as namespace_module + from reflex_xy import data_plane as data_plane_module - build_wire_payload = namespace_module._build_wire_payload + build_wire_payload = data_plane_module._build_wire_payload def tagged_build_wire_payload(figure, px=None): spec, buffers = build_wire_payload(figure, px) return {**spec, "_test_px": px}, buffers - monkeypatch.setattr(namespace_module, "_build_wire_payload", tagged_build_wire_payload) + monkeypatch.setattr(data_plane_module, "_build_wire_payload", tagged_build_wire_payload) rebuilt = [] async def rebuild(token_str): @@ -1427,29 +1451,21 @@ async def main(): state_token = build_state_token(CLIENT_TOKEN, "root.some_state", "chart") registry.publish(state_token, make_figure(16), broadcast=False) async with data_plane_server(rebuild=rebuild) as (url, _): - existing = await connect_client(url) - existing_collector = Collector(existing) - await existing.emit( - "sub", - {"fig": state_token, "px": 640, "mid": "existing"}, - namespace="/_xy", - ) - first = await existing_collector.next(existing_collector.payloads) + # Two mounts of one figure on one page: they share the tab's + # websocket, so `mid` is the only thing separating their traffic. + client = await connect_client(url) + collector = Collector(client) + await client.emit("sub", {"fig": state_token, "px": 640, "mid": "existing"}) + first, _ = await collector.next(collector.payloads) assert first["mid"] == "existing" assert first["spec"]["_test_px"] == 640 evicted = registry.get(state_token) assert registry.sweep(now=evicted.last_access + 1_000_000.0) == [state_token] - joining = await connect_client(url) - joining_collector = Collector(joining) - await joining.emit( - "sub", - {"fig": state_token, "px": 123, "mid": "joining"}, - namespace="/_xy", - ) - room_payload = await existing_collector.next(existing_collector.payloads) - direct_payload = await joining_collector.next(joining_collector.payloads) + await client.emit("sub", {"fig": state_token, "px": 123, "mid": "joining"}) + room_payload, _ = await collector.next(collector.payloads) + direct_payload, _ = await collector.next(collector.payloads) assert room_payload["version"] == 2 assert "mid" not in room_payload @@ -1458,16 +1474,47 @@ async def main(): assert direct_payload["mid"] == "joining" assert direct_payload["spec"]["_test_px"] == 123 with pytest.raises(asyncio.TimeoutError): - await Collector.next(joining_collector.payloads, timeout=0.15) + await Collector.next(collector.payloads, timeout=0.15) - await existing.disconnect() - await joining.disconnect() + await client.disconnect() assert rebuilt == [state_token] run(main()) +def test_a_second_tab_gets_its_own_client_token_and_no_affinity(_fresh_registry): + """Affinity is per tab, resolved by Reflex — not by the query string. + + Reflex mints a fresh client token when a connection presents one that is + already live, so a figure minted for the first tab is another session's as + far as the second tab is concerned. The data plane reads the resolved + token off the channel session and inherits that for free. + """ + + async def main(): + state_token = build_state_token(CLIENT_TOKEN, "root.some_state", "chart") + registry.publish(state_token, make_figure(8), broadcast=False) + async with data_plane_server() as (url, _): + first = await connect_client(url) + second = await connect_client(url) # same ?token=, new tab + first_collector = Collector(first) + second_collector = Collector(second) + + await first.emit("sub", {"fig": state_token, "mid": "m1"}) + payload, _ = await first_collector.next(first_collector.payloads) + assert payload["fig"] == state_token + + await second.emit("sub", {"fig": state_token, "mid": "m1"}) + error, _ = await second_collector.next(second_collector.errors) + assert "another session" in error["error"] + + await first.disconnect() + await second.disconnect() + + run(main()) + + def test_interaction_after_ttl_rebuild_receives_new_payload(_fresh_registry): rebuilt = [] @@ -1479,45 +1526,37 @@ async def main(): state_token = build_state_token(CLIENT_TOKEN, "root.some_state", "chart") registry.publish(state_token, make_figure(16), broadcast=False) async with data_plane_server(rebuild=rebuild) as (url, _): - first_client = await connect_client(url) - second_client = await connect_client(url) - first_collector = Collector(first_client) - second_collector = Collector(second_client) - await first_client.emit("sub", {"fig": state_token, "mid": "m1"}, namespace="/_xy") - await second_client.emit("sub", {"fig": state_token, "mid": "m2"}, namespace="/_xy") - assert (await first_collector.next(first_collector.payloads))["version"] == 1 - assert (await second_collector.next(second_collector.payloads))["version"] == 1 + # Two mounts on the page's one websocket, distinguished by `mid`. + client = await connect_client(url) + collector = Collector(client) + await client.emit("sub", {"fig": state_token, "mid": "m1"}) + await client.emit("sub", {"fig": state_token, "mid": "m2"}) + first, _ = await collector.next(collector.payloads) + second, _ = await collector.next(collector.payloads) + assert (first["version"], second["version"]) == (1, 1) + assert {first["mid"], second["mid"]} == {"m1", "m2"} registry.append(state_token, x=[2.0], y=[6.0]) - assert (await first_collector.next(first_collector.messages))["version"] == 2 - assert (await second_collector.next(second_collector.messages))["version"] == 2 + push, _ = await collector.next(collector.messages) + assert push["version"] == 2 evicted = registry.get(state_token) assert registry.sweep(now=evicted.last_access + 1_000_000.0) == [state_token] message = {"type": "pick", "trace": 0, "index": 2, "seq": 31} - await first_client.emit( - "msg", - {"fig": state_token, "mid": "m1", "v": 2, "m": message}, - namespace="/_xy", - ) - first_replacement = await first_collector.next(first_collector.payloads) - second_replacement = await second_collector.next(second_collector.payloads) - assert first_replacement["version"] == 3 - assert second_replacement["version"] == 3 + await client.emit("msg", {"fig": state_token, "mid": "m1", "v": 2, "m": message}) + replacement, _ = await collector.next(collector.payloads) + assert replacement["version"] == 3 + # The request predates this worker's authoritative payload, so the + # rebuild re-primes the room instead of answering old coordinates. with pytest.raises(asyncio.TimeoutError): - await Collector.next(first_collector.messages, timeout=0.15) + await Collector.next(collector.messages, timeout=0.15) message["seq"] = 32 - await second_client.emit( - "msg", - {"fig": state_token, "mid": "m2", "v": 3, "m": message}, - namespace="/_xy", - ) - reply = await second_collector.next(second_collector.messages) + await client.emit("msg", {"fig": state_token, "mid": "m2", "v": 3, "m": message}) + reply, _ = await collector.next(collector.messages) assert reply["version"] == 3 assert reply["message"]["seq"] == 32 - await first_client.disconnect() - await second_client.disconnect() + await client.disconnect() assert rebuilt == [state_token] @@ -1529,8 +1568,8 @@ async def main(): async with data_plane_server() as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": "xyfig-doesnotexist", "mid": "m1"}, namespace="/_xy") - err = await collector.next(collector.errors) + await client.emit("sub", {"fig": "xyfig-doesnotexist", "mid": "m1"}) + err, _ = await collector.next(collector.errors) assert err["error"] == "unknown figure token" await client.disconnect() @@ -1545,15 +1584,15 @@ async def main(): async with data_plane_server() as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": token, "mid": "m1"}, namespace="/_xy") - first = await collector.next(collector.payloads) + await client.emit("sub", {"fig": token, "mid": "m1"}) + first, _ = await collector.next(collector.payloads) assert first["version"] == 1 registry.publish(token, make_figure(48)) # e.g. a dep-driven recompute - second = await collector.next(collector.payloads) + second, buffers = await collector.next(collector.payloads) assert second["version"] == 2 assert "mid" not in second - xcol = np.frombuffer(second["buffers"][0], dtype=np.float32) + xcol = np.frombuffer(buffers[0], dtype=np.float32) assert len(xcol) == 48 await client.disconnect() @@ -1561,18 +1600,16 @@ async def main(): def test_append_streams_to_subscribers(_fresh_registry): - import reflex_xy - async def main(): token = registry.register(make_figure(4)) async with data_plane_server() as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": token, "mid": "m1"}, namespace="/_xy") + await client.emit("sub", {"fig": token, "mid": "m1"}) await collector.next(collector.payloads) reflex_xy.append(token, x=[2.0, 3.0], y=[6.0, 9.0]) - push = await collector.next(collector.messages) + push, _ = await collector.next(collector.messages) assert push["message"]["type"] == "append" assert push["version"] == 2 assert push.get("mid") is None # pushes are room-wide, not mount-addressed @@ -1584,22 +1621,20 @@ async def main(): def test_rows_selection_push_carries_its_figure_generation(_fresh_registry): - import reflex_xy - async def main(): token = registry.register(make_figure(8)) async with data_plane_server() as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": token, "mid": "m1"}, namespace="/_xy") - payload = await collector.next(collector.payloads) + await client.emit("sub", {"fig": token, "mid": "m1"}) + payload, _ = await collector.next(collector.payloads) reflex_xy.select(token, rows={0: [1, 3, 5]}) - push = await collector.next(collector.messages) + push, buffers = await collector.next(collector.messages) assert push["message"]["type"] == "selection_rows" assert push["version"] == payload["version"] == 1 assert push.get("mid") is None - assert push["buffers"] + assert buffers assert registry.get(token).version == 1 await client.disconnect() @@ -1612,9 +1647,9 @@ async def main(): async with data_plane_server() as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": token, "mid": "m1"}, namespace="/_xy") + await client.emit("sub", {"fig": token, "mid": "m1"}) await collector.next(collector.payloads) - await client.emit("unsub", {"fig": token, "mid": "m1"}, namespace="/_xy") + await client.emit("unsub", {"fig": token, "mid": "m1"}) await asyncio.sleep(0.05) registry.publish(token, make_figure(12)) await asyncio.sleep(0.2) @@ -1626,7 +1661,7 @@ async def main(): @pytest.mark.parametrize("departure", ["unsub", "disconnect"]) def test_subscription_departure_releases_evicted_version(_fresh_registry, departure): - """Namespace lifecycle handlers release rebuild-version tombstones once + """Data plane lifecycle handlers release rebuild-version tombstones once the last live subscriber leaves, bounding scalar metadata after eviction. """ @@ -1636,21 +1671,20 @@ async def main(): async with data_plane_server() as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": state_token, "mid": "m1"}, namespace="/_xy") + await client.emit("sub", {"fig": state_token, "mid": "m1"}) await collector.next(collector.payloads) evicted = registry.get(state_token) assert registry.sweep(now=evicted.last_access + 1_000_000.0) == [state_token] if departure == "unsub": - await client.emit("unsub", {"fig": state_token, "mid": "m1"}, namespace="/_xy") + await client.emit("unsub", {"fig": state_token, "mid": "m1"}) await asyncio.sleep(0.05) else: await client.disconnect() await asyncio.sleep(0.05) replacement = registry.publish(state_token, make_figure(8), broadcast=False) - if client.connected: - await client.disconnect() + await client.disconnect() assert replacement.version == 1 run(main()) @@ -1702,8 +1736,8 @@ async def main(): async with data_plane_server(rebuild=make_rebuild_hook(app)) as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": composite, "mid": "m1"}, namespace="/_xy") - payload = await collector.next(collector.payloads) + await client.emit("sub", {"fig": composite, "mid": "m1"}) + payload, _ = await collector.next(collector.payloads) assert payload["fig"] == composite assert payload["spec"]["traces"][0]["n_points"] == 24 # interactions round-trip against the bound figure @@ -1715,9 +1749,8 @@ async def main(): "mid": "m1", "m": {"type": "pick", "trace": 0, "index": 3, "seq": "pick:1"}, }, - namespace="/_xy", ) - reply = await collector.next(collector.messages) + reply, _ = await collector.next(collector.messages) assert reply["message"]["type"] == "pick_result" await client.disconnect() # both halves are cached now: columns and the bound figure @@ -1742,8 +1775,8 @@ async def main(): async with data_plane_server(rebuild=make_rebuild_hook(app)) as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": composite, "mid": "m1"}, namespace="/_xy") - payload = await collector.next(collector.payloads) + await client.emit("sub", {"fig": composite, "mid": "m1"}) + payload, _ = await collector.next(collector.payloads) assert payload["spec"]["traces"][0]["n_points"] == 7 await client.disconnect() @@ -1764,10 +1797,10 @@ async def main(): async with data_plane_server(rebuild=make_rebuild_hook(app)) as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": scatter_fig, "mid": "m1"}, namespace="/_xy") - first = await collector.next(collector.payloads) - await client.emit("sub", {"fig": line_fig, "mid": "m2"}, namespace="/_xy") - second = await collector.next(collector.payloads) + await client.emit("sub", {"fig": scatter_fig, "mid": "m1"}) + first, _ = await collector.next(collector.payloads) + await client.emit("sub", {"fig": line_fig, "mid": "m2"}) + second, _ = await collector.next(collector.payloads) assert {first["fig"], second["fig"]} == {scatter_fig, line_fig} # the data var recomputes (as a state delta evaluation would) @@ -1777,7 +1810,7 @@ async def main(): ) refreshed = {} for _ in range(2): - payload = await collector.next(collector.payloads) + payload, _ = await collector.next(collector.payloads) refreshed[payload["fig"]] = payload["spec"]["traces"][0]["n_points"] assert refreshed == {scatter_fig: 5, line_fig: 5} await client.disconnect() @@ -1794,8 +1827,8 @@ async def main(): async with data_plane_server(rebuild=make_rebuild_hook(app)) as (url, _): thief = await connect_client(url, client_token=OTHER_TOKEN) thief_collector = Collector(thief) - await thief.emit("sub", {"fig": composite, "mid": "m1"}, namespace="/_xy") - error = await thief_collector.next(thief_collector.errors) + await thief.emit("sub", {"fig": composite, "mid": "m1"}) + error, _ = await thief_collector.next(thief_collector.errors) assert "another session" in error["error"] await thief.disconnect() @@ -1812,8 +1845,8 @@ async def main(): async with data_plane_server(rebuild=make_rebuild_hook(app)) as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": composite, "mid": "m1"}, namespace="/_xy") - error = await collector.next(collector.errors) + await client.emit("sub", {"fig": composite, "mid": "m1"}) + error, _ = await collector.next(collector.errors) assert "feedfacefeedfacefeed" in error["error"] assert error["resync"] is True await client.disconnect() @@ -1839,8 +1872,8 @@ async def main(): async with data_plane_server(rebuild=make_rebuild_hook(app)) as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": composite, "mid": "m1"}, namespace="/_xy") - error = await collector.next(collector.errors) + await client.emit("sub", {"fig": composite, "mid": "m1"}) + error, _ = await collector.next(collector.errors) assert "plan binds" in error["error"] assert "'x'" in error["error"] assert error.get("resync") is None @@ -1862,11 +1895,11 @@ async def main(): async with data_plane_server(rebuild=make_rebuild_hook(app)) as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": composite, "mid": "m1"}, namespace="/_xy") + await client.emit("sub", {"fig": composite, "mid": "m1"}) await collector.next(collector.payloads) registry.publish_columns(data_token, {"wrong": [1.0]}) - error = await collector.next(collector.errors) + error, _ = await collector.next(collector.errors) assert "plan binds" in error["error"] assert error["resync"] is True assert registry.get(composite) is None @@ -1883,8 +1916,8 @@ async def main(): async with data_plane_server(rebuild=make_rebuild_hook(app)) as (url, _): client = await connect_client(url) collector = Collector(client) - await client.emit("sub", {"fig": plane_data_token(), "mid": "m1"}, namespace="/_xy") - error = await collector.next(collector.errors) + await client.emit("sub", {"fig": plane_data_token(), "mid": "m1"}) + error, _ = await collector.next(collector.errors) assert error["error"] == "unknown figure token" await client.disconnect() diff --git a/tests/reflex_adapter/test_page_plan_registration.py b/tests/reflex_adapter/test_page_plan_registration.py index 3ab3fd078..6733eaa2b 100644 --- a/tests/reflex_adapter/test_page_plan_registration.py +++ b/tests/reflex_adapter/test_page_plan_registration.py @@ -78,14 +78,9 @@ def test_apps_without_unevaluated_pages_are_a_noop(): def _fake_app(pages: dict, tasks: list) -> SimpleNamespace: - """Enough of an `rx.App` for `setup()`: a socket server and task sink.""" - - class _Sio: - def register_namespace(self, namespace) -> None: - pass - + """Enough of an `rx.App` for `setup()`: a channel sink and a task sink.""" return SimpleNamespace( - sio=_Sio(), + register_channel=lambda channel: None, _unevaluated_pages=pages, register_lifespan_task=tasks.append, _state=None, diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py index 47a2b378b..d7e192975 100644 --- a/tests/test_dependencies.py +++ b/tests/test_dependencies.py @@ -35,12 +35,19 @@ def test_core_publishes_only_the_reflex_optional_dependency() -> None: extras = project.get("optional-dependencies") or {} assert set(extras) == {"reflex"} + # TEMPORARY, tracked in spec/design/reflex-integration.md § Status: the + # data plane needs Reflex's channel transport, which is unreleased, so the + # extra names the branch instead of a floor. A direct reference is + # unpublishable by design — the release fails at upload rather than + # shipping a floor that installs a Reflex where every chart stays blank. + # Restore the `>=` assertion when channels are released. assert any( - _dependency_name(requirement) == "reflex" and ">=0.9.6" in requirement + _dependency_name(requirement) == "reflex" + and "git+https://github.com/benedikt-bartscher/reflex.git@make-sio-optional" in requirement for requirement in extras["reflex"] ), ( - "xy[reflex] must select the supported Reflex floor while the adapter " - "source remains bundled in the xy distribution" + "xy[reflex] must select the Reflex the adapter actually needs while the " + "adapter source remains bundled in the xy distribution" ) assert {"dev", "codspeed"} <= groups.keys() group_names = { diff --git a/tests/test_tailwind_root_customization.py b/tests/test_tailwind_root_customization.py index 986e77757..5fcaa0621 100644 --- a/tests/test_tailwind_root_customization.py +++ b/tests/test_tailwind_root_customization.py @@ -431,7 +431,7 @@ def test_live_wrapper_silently_hydrates_durable_selection_and_all_axis_ranges() assert jsx.count("hydrateSelectionForRepublish(selectionToRestore);") == 2 assert jsx.count("restoreSelectionMask(selectionMaskRequest);") == 2 assert "if (isRestore) clientMessage = { ...message, suppress_event: true };" in jsx - assert "dispatchToView(clientMessage, data.buffers || [])" in jsx + assert "dispatchToView(clientMessage, buffers)" in jsx # Geometry is hydrated before the one direct mask re-request. assert jsx.index("view._applyStatePatch?.(") < jsx.rindex( "restoreSelectionMask(selectionMaskRequest);" diff --git a/uv.lock b/uv.lock index c9d6f19b5..2e1a8f416 100644 --- a/uv.lock +++ b/uv.lock @@ -146,15 +146,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] -[[package]] -name = "annotated-types" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, -] - [[package]] name = "anyio" version = "4.14.2" @@ -209,15 +200,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] -[[package]] -name = "bidict" -version = "0.23.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, -] - [[package]] name = "certifi" version = "2026.7.22" @@ -373,91 +355,91 @@ wheels = [ [[package]] name = "granian" -version = "2.8.0" +version = "2.8.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/22/f289f681277c8a1cd2c29177fc21bad080b270d6b60ac6e91a19cafe795a/granian-2.8.0.tar.gz", hash = "sha256:be842c6737b41beb6fe6521f9c46695a1843ec9e086a18bbbc945ed170ff28e2", size = 129256, upload-time = "2026-08-03T18:58:55.371Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/54/a18bbca2647d2faa720da32cd7e339356ab0a2dc4ec7243d015ee6a97b50/granian-2.8.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab31b4205991954dbc6ccd8ab243f4b141a50da695bf2d9de46e56bec99bc0cf", size = 4526820, upload-time = "2026-08-03T18:56:25.927Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2e/afa50e6bf9dace1dbc8ec814e2a251fc0fc09326590b90887ee9e3d1a017/granian-2.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7990e16ada156dc8dd66c8bea4688cbe14fc013e10ab8f2d554936a1fe9ee14a", size = 4329002, upload-time = "2026-08-03T18:56:27.541Z" }, - { url = "https://files.pythonhosted.org/packages/a6/af/a74dc0a2699b07ece151fd6d9a5ff151eff70254b7cde49f8039034a3e00/granian-2.8.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10889fbe3a46f65184dd877450ba07fb653def537246bdc5c135c5eb17619388", size = 4769750, upload-time = "2026-08-03T18:56:29.171Z" }, - { url = "https://files.pythonhosted.org/packages/58/23/0b1e5beedcdefca60314d869f7e8eb25da5e741328ef31a58f1b5467fdba/granian-2.8.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87dbd544e41074a5b2b4d6ba82b1c8172c164bfc4543819f41161ad1222c4799", size = 4449131, upload-time = "2026-08-03T18:56:31.052Z" }, - { url = "https://files.pythonhosted.org/packages/f8/7a/41772d8980a33383f9c47b6cc5a9e2c2266928abe82734e83d72960f6830/granian-2.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42ffef3148f6d7387182fa088834a4ffdf48d1de42c2e61fb961a8552eadb667", size = 4986278, upload-time = "2026-08-03T18:56:32.592Z" }, - { url = "https://files.pythonhosted.org/packages/27/3a/a65c17a8caec6ad567c4ede495ab744ed6849a3a079657edc919dfb2cf1b/granian-2.8.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df76b03542b0f9ad67c0ac6f927faa1c4a029ea001be97fdcc5d03bbea617975", size = 5087401, upload-time = "2026-08-03T18:56:35.164Z" }, - { url = "https://files.pythonhosted.org/packages/92/fb/4d805d1847d737c5f70c8ee916e439da6b801356371876dcb0a4eeab4231/granian-2.8.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2c4c3982cdc2e07db7b85bc421632b4cbf9a3855dc02eeb1300c6488126b9cf3", size = 5053305, upload-time = "2026-08-03T18:56:36.952Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/eaa2ba68ada39123b9c083247536a006ccc3684c479644701b48e295d43e/granian-2.8.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:58f97da58709a017c10564a5472854696a27278ae92083f65937213609b7e667", size = 4901784, upload-time = "2026-08-03T18:56:38.603Z" }, - { url = "https://files.pythonhosted.org/packages/27/dc/1b865720bce1fd33283e478d3151aeb01e74babcb02b3fcaa4bc0da069ba/granian-2.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:31d424ca1fd98381069e5bf684bcb78d0dd906ee20383e48a46fcb75c6bc4dc5", size = 5045412, upload-time = "2026-08-03T18:56:40.189Z" }, - { url = "https://files.pythonhosted.org/packages/2a/1c/cec72287bb2d313e74cd8a99140689151bd8eafe628276ad9035433da5ac/granian-2.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:1fb070bf50122353e316692927dc77e2fd84fde5bd17e89d83d66c3ad4b39809", size = 3126580, upload-time = "2026-08-03T18:56:41.72Z" }, - { url = "https://files.pythonhosted.org/packages/45/ab/432706e82c0bb95ecaf9aeee26d36c13b2917a358377ecf07675b363d61a/granian-2.8.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:de8fc64f423f000a0097886e0c4e8ae1bfb4b4d0699edc6244da768c05c0b40b", size = 4442542, upload-time = "2026-08-03T18:56:43.266Z" }, - { url = "https://files.pythonhosted.org/packages/2d/a5/b7835933f999a73d8fd5b15f6734f6abb40edac4d759b2490a069762c1ad/granian-2.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76fdd00bb5842a7993ce57e7d254e75d5e9d7425b49d02a341b93d618047f1fa", size = 4264376, upload-time = "2026-08-03T18:56:45.041Z" }, - { url = "https://files.pythonhosted.org/packages/18/63/5245e1d11a3920b1d7705d6748e736b07a48f1415817a2b7b0eab921b26c/granian-2.8.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:edca648754927c913dfcf52068b96418b66810017fd628d19cb1b65f78dfd9a6", size = 4812379, upload-time = "2026-08-03T18:56:46.69Z" }, - { url = "https://files.pythonhosted.org/packages/93/00/a2dd16309d39be4888fe90da677692f0148feff589438b536425ffd3d3de/granian-2.8.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:899b1af43754ee8ef6beaece26947cee3fa03487f0193d5fc17fb55435903a68", size = 4446390, upload-time = "2026-08-03T18:56:48.573Z" }, - { url = "https://files.pythonhosted.org/packages/95/87/8f9e929ac017233ce14123601765d095305b407df0a512ae8c14ea741380/granian-2.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278db6788bacb37e9e57abb429229f74c5b5f6f5dca06f66fad027a4ff6dc51c", size = 4860867, upload-time = "2026-08-03T18:56:50.346Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2e/5124c9c4d66d3e8405f3cc5fa4074d06a703546456c002587af9d3c2822a/granian-2.8.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5827ee36b7e5af4a104430d802cc0454913dd2f984b0b594c42ba049a1deeb61", size = 5023347, upload-time = "2026-08-03T18:56:52.17Z" }, - { url = "https://files.pythonhosted.org/packages/5a/14/d4624e7c22c7b26d1ba0e296a6c25621a6daeedd09a1399fdf9c34e2e8ec/granian-2.8.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:67e1f73705aac805ec1e7e10c38946a4dd49f26e018d250014d1fd9a924dca64", size = 4993318, upload-time = "2026-08-03T18:56:53.693Z" }, - { url = "https://files.pythonhosted.org/packages/bc/7b/55fd1dca67c273460fe13f622cfbcf17812b27077656bb9c7982b88e9d31/granian-2.8.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:89d4904bb657503ab41b304340c42371c9f9b01f83b6c7c136f5a0d8bff50efd", size = 4923508, upload-time = "2026-08-03T18:56:55.918Z" }, - { url = "https://files.pythonhosted.org/packages/18/b7/1ee1f572b86c5681387ca10e10750c3d35583db43aa85213bbc33f2cf6f3/granian-2.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49bbcf8c20370cba6d0d5ba1aac95f5e9d22a3e95b10b420d49337629d2f4fcc", size = 4924703, upload-time = "2026-08-03T18:56:57.783Z" }, - { url = "https://files.pythonhosted.org/packages/bc/2f/0b4e8a671e52b868140a80b67ddf665aa85622114a2dc28dc550119cbf49/granian-2.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:471e4d122518ca23776677bcd104c37bdf31377a4a8b8c668e3d6551e90213bf", size = 3112321, upload-time = "2026-08-03T18:56:59.879Z" }, - { url = "https://files.pythonhosted.org/packages/95/45/1c1f3b80b19813a8c3f025397aea4b56b62d2d422100b10498a7f13bcf35/granian-2.8.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9e28aa83940629169986615ecbcc39c026e193df41ba8264202fefba5edaa8b6", size = 4442295, upload-time = "2026-08-03T18:57:01.543Z" }, - { url = "https://files.pythonhosted.org/packages/28/1b/e686b7504e125a2dd5757f2b668c0ef4707467d04add2e42cfa9b19f9139/granian-2.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf3ed0a22327aeb1b15e36c3d56c2174a68a0dd6a57554ac5b7456fd231e258a", size = 4263150, upload-time = "2026-08-03T18:57:03.18Z" }, - { url = "https://files.pythonhosted.org/packages/bb/03/740fa22bdbeec4fa55cf017956955532ba760584a9479690442e1c831179/granian-2.8.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6848acafe50547b2f6c3f5d92b5a3d873dff97267287195a45f18a9270ca2952", size = 4812578, upload-time = "2026-08-03T18:57:04.911Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/4971ab3775153452940c1f38dff2a141168dc2bcffb4348382ed47fe828d/granian-2.8.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f03da08e21e6d0a6b0ac6be3dca4485e2ec1a609e9349e07c390c745f0c8b29c", size = 4446673, upload-time = "2026-08-03T18:57:06.839Z" }, - { url = "https://files.pythonhosted.org/packages/82/f3/bd62b2c9830fa3bfbfbac7e07ab086a0f0b3e5ab6613fcf017a389f975ef/granian-2.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:682a63cb450f71760a56c80794c0c49bc638d58f8f4e9f91202a21c08014994c", size = 4859343, upload-time = "2026-08-03T18:57:08.593Z" }, - { url = "https://files.pythonhosted.org/packages/23/fd/10345aabf40982f9483671c93be9a1892ece07ca3bfe68babc4171caa946/granian-2.8.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:6ebfd8af38136054e6ea2e86b18649c5676d924fc3a40383ebbb3af6185e929e", size = 5023131, upload-time = "2026-08-03T18:57:10.981Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/42d540dae26873938dbc19cf0d692b9644c8d8a6aee668fa78c79f751897/granian-2.8.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:39b620297be07d71ff92aaf2c52f5a376a7888d251a3e953cad81620751ee0a6", size = 4993372, upload-time = "2026-08-03T18:57:13.361Z" }, - { url = "https://files.pythonhosted.org/packages/e3/09/025227fb79b3b7bcfc810922863fdbc05bf3932030d207773bf2bba5bf60/granian-2.8.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:67c1db11193797fe1de61e74ac858fbac4e4d27f1b8c4b437be40b15700fccf8", size = 4923411, upload-time = "2026-08-03T18:57:15.079Z" }, - { url = "https://files.pythonhosted.org/packages/11/a3/aae60898ffb725ebc7f2caa5055d1aa4c6aa49c8ba29dea9f559e2eeea3a/granian-2.8.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:4f4fa63ed4fe67ec12f9a26d29779f52c35cbc8c57fe337f7bea25f8c47981f6", size = 4924152, upload-time = "2026-08-03T18:57:16.83Z" }, - { url = "https://files.pythonhosted.org/packages/68/7b/5d5b9b3b02f0abe30496ad3cd7123221666fd54b0a3a3d9fa3a18c66de51/granian-2.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a1d8dce913504188d4fddb12e33515ee5bef6c4658e9eabb0bd5dd43d9c1e4de", size = 3112465, upload-time = "2026-08-03T18:57:18.622Z" }, - { url = "https://files.pythonhosted.org/packages/dc/10/1c8231d6592bce2855d002c0b82939c62bca317c82cf37ac98ed219b94d7/granian-2.8.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:55aad6e52f7b5d22c468304f5862b154e08ff231c622e956d63f4b5a7f308e36", size = 4423359, upload-time = "2026-08-03T18:57:20.484Z" }, - { url = "https://files.pythonhosted.org/packages/69/37/39b99ffff95b9cce64c9964ff21f15e75b3b5c96f0529b56ecd87a8df037/granian-2.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:95517d0f5c38cbed7b94a6d28dfb6710330b879024a9ee73dacf6b4ade638925", size = 4259820, upload-time = "2026-08-03T18:57:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/e1/32/846784ed1e3b7aa84e473268a9c43cfd2c5b97cb9dd0161e4719e04ecf53/granian-2.8.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:36a53d61e6d5f04af257700d2b83700d64be5f392d0d07a2cb568daa2f778c8e", size = 4805453, upload-time = "2026-08-03T18:57:24.103Z" }, - { url = "https://files.pythonhosted.org/packages/89/c6/7e24caa2211fc669076aa223867e2bdb3f0a5bb9d74bbdbe9cfb4c8b5d9a/granian-2.8.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc2f421b899d3a8615ae705016f41a6e0a4940c7005dbf58fc8962a412bd760", size = 4444078, upload-time = "2026-08-03T18:57:26.367Z" }, - { url = "https://files.pythonhosted.org/packages/ea/be/9eedfbc98b0fb6f0213adc0338d530467cd1acaa98aa026fab881c672a8f/granian-2.8.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036eac55cb9d67457b50b86a1c364d39c3f0ce4e06d86a87d3af8de7d173e721", size = 4870806, upload-time = "2026-08-03T18:57:28.289Z" }, - { url = "https://files.pythonhosted.org/packages/d4/eb/3b140540414e540a2708a4008eabfe3958c2b010ca998738fe6173300f9a/granian-2.8.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:35624cf9fe8bbb416890871e63de7f3460555ed9bf0ac69b533f796a4382a746", size = 5026508, upload-time = "2026-08-03T18:57:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/8e/d4/adcec457beec4f2775ee3cc5d4ac43c795edb0adf104cb48d9d1181b0924/granian-2.8.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:09c73e7835a7bba35f14320b8e57dac1da4981c8c6d3a08cd6e9c557860bbb1b", size = 5043848, upload-time = "2026-08-03T18:57:31.992Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7b/0ef38e58546d757c12d751bf8bc9ab3b16287d6cf09656ec6d53874b0ed0/granian-2.8.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:1bb0c4c2ac258f5e96cb5ea1d1c674c3d1b7c7bdf110ae97bae173bb003405e4", size = 4918203, upload-time = "2026-08-03T18:57:34.282Z" }, - { url = "https://files.pythonhosted.org/packages/a6/df/e832f520be4c3439148ea13b8c5b635297138bc572115284b55c4425343f/granian-2.8.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:de30f61a321571cfaa7ab1650f324b26959fce6826d341bae520cf2786368a09", size = 4933696, upload-time = "2026-08-03T18:57:36.117Z" }, - { url = "https://files.pythonhosted.org/packages/eb/40/965f20eb6cedc318d08c361fcc8b3c85efac3aadf5adf298b32f6075ebbc/granian-2.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:dca589ddf61239e08a52ec6fa7f62290cd9ab707588dc928d9b65df971ef202c", size = 3096154, upload-time = "2026-08-03T18:57:37.979Z" }, - { url = "https://files.pythonhosted.org/packages/b6/93/64cf513a32fa2f64a421b58b0b00486d03e48cdd69c367ce185140e00f62/granian-2.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a3e0905fa79ab2b5752e9222fb87003b1e69f7162eebe6eeec0ab924e5e67312", size = 4320699, upload-time = "2026-08-03T18:57:39.887Z" }, - { url = "https://files.pythonhosted.org/packages/4d/d4/4bbb6b1d9c56c9cb5ce636fc3c2d31ec0baacf6975d379b5ae4ab2c3627c/granian-2.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4c228897c11931c623ee27e07ba512a86667d7c7af0172c4334ae8a0dc66c6f6", size = 4144380, upload-time = "2026-08-03T18:57:41.668Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1c/da817b9cf934fb041ae423744a1c89823af3299f91c2569fa04b4cd0f3b7/granian-2.8.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:640003ab77b6dc29b0c247cdb8d970cb2f4986de2e7c72849e9f5fe565eca18e", size = 4282845, upload-time = "2026-08-03T18:57:43.494Z" }, - { url = "https://files.pythonhosted.org/packages/ba/09/35c8b5d5001ec3024a1bef4c6305582cb5e8f8a2f2e85f90eead59910593/granian-2.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81e12dcd0b504c8704b54c6a88733a57de21726ee9567a018f8cf7972e190753", size = 4812269, upload-time = "2026-08-03T18:57:45.884Z" }, - { url = "https://files.pythonhosted.org/packages/36/6e/99b59975f249faaa066320c4f2927102c6740de87be8421cae99c47688d1/granian-2.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bfd4726c4c61f39dbdb4e4e2bfe8fd0ace2cea58eb34ce720036e8e09b1c27a", size = 4748058, upload-time = "2026-08-03T18:57:47.899Z" }, - { url = "https://files.pythonhosted.org/packages/c0/83/42aeb107969349a07b7385dda0222fad830d318f1d5bdca084823650f859/granian-2.8.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6c1aeca05c84434ecafb2297aabaaf1e39a267e66eee4420b7669cf0c47df369", size = 4842775, upload-time = "2026-08-03T18:57:50.651Z" }, - { url = "https://files.pythonhosted.org/packages/17/35/d5f6e690eecdf4a63460d65f363527fe3db69477bfd0df631f395c554a4f/granian-2.8.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:ec5281054da6e342e7be3d3d5bf764288faa2e6407d26ef5b9a909fa5b5ca44d", size = 5038742, upload-time = "2026-08-03T18:57:52.407Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e1/98b97433013a1219ed1c1238befa7aa0eb83d7bbc872e63193e847e152fe/granian-2.8.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:7b76586d95b798453ee89304a1bb25b1ea9301f564cd3a6a20268b8139a9cad5", size = 4944066, upload-time = "2026-08-03T18:57:54.488Z" }, - { url = "https://files.pythonhosted.org/packages/41/70/47897e417dd22c83e731d85e8f388ed29a1b88a97404063c05a2510fc11e/granian-2.8.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:7d516e7ae705e0d5f8ece085ac18b8817dc4c8adfb7f0f68e9ee529e991664b2", size = 4987791, upload-time = "2026-08-03T18:57:56.46Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8e/76f58b9953a1aa1ba317ce0d96073a47dbb515d562fa524ea8a6b9564bc7/granian-2.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9383c97c12829b0b0accb3aad5011ad765e74a90d54d8381cf722ca4f7bdfc0a", size = 3082991, upload-time = "2026-08-03T18:57:58.204Z" }, - { url = "https://files.pythonhosted.org/packages/47/8e/264f9215206a177d6a0958ec6a496923c6fa0fca785a350884e9d134fce8/granian-2.8.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:c13e89315cfd91821399c78f1dfe5c63b95cd9b44c9da1052eee66c00e60c719", size = 4424059, upload-time = "2026-08-03T18:57:59.857Z" }, - { url = "https://files.pythonhosted.org/packages/42/bd/6c62d461da7ccb76ecc57e2e4c29a63fcb55d21a4b04fa030e849e6d0225/granian-2.8.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f956f26d638268312b7cd583fc079cee1bc3474a29508dcb2bf7f797822912bb", size = 4259907, upload-time = "2026-08-03T18:58:01.491Z" }, - { url = "https://files.pythonhosted.org/packages/95/ed/cd6b65766c1f35718fe1d83de985ae79ebd2068ea27ab39457ab95439f54/granian-2.8.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fcd0888e6ab17a14dabd7b8b3a1cda12795a754660279d2b17b6abcbaa65644", size = 4807122, upload-time = "2026-08-03T18:58:03.404Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c7/36c80a53a43e20e8ecc0587c9b82e0c23ef82b26e258ff05750462fe689f/granian-2.8.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b771aec7246a9e220f841c24e522f493b67ad2df4a7474e4fa8f960844826c9", size = 4444740, upload-time = "2026-08-03T18:58:05.083Z" }, - { url = "https://files.pythonhosted.org/packages/0f/34/760cbeca09daae682e20a6f4a894248edae35dd5e40f27ef6fe8aab315db/granian-2.8.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2fdbbe31574800b2b14dcd2868be06f0e0b333817a37fc0fdae2024de2d4612", size = 4870669, upload-time = "2026-08-03T18:58:06.874Z" }, - { url = "https://files.pythonhosted.org/packages/e5/17/daad58c44a0f934ffa5991440054b738758c645ecd35a8d81266f6bdd2df/granian-2.8.0-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:826bb163325f1bff16f884c98b7f109998a3a9042406c31f7f407114e8196020", size = 5026641, upload-time = "2026-08-03T18:58:08.82Z" }, - { url = "https://files.pythonhosted.org/packages/2f/8e/2ed4b28cfb482ccb3e9d6b0812f64ef40598829c53dcca9260aa93994fc5/granian-2.8.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:fe48c902ace46e8dfaa24585c2941b51a22b5d77d3676a502ae48caf4ba624ca", size = 5043662, upload-time = "2026-08-03T18:58:10.706Z" }, - { url = "https://files.pythonhosted.org/packages/c8/df/9afd95c900d0c4fb5d57d1de1d9340cbcad9813b0374c4ac37b0a00c710d/granian-2.8.0-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:6f9876220f56a0ffc2bc41a977f6fdbe0458a8559c3f950a0497fe71e52a16e3", size = 4918816, upload-time = "2026-08-03T18:58:12.489Z" }, - { url = "https://files.pythonhosted.org/packages/16/ff/ec09ab42ba46676440715b4810aca60ef92d39dffa68669a4daa0bf01252/granian-2.8.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:0d797398949f8de4259926df69d0020b6897ca325672043529d9a6b702bddafe", size = 4934107, upload-time = "2026-08-03T18:58:14.417Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a0/1f2de683c90137ae5a66e89ca7aa72752edb2e0c55490442169007a9bbae/granian-2.8.0-cp315-cp315-win_amd64.whl", hash = "sha256:808986933f844f54dd9c6ea47a9e0e39cbbd12b91b315a8de1ea98abb9314e80", size = 3096056, upload-time = "2026-08-03T18:58:16.3Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d2/21d2fbf8517b2c0d82bda45dfa7ed26d933a5698ac7107c43da366635223/granian-2.8.0-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:8bf00fd66844846dac9278a15db864ec68c60878098ad638451fc8a3e61f845b", size = 4320343, upload-time = "2026-08-03T18:58:18.126Z" }, - { url = "https://files.pythonhosted.org/packages/94/1e/81284fa128f77dd6132ed62f29f0a099aec8622f2acd3c3fa7a733dc8375/granian-2.8.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ca1a764c00b89509eb7a45f1479118481628380e1bd1d94c2248e097abd63017", size = 4144285, upload-time = "2026-08-03T18:58:20.122Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8b/2f6a7012264011ebe6ea29afdeb333b6c50be3956b70cd59eecf248d599c/granian-2.8.0-cp315-cp315t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:042c177e75a5e67144a39476882ed735ac9558e48097a5658111adbf939c0d1e", size = 4281969, upload-time = "2026-08-03T18:58:22.433Z" }, - { url = "https://files.pythonhosted.org/packages/76/72/ff6d27931f004e231dcead6c9d5d0c252a73358462885f19e1223784c72a/granian-2.8.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:faa739c7674c53fb9e6b72500232e6360c13e531d8186cf5ef569ba8ccbd5f2c", size = 4813597, upload-time = "2026-08-03T18:58:24.563Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8d/7501ff01a8ca751608783252729ec067a7dc498c4323ae2e31ac17c1dc30/granian-2.8.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04c83c5d7a387635d8c7cde11a95f0babee1cabce3c469de4494a9f93a015394", size = 4746980, upload-time = "2026-08-03T18:58:26.392Z" }, - { url = "https://files.pythonhosted.org/packages/2f/14/3f635ba0065fc36f7820704059bbc46065dd9e4fae55b28de46a91dff9f9/granian-2.8.0-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:7cd88ca58924a63eed769ce64cea3c29c876791db14988f2d6f3f6fd5cb18061", size = 4842380, upload-time = "2026-08-03T18:58:28.178Z" }, - { url = "https://files.pythonhosted.org/packages/da/6e/008efb0f414ff7c8c8d0d9112abb5fa9a93574043cbe4e5f69f9ab80caf2/granian-2.8.0-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:2052d1cf74369430b960f7731db823a1f604e375179182bba3511e88d976eb43", size = 5038154, upload-time = "2026-08-03T18:58:30.131Z" }, - { url = "https://files.pythonhosted.org/packages/da/4b/01e0f0ad66c886a55c44912385ec72e7da68ece6becf3b67bb402022b605/granian-2.8.0-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:0e96f5bdbbac30e6abff4e7c2268582734ba8a908608c95fbc9a1db5b849bc3f", size = 4946307, upload-time = "2026-08-03T18:58:32.26Z" }, - { url = "https://files.pythonhosted.org/packages/5d/50/2a961f66228426d9c95b4d799f1fea5460f34ea01749108118d81ea33eb8/granian-2.8.0-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:0d84b516c3d720d22448d935e136d8ddaabb5a8715bf839d8f28de578d35443f", size = 4987962, upload-time = "2026-08-03T18:58:34.178Z" }, - { url = "https://files.pythonhosted.org/packages/1f/0a/711050f09a1b027b13db918e7f272ff5e7f7b4699d9994fbe39f285e2e74/granian-2.8.0-cp315-cp315t-win_amd64.whl", hash = "sha256:5c4d67d96ab296164c36ef672a55251ef12c5e4060c1796fcba6c3fd82394d27", size = 3082796, upload-time = "2026-08-03T18:58:36.052Z" }, - { url = "https://files.pythonhosted.org/packages/9e/07/1b6c05cb38172fb9e7c7aeef00be1956f7b7eeb096c4929df6eb58da399d/granian-2.8.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:efcb466cfaa76391efe5e0fb336b888aa86eb4b8e6177f79f7a01ffea03a414c", size = 4516704, upload-time = "2026-08-03T18:58:38.373Z" }, - { url = "https://files.pythonhosted.org/packages/3b/6f/c3aa2246f01fa98ed273c6e0f8f1a90f4b91afaaf6ef3104cf0802b88a2e/granian-2.8.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:018fd40bc1e4c22d367d24df1265f2a349776a20b68b756d4a52107ce8248c82", size = 4306260, upload-time = "2026-08-03T18:58:40.375Z" }, - { url = "https://files.pythonhosted.org/packages/62/8d/e13731afb5ccd64256b6d9e3b0147fc141201100d9b3f8336b557396ece1/granian-2.8.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:007fcd0bcaa9756285b8ad844729af0dedf41dbd6b3df91344f086c6db628ae2", size = 4916315, upload-time = "2026-08-03T18:58:43.066Z" }, - { url = "https://files.pythonhosted.org/packages/d9/84/1f77905ad0c4a89cb223793fb57847d4b97a435af63099d8ee98d9364d5b/granian-2.8.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4df16c414f55eb1324aa5b0f5486ea5aa39cb93e9a49ee15a83df6a987e3c5f4", size = 5074511, upload-time = "2026-08-03T18:58:44.906Z" }, - { url = "https://files.pythonhosted.org/packages/38/2e/e268ec727c3bbc397b731754a3a5f9d48f14d7c2ee13e0dac91149ef66f7/granian-2.8.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5d727b586bcb24a9568fccf37e25ef10c5bfcc2b0401cf1b28d0a570bdc3a4f0", size = 5046410, upload-time = "2026-08-03T18:58:47.493Z" }, - { url = "https://files.pythonhosted.org/packages/9b/3e/27d1725cd43e1348b0df74731f6423626dad6f761ec3c5e8057217fbba6f/granian-2.8.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:12cff22f282304378b05095484227ef55199dcb2b413e4a5f1a070c0aa5193f4", size = 4969944, upload-time = "2026-08-03T18:58:49.443Z" }, - { url = "https://files.pythonhosted.org/packages/c7/9a/3033a6c43846895af17fe01a119a6ec4fe1ffea5dc786ca4ddfa8a7c37d0/granian-2.8.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7fd9c8a78a55d003f36dfb9fbbd8f2369015c5176333449da719e430bcc0e0e4", size = 4976158, upload-time = "2026-08-03T18:58:51.619Z" }, - { url = "https://files.pythonhosted.org/packages/85/bb/806619a7225c5ed42d980e91809e86ecb1ba7d4b759d83d2758c769329a2/granian-2.8.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3932a061a803ff3441e249895fb080e6b12398a722d895ac6fb4c0f70122cdbb", size = 3138711, upload-time = "2026-08-03T18:58:53.561Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/80/74/9903bd396a3ea5e40163ce4946df974bfb9f7280834b1c59d727df65867c/granian-2.8.2.tar.gz", hash = "sha256:466a23e8cb44d4b407fa3db0f37aeb45ae722cad4d4b3701d6fa6b13ca54b1ef", size = 129359, upload-time = "2026-08-23T17:40:23.248Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/7c/961d2db920e9eb4fcb079f08a0209d4873f9cc5ce33e23e2dfd197be7409/granian-2.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1da543c6fafbae059e90df5756df17095ee059c9a9ec7acabd7dd88cc273184", size = 4646222, upload-time = "2026-08-23T17:38:23.72Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4c/cb16e44acceb935876599195f2dcc8a29394633aaeeb454a87066643e209/granian-2.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:54d64fba52ae5b29e7fb8489fdec5185859b0e299a6add92d4d69bf94d8684e9", size = 4470625, upload-time = "2026-08-23T17:38:25.064Z" }, + { url = "https://files.pythonhosted.org/packages/b2/27/3b85635168f5e87e6e25fbd3d5ff34066ba353add7f66659510748b8a71c/granian-2.8.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4053a99b6fb82e98807f854d3c6d6ebe0a49353c9eb7797999d166a99c1ae399", size = 4997931, upload-time = "2026-08-23T17:38:26.478Z" }, + { url = "https://files.pythonhosted.org/packages/70/95/2ffd4f0dd60cfba6edc0708efd5888fd5bc8898943cb8a4175b8a6758650/granian-2.8.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5ef2175682f3016589df34c0f348a7840e7513c60a1a3f60b6b78326509b5aa", size = 4560356, upload-time = "2026-08-23T17:38:27.783Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/e7098bae4e0ec550bd28e3ac7a4ffda23e1b8659d155ed4e56076aa3a2c6/granian-2.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684fbb039483b42606bf74e8675262e1947ae7303e5a844a3194e02f2f853d51", size = 5017099, upload-time = "2026-08-23T17:38:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/65/e2/fc8d86ac636d1ac2d74145c7e4e262ecc217f25941fb4ab62f1411d5e2f8/granian-2.8.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:84fd77bb1a66d9cb06ebb68fa4480204b69ef6bb314e942ebad3f2952ea0e072", size = 5180432, upload-time = "2026-08-23T17:38:30.346Z" }, + { url = "https://files.pythonhosted.org/packages/9a/55/a83cf2a1c3c0c711c0bad5d70f194fdebab6049a661b89bf7d880c8b55cd/granian-2.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d8d2ea99f8b4412eb4c320478148e58d76e5ebbb12bb487d7c64f7b4100517b0", size = 5237694, upload-time = "2026-08-23T17:38:31.607Z" }, + { url = "https://files.pythonhosted.org/packages/cf/40/16489cc1a76560ab31306a656ca65fb856edc5ffad89fb6ad8eb9a1a22ee/granian-2.8.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:79be108e63e7812237a67a7d2c97e1ab34411d4b3f7ad537e196f6afc0803659", size = 5107893, upload-time = "2026-08-23T17:38:33.099Z" }, + { url = "https://files.pythonhosted.org/packages/58/33/4ce91368b66a471f93b55a805da3f407e5c22a8888859db775f2c81e55b6/granian-2.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b4006292f09145ce642131e2cb53e79ee12a92ef0f938d8e84e5d4d28cb7a030", size = 5119465, upload-time = "2026-08-23T17:38:34.975Z" }, + { url = "https://files.pythonhosted.org/packages/64/4e/10566f58dcd8f060e881a8b3f525610897bb3d6e6a832c02338be7d00e72/granian-2.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:f2738a9c49015c65c83a077a8c35a0e525152a6505ac267ba25d7b25a4547bb8", size = 3136927, upload-time = "2026-08-23T17:38:36.224Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/394e7ddd853f732a678eb54a60fd54334070c99b74dfe156614271a4419c/granian-2.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:15fca7c867b0477209dd02940d52a35dcf0785f70081824bca5dabf7ab0b3ba7", size = 4550249, upload-time = "2026-08-23T17:38:37.552Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/a5c7d9d228e5aa3964681726ecc1f560a9179581d0b81f649d631e48ddf5/granian-2.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e45ed005bbb6cb7f77682de2c72e33797f84310f8ffd0c97d0bd5b93aae4e185", size = 4363052, upload-time = "2026-08-23T17:38:39.12Z" }, + { url = "https://files.pythonhosted.org/packages/0d/7f/6de20e29ef52bab40409e25a019419b876ddba7eba94422690c6a47539f3/granian-2.8.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c6bb7a7bbedea92a6c6c200e1b01f3b5059a6c5b5b8face1fcd396682f0e29", size = 5069488, upload-time = "2026-08-23T17:38:40.8Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0d/9b86a600cc193a41ce00c46979f763bd13dc60d8a26e98d88cd5c1aa8a04/granian-2.8.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887c822fbe85e603dcab24138fcdfa02262e41737ccc016238c435e44b4a53dc", size = 4595582, upload-time = "2026-08-23T17:38:42.162Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f8/ba7e1b9769bd1723369156acd3c0191fc1785d1fd224771a7b890842bf19/granian-2.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2fbff8464c7831cc7e2dc9dd7f04301de035c44481c1fafad7e87d2e479cddd", size = 4969631, upload-time = "2026-08-23T17:38:43.613Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/de45ae209366f174ab9201a854a20143c7af1578d6d993a56e62a4b4e7ff/granian-2.8.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7341d8672475707c733f4b6f98ca8524833aa70eaab2826f333f17214cb29132", size = 5096407, upload-time = "2026-08-23T17:38:45.051Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/b6c2b697ddd93c0066ce97a037ed0f754d05cf6bf3707c8bfe0246b9bab8/granian-2.8.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:aacebc0cbf1e4068918b0d6450ab538ad7bd4c42cd866e76bbb13f87af45def4", size = 5132279, upload-time = "2026-08-23T17:38:46.474Z" }, + { url = "https://files.pythonhosted.org/packages/82/e3/b5ce63e182a8623e1c735e60d53b0cf91f7fb0a12e588ba57a22556b5e64/granian-2.8.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:9e92f4319f2fb955f6e8f620381fe015e67c66506de73dd0e92ef0e0d10fab20", size = 5170524, upload-time = "2026-08-23T17:38:48.068Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/568cbde461f6685018afb107168592d98d4539fa63c4a63a87628eed5ddf/granian-2.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a55b966ce6e3cced43b1b337652fb714ea355e8cc4647045118b525e2e57c722", size = 5073762, upload-time = "2026-08-23T17:38:49.407Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d0/5603f7bdf8c5f2e58dd344323759d9c78688fd993253e28fda17e4c3b324/granian-2.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:9602e34f57f1c5c7c4c4b9b5fe11968c3223182cabfc6dbd7d7ee06e9ff25b95", size = 3150885, upload-time = "2026-08-23T17:38:50.694Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3c/2b62d895c472d08f41a845497808dd8c591a82ad60fc9828fdd240cbb7e7/granian-2.8.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:434deec2c9af78785c93cd7f7a81f9869afc3003170e309ffc23f9b8ad6bbd35", size = 4561048, upload-time = "2026-08-23T17:38:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/05/96ac5277451f226941a17670f3607055b84e3e12b15944af7d420d5c9964/granian-2.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:679ac93bc56b6af17363b6577b8e36c399e0283128f76d00e6254433b26fd037", size = 4384248, upload-time = "2026-08-23T17:38:53.262Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/c58067f3d913f45307c7efa7e09d4248a3b4b1289ac5b5a56c8a0730c10f/granian-2.8.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae8805ea5d0dbb31d232437df9d23bf4a21a1a7e472cce05b11594662278b3b4", size = 5070839, upload-time = "2026-08-23T17:38:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d8/2fe874582f1a688f9cceef842be1ec47c5890b7e80f596cf2ddbee116fa5/granian-2.8.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35414a2eea2adf92e71762a6793412794ae32540e1103400e62dd423f38b5a7a", size = 4597622, upload-time = "2026-08-23T17:38:55.967Z" }, + { url = "https://files.pythonhosted.org/packages/43/1e/4558dd1a4df3b4fe92e5ee29f35773f9b152dba9eaf1deda28cc8437af53/granian-2.8.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdc50c290dc26d61891255b6e118606c1fd8fbdfba3059da199052172ecb539", size = 4982980, upload-time = "2026-08-23T17:38:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/1e3edea3be88a5b133cda591a20b227cab5a4a054c6b9a2902e63d594c06/granian-2.8.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:8a9d20c8a509213bf0c3235c79c3d1892aa887521dd7ea4c36a2ee40dcdb72ab", size = 5110060, upload-time = "2026-08-23T17:38:58.902Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c4/764a6de6613395d85a4e6f6c26f048e95226454b2189987d1983e97f1024/granian-2.8.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:63ba5fada798ff9d7fdedc3bd1fbed60d8269195fd13d4f48c273024eb23a292", size = 5147486, upload-time = "2026-08-23T17:39:00.285Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/790a3bd672719c1d0487adaa2db4a962e401d6c0f2d61c7af5ae94b658cc/granian-2.8.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:b22cbcc8e5ca399c0b231a74bb87b4f477a59b4c4852daa7f488c0a6561b1666", size = 5173157, upload-time = "2026-08-23T17:39:01.788Z" }, + { url = "https://files.pythonhosted.org/packages/a8/45/c64bbc08bf1af0efbc5424fbb5760008b87b7e0782759c7367c59fa1d314/granian-2.8.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:43c670710b34b65693f3e36d7665b18eb819e4783d43368c8144e2e9deff6b40", size = 5083624, upload-time = "2026-08-23T17:39:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/b9/12/2c7c44c5a4c1afe2841cb1f3e91fa47411cc86b3c8d5409d23e5a1873ff4/granian-2.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:000d459d3b6cc7eb43ae673ba77411e27bdc1e278b6f7e4e01cf3fcdad2d4c6d", size = 3144567, upload-time = "2026-08-23T17:39:04.446Z" }, + { url = "https://files.pythonhosted.org/packages/92/f9/69107c8b1743a41e9d125cb6d2a98cb43eea76b8d33761e0b0328aef1bb8/granian-2.8.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:325458915a148c878275524ddd959bfd35a83d1672369aa45df04a52c71dd5db", size = 4515603, upload-time = "2026-08-23T17:39:05.829Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a7/c71267bcb49a9f9dab7997c2463d8a452539bde8dbb25c171c3be37be6e0/granian-2.8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3d150f1678ed5c90e3bd17db5ab66c8355f01dfc66ff46adc898e792d0cb577f", size = 4319118, upload-time = "2026-08-23T17:39:07.179Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cf/774845ff6c8bf26ceb1f439c4189bad1d31c6cd8da91f7fd88f3b6684edd/granian-2.8.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:89db0fbec47cc45c9044c4b91ca0ce00d6f048145eaa3f49e9d4b1e420057fd6", size = 5068503, upload-time = "2026-08-23T17:39:08.485Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9c/e24207aefdb08ffd9d07797d261117dd7f92b503416aab238b2ed7bae946/granian-2.8.2-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6521b5022e8d4fa0e7c68f5189ce00f5e83af7a36e84f0475f02612aa1c8c70e", size = 4594912, upload-time = "2026-08-23T17:39:09.94Z" }, + { url = "https://files.pythonhosted.org/packages/29/b5/41a8d350c3f9414f62c136290f5ffa866d589a7b225046be5839f5e9b199/granian-2.8.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d33a2be566fdb81fc6de918930a9cd3b434eb6d696a086ddbb0ff73c180402c", size = 4982988, upload-time = "2026-08-23T17:39:11.511Z" }, + { url = "https://files.pythonhosted.org/packages/9d/06/c80cc94cb48d39c59ddac3f83cbea7f25bd869b9c90d021932ea0b3eca8a/granian-2.8.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:52d59102c33717960edd3ffc4d81719509e15f06f049e6321e41ccf444d58eef", size = 5151722, upload-time = "2026-08-23T17:39:12.893Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a1/e0ab4e7ab2c24976230d7a925492c5d7337c53fcde759d793f614bb1bbaf/granian-2.8.2-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:e829a39c3ead7e91ab58cbf82800cdac4306a71805962d5e0ddb0c292d521696", size = 5186052, upload-time = "2026-08-23T17:39:14.335Z" }, + { url = "https://files.pythonhosted.org/packages/ae/2c/b88aaad8cf77d88efa9409d4defc624e9b50d223aaf5765e511a8d3f4791/granian-2.8.2-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:37536dcc0592bc7f65dcbb260173e7d5719207fdf3f8dcdc8d573bc664ad014e", size = 5170986, upload-time = "2026-08-23T17:39:15.88Z" }, + { url = "https://files.pythonhosted.org/packages/8d/02/b1ad423fe2bc981b38e42d701284aa16f39d4711e5322af5707496729e4e/granian-2.8.2-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:3a01905b1cef50c502f1866434770b2821a7ccc2cdd7b9d8ab06dee84e19720b", size = 5083593, upload-time = "2026-08-23T17:39:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f0/16d5281e21e677443afc26a504340bee8bd56194dc07a3f07bc9e5ca7903/granian-2.8.2-cp314-cp314-win_amd64.whl", hash = "sha256:23474c7cd397741bd375f2a2c66244c0406f8911619c59a260149b22f86f76c9", size = 3145278, upload-time = "2026-08-23T17:39:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/a4/dc/e497711f5379b8f09595f1d3989c0fd4e9198be8d5735797e5b23841e3e6/granian-2.8.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:825481c04ecd4c8e493a9f6c4b0f35d49ddf62a5576d7247e91d8e19a4bc87ff", size = 4483204, upload-time = "2026-08-23T17:39:20.186Z" }, + { url = "https://files.pythonhosted.org/packages/3c/ac/1e0ca7917b84d302f81929cfe1dc5ee43b58a759168008474ee9b52f3a16/granian-2.8.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:df45b9f1e7ddafe4e6e51382cb39cd458e99a5536278b5c5e713dac8c698bee7", size = 4268497, upload-time = "2026-08-23T17:39:21.7Z" }, + { url = "https://files.pythonhosted.org/packages/48/2a/122ff8c770824e34b852d32b8d3ad9562b246b4740b978a5f6b616e2f4b4/granian-2.8.2-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e16cd27f6896238d9a09998e1bd2244a68b0ea43e6325f2e6107fd6497731248", size = 4408212, upload-time = "2026-08-23T17:39:23.064Z" }, + { url = "https://files.pythonhosted.org/packages/25/8d/a1a287b41da872bd9a014b6fefe80d9a224f9d709b5b0b3b4583e2ff8766/granian-2.8.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e624b05e9c7ef50cbf7f3fb69d54a8b8e5924c21161634f02d93e2cbe845337", size = 5027091, upload-time = "2026-08-23T17:39:24.401Z" }, + { url = "https://files.pythonhosted.org/packages/74/23/3e2a836393f49e5a637049ef91a2ac1ab3088c5512fd1e3795745f152e0c/granian-2.8.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b659f4f8cfa388734550db794752dcf8fb7bcef7fa2e55ba877e961350fcb8fb", size = 4876201, upload-time = "2026-08-23T17:39:26.325Z" }, + { url = "https://files.pythonhosted.org/packages/c1/13/201a2709a0d5068d65a8b0c055d563ea8e8ca522e41520589a7629a2b62b/granian-2.8.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:77a1119ef84fbde0c4705cb09f3ebaa23807f5d4ddf4d1a5f7bf11056842b8d5", size = 5015172, upload-time = "2026-08-23T17:39:28.008Z" }, + { url = "https://files.pythonhosted.org/packages/17/41/c684ca1ed8cd2e84278d39d741cdaf39c3b8698a141e1f22370e44c54fc1/granian-2.8.2-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:8af72cee8823da6280251e53aec774abfd093588a6db9ce8193d0076851601d1", size = 5211701, upload-time = "2026-08-23T17:39:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/38/cc/0d8680d6cd9cd1f2e0629e2a999185cdb6e9c8e14e76fb31404167ab289b/granian-2.8.2-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:886e727e11706897db81d97b976c12e3613c22df299d56bde446e71906ebbc9a", size = 5160925, upload-time = "2026-08-23T17:39:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/76/83/3ef6394c090e33bc69905a630852b3931b7009f610403ca5facd7d99e046/granian-2.8.2-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:de73d86d7ae6af5b6248840c4b700a39cdb13f7ac8f5e31d74f203c8cffad8fc", size = 5116259, upload-time = "2026-08-23T17:39:33.248Z" }, + { url = "https://files.pythonhosted.org/packages/9d/09/75c9b7d78d0a15fe6b74a160fbc799d141fea9b67ef869c149bccc2de0a7/granian-2.8.2-cp314-cp314t-win_amd64.whl", hash = "sha256:144af53b25ef35e119cb15b79514600896c6f5f3bdac83f7d6c80a2a7384cfaa", size = 3109679, upload-time = "2026-08-23T17:39:34.931Z" }, + { url = "https://files.pythonhosted.org/packages/40/f6/1677a9266332a6d044247af0f89e98d89fddfa6c93ae598ed7db3be9b904/granian-2.8.2-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:63fc5f40e7e258be3f61199a73fd85f49b74d0514fbc993f5e6263fa9d104013", size = 4515394, upload-time = "2026-08-23T17:39:36.407Z" }, + { url = "https://files.pythonhosted.org/packages/03/5e/b01066c3a5695fc97654855617222c0d4e77dafd94c31dc7a1b2ed0349d1/granian-2.8.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f42da6a579030774a25df67b5432ced73bd2f8eda36df11d79739059c03c4740", size = 4318475, upload-time = "2026-08-23T17:39:37.814Z" }, + { url = "https://files.pythonhosted.org/packages/7b/81/ca766e0f223cf88e8abcd1e9edc7513b0a4960f9c351d2b9bc594545293e/granian-2.8.2-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:927e248fc2225709ef82d8fbec88e1bc44286cfcb2e033e83d9bc935e863a897", size = 5067760, upload-time = "2026-08-23T17:39:40.097Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/741cc89d3820b2d771b1f318450ebbf4d6c992b98df2e9bbb07a93020321/granian-2.8.2-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbbe64f19cdceb306b91bed01ce875e3f4ffcbafedaf2d30bedbeb33682a026d", size = 4594947, upload-time = "2026-08-23T17:39:41.566Z" }, + { url = "https://files.pythonhosted.org/packages/72/47/6eb90fdedf5c90d0057c64454a75bb3d108e942b29284ab05a5bade7b70e/granian-2.8.2-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225fc15fce8201a3d341e2eaece168a6e344dcf38cace59ecf2049be286dca33", size = 4983791, upload-time = "2026-08-23T17:39:42.928Z" }, + { url = "https://files.pythonhosted.org/packages/9d/b0/e62d321d983cfe7ac4a0472b5d66ef672aeebec7c3d9d4b6ab4fbaabf4f0/granian-2.8.2-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:d3c881e567ee36f791b850358154daadb5b1262a9bf8a56b2048f53586b1e678", size = 5150521, upload-time = "2026-08-23T17:39:44.354Z" }, + { url = "https://files.pythonhosted.org/packages/5b/00/8fb27a5f60bb0ebab2089153c438190f154613f7838ae6ac3af1d33ab408/granian-2.8.2-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:f20441ccb3b500c5e6368afc237257775fb893584aebc087157056f78643f3e8", size = 5185020, upload-time = "2026-08-23T17:39:45.903Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c8/5d85ca4818e74f9ac0ac71a88752a2f81655e3df787da7698667141fcd4e/granian-2.8.2-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:1dc5155ccadeedafa25baea4ad2cd3003db7127257ef1eb623039b2e7087c759", size = 5170661, upload-time = "2026-08-23T17:39:47.35Z" }, + { url = "https://files.pythonhosted.org/packages/bc/30/7248eafd8742ffb47ebcfd194a211b6447445214859ececbb2b26e0d4357/granian-2.8.2-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:80c10fd8879dd5972ef67cc91255d628e860f477b0f9c9f165331132916ca637", size = 5082632, upload-time = "2026-08-23T17:39:49.201Z" }, + { url = "https://files.pythonhosted.org/packages/51/59/345032b735050959c59a12e2c8cd2def3561751824ee9c08ef0779f4f3f2/granian-2.8.2-cp315-cp315-win_amd64.whl", hash = "sha256:e4ebcde088974cb23332f921b6418448d4328873311006798e61e23ceb773376", size = 3146049, upload-time = "2026-08-23T17:39:50.663Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/0a8938c3a30fbaba97d89df23d15ac05bf5a5bd9afea318734a6ad17efaf/granian-2.8.2-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:0c78a53649ce6aa238fa7da79a4a93931cacd80b7e9bdbe89b296902f2533aed", size = 4482600, upload-time = "2026-08-23T17:39:52.278Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8a/dc58d836789d4eb8e488419dd1ba2a6328b9d8ffa6465319d235a9a24c50/granian-2.8.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c14da904d02f71b22e02004188ee0df66568415f6941ea32f124a0c07d57b88b", size = 4268982, upload-time = "2026-08-23T17:39:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/0d/8f/0533072ff8f4b6e11ce722277ec65ba88879ab654dcf3f46d20c209ad70c/granian-2.8.2-cp315-cp315t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:94ea4531e2bbe385cc2dc965e1cb33015996e808f966c0a334ee2ad8f381264b", size = 4409316, upload-time = "2026-08-23T17:39:55.293Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b2/e8e9b6a3da2892673804c59c9a7c8fb29bcf40bda99f283cd47d5dad7088/granian-2.8.2-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15104fb8e7946a6639eccd89c16d05c43ecdd493c97c415d1ad0b00cb6722540", size = 5024561, upload-time = "2026-08-23T17:39:57.321Z" }, + { url = "https://files.pythonhosted.org/packages/e4/21/03f438fbcc1ca6137f5af59ff58c7eca2f5b91bd0b443a59440304ae1fbf/granian-2.8.2-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3e58821fce2fa93406bba043eac6a8c24518e63d9ead044623d1fc92205380f", size = 4876756, upload-time = "2026-08-23T17:39:58.917Z" }, + { url = "https://files.pythonhosted.org/packages/34/53/6e2539b4c667cbaceea672b2080cc7093f58b7274c787a49c17e0b086bdb/granian-2.8.2-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:39fadbc69e5279d1b5181411d80239a0ffd1fa75422903fb97e871545c8edb5d", size = 5015270, upload-time = "2026-08-23T17:40:00.493Z" }, + { url = "https://files.pythonhosted.org/packages/f4/48/ae77db1206b2409149b4a96192354250042a7664b412346d3e5b6824665c/granian-2.8.2-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:956968b9b32a74eaade95502c1664038c978731c17bb2c4e039bbdcf0279653a", size = 5211393, upload-time = "2026-08-23T17:40:02.648Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b4/0af0d59f172dfa6e56c55911e6461d7fff0d088268e02cef5e9598a9143b/granian-2.8.2-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:214d4e1b7353216e3ec16cc49d8eecb29e8949ae6e2a815891250465b7c3b0f5", size = 5161076, upload-time = "2026-08-23T17:40:04.177Z" }, + { url = "https://files.pythonhosted.org/packages/e6/77/cb8b3de1a9e33e0f9b17b74a594377c9600348758f051a2b720a82665643/granian-2.8.2-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:587f1121c44cab7df8d71b3f9bde0ac90d603096685ba212a4e193ae6fd2209c", size = 5117000, upload-time = "2026-08-23T17:40:05.907Z" }, + { url = "https://files.pythonhosted.org/packages/f5/94/0097b0b7214cedb8d39f2dc6f571fcb9b478148a11b6f6cda8ef2dda08ef/granian-2.8.2-cp315-cp315t-win_amd64.whl", hash = "sha256:c4047b3dd1b581b56808a8a5e6932bb246c81cb7de3b5eeb82b2fca23233f6b3", size = 3108913, upload-time = "2026-08-23T17:40:08.203Z" }, + { url = "https://files.pythonhosted.org/packages/9d/37/8e8a304527630457e8045ffa904864ddbeaddef7482b9681973302752151/granian-2.8.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d275ca1b6dafde6807a5b1baece9f49ecf63c2cb744f61e774800c07cac78d0d", size = 4656379, upload-time = "2026-08-23T17:40:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e6/e8195bc4847cef39cb335bd75c440c11c13d80c6dbd7c19cb59a09d75dee/granian-2.8.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:42be026b47f8bc6beda8ce01a8a193e297a93b62613340a746a06a8a17751a81", size = 4483819, upload-time = "2026-08-23T17:40:11.44Z" }, + { url = "https://files.pythonhosted.org/packages/83/6f/b4453648d3e3a812d757dde46d7fa5264b5ff5b16f1b8a6367cdac108cae/granian-2.8.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0310c68d288b7892d0ae852ec0c5e1e894a1c642deaca1e07ca18960e3336851", size = 5090340, upload-time = "2026-08-23T17:40:13.166Z" }, + { url = "https://files.pythonhosted.org/packages/a0/56/b96f84606653875cff972d418514079e634613be9845a22213045de63054/granian-2.8.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5e70dd701be4263c6b2b2f16094bcb6f6ed03fe7782165e40f850fb746a109b8", size = 5266359, upload-time = "2026-08-23T17:40:14.873Z" }, + { url = "https://files.pythonhosted.org/packages/49/69/94ba53cc13222b54747b8aedf60900d776f077548ff44749b04aa6ad859c/granian-2.8.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dac13e7f83f797e9a9106ce6d6e0263a65962188ceba74326fcfbe1d485c658b", size = 5303236, upload-time = "2026-08-23T17:40:16.564Z" }, + { url = "https://files.pythonhosted.org/packages/80/97/020970b6a28ce1faaf7867ffb027d46bcd9d52352a27e6312c517525fff0/granian-2.8.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:77dacca3c0a858b958a7442557652d182f985a3b335f43d22e65d46929975f22", size = 5065183, upload-time = "2026-08-23T17:40:18.29Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9b/a2071665a342905dfdacfd774e21cf380274bc0c4da95e249b5c7b5e136e/granian-2.8.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:8475e23ea2aa9dae4bac28f3ccae403e2cd07c36162a2b4a2bf8dbe43cf28509", size = 5186529, upload-time = "2026-08-23T17:40:20.017Z" }, + { url = "https://files.pythonhosted.org/packages/8b/6a/0d333d1e2433348bcd87d0d0281c6c4abfe2a66a24c90180faa15597b7b0/granian-2.8.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f5967b021e36448d870012c0853342a119d7427ea67e3b23abb8798897bf14be", size = 3148213, upload-time = "2026-08-23T17:40:21.607Z" }, ] [package.optional-dependencies] @@ -1316,123 +1298,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/8d/d236e9c82fe315f9128885c8be3ec719f41965a1eb6b6f4b42470904cd41/pyarrow-25.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:13240f0d3dc5932ccd0bfa90cd76d835680b9d94a7661c635df4b703d40ce849", size = 28743657, upload-time = "2026-07-10T08:29:42.742Z" }, ] -[[package]] -name = "pydantic" -version = "2.13.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.46.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, - { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, - { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, - { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, - { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, - { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, - { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -1492,18 +1357,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/b2/1d2a993c532146dce9eca5b5942d51898021c3579ce18b2454f932a915f8/pytest_codspeed-5.0.3-py3-none-any.whl", hash = "sha256:fe2ea83c924c2250675b75686c3ee456b8cf0208d83d552e182a195fdf467378", size = 74033, upload-time = "2026-05-22T16:20:26.814Z" }, ] -[[package]] -name = "python-engineio" -version = "4.13.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "simple-websocket" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/56/10a529f5396df653181f747997f970dba31f8f2eac3b9a88c1f9d7bb25c3/python_engineio-4.13.4.tar.gz", hash = "sha256:413cb98d56c62f0f5ef29931592a360d437b82b3fa7ab415da3f6c7d3ebc0cb7", size = 79880, upload-time = "2026-07-31T10:30:55.852Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/3d/26e14cf47c56c9ba3c3e12cae21f24716bc3182bb52260213ec0c819d0b9/python_engineio-4.13.4-py3-none-any.whl", hash = "sha256:272de73124e255d3d2bba6f86358c1a1ba618f938f337a0c868b60550fe38719", size = 60129, upload-time = "2026-07-31T10:30:54.637Z" }, -] - [[package]] name = "python-multipart" version = "0.0.32" @@ -1513,19 +1366,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, ] -[[package]] -name = "python-socketio" -version = "5.16.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bidict" }, - { name = "python-engineio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/32/2d/ffce71017c106b75099fea569df6518c63fee5d6202ce0cfe7b01e6f22c3/python_socketio-5.16.3.tar.gz", hash = "sha256:89b136f677ae65607a84cecda9b4d6c5377b40a97582c504c25df89af16d520e", size = 128095, upload-time = "2026-06-15T22:07:04.003Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/38/8c5e72d53ff8eb27497c4f268a7f6d9121e727a50b65248288ad79a93053/python_socketio-5.16.3-py3-none-any.whl", hash = "sha256:e7ad14202a5e6448824c7c2f86161d04e13dec05992257df5c709e6a2798c041", size = 82087, upload-time = "2026-06-15T22:07:02.498Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -1595,8 +1435,8 @@ wheels = [ [[package]] name = "reflex" -version = "0.9.8" -source = { registry = "https://pypi.org/simple" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, @@ -1604,7 +1444,6 @@ dependencies = [ { name = "packaging" }, { name = "psutil", marker = "sys_platform == 'win32'" }, { name = "python-multipart" }, - { name = "python-socketio" }, { name = "redis" }, { name = "reflex-base" }, { name = "reflex-components-code" }, @@ -1625,46 +1464,33 @@ dependencies = [ { name = "typing-extensions" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/b7/73bedfef3eeab60b3f3c675f292dddbed7061eff5524515e5b7e5c9a035f/reflex-0.9.8.tar.gz", hash = "sha256:ed0d2905c4968226e37da6999ccad77d4600a1370399afbed1038826dec3fc9b", size = 406093, upload-time = "2026-08-04T18:55:55.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/c8/d34ac31978bce63d9bddcbd3ed1522ce0f367290a5923a6e3012618c5583/reflex-0.9.8-py3-none-any.whl", hash = "sha256:4f50452f5c6540d98711bfdc138085ce5339c5b3b2cf27444b93952e10906f86", size = 660205, upload-time = "2026-08-04T18:55:53.86Z" }, -] [[package]] name = "reflex-base" -version = "0.9.8" -source = { registry = "https://pypi.org/simple" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "packaging" }, { name = "platformdirs" }, - { name = "pydantic" }, { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/5d/9d9bed0d9294276d7e4b7d9b1e6f3185fa888657d0e73a996de30012a6c7/reflex_base-0.9.8.tar.gz", hash = "sha256:e4c6c5a221920c153361f7855c8ab6dc43fc6a9f1dfce402c6d631728f313d82", size = 275540, upload-time = "2026-08-04T18:54:24.365Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/f8/16f00aceb2f8cac1d42c4ed47f91d76aa4fa242fb011e5cfe0fe27449589/reflex_base-0.9.8-py3-none-any.whl", hash = "sha256:e91c453fef26b70d058bcc926f5811d01e8b01257f033507efed22734ed3be8f", size = 315203, upload-time = "2026-08-04T18:54:25.317Z" }, -] [[package]] name = "reflex-components-code" -version = "0.9.3" -source = { registry = "https://pypi.org/simple" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, { name = "reflex-components-lucide" }, { name = "reflex-components-radix" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/28/599dc8a3f78a0ad2cb2981c4bc2f0db7c10b3b28e2b3173b524139f549c8/reflex_components_code-0.9.3.tar.gz", hash = "sha256:a6127241d3d0ce43ca2466eb6357447ea4d596b9bbb92a1aa479413799c0bcc7", size = 19648, upload-time = "2026-08-04T18:54:19.274Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/53/729de287888513dfb0cbb267ff91ce615993fb876ec75d03acfcaf16f96b/reflex_components_code-0.9.3-py3-none-any.whl", hash = "sha256:32a3d0c6d17f8950affe0ae3768149e2f1c637d38b634813878f4ad62e4b766b", size = 24157, upload-time = "2026-08-04T18:54:20.145Z" }, -] [[package]] name = "reflex-components-core" -version = "0.9.8" -source = { registry = "https://pypi.org/simple" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "python-multipart" }, { name = "reflex-base" }, @@ -1673,144 +1499,100 @@ dependencies = [ { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/a5/4c9fe7f48838cae2d7059e69a3e9c5460c0cb8d6f1637d01dbc8f6de39af/reflex_components_core-0.9.8.tar.gz", hash = "sha256:a07599767a4cafe9d95e604eea1f46e45dea30569a9e35439483e2ebbccbe989", size = 109553, upload-time = "2026-08-04T18:54:30.369Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/dc/6db089f17eb00da7528375b109d71eb8992e188a1ff638fecbe4b88dc760/reflex_components_core-0.9.8-py3-none-any.whl", hash = "sha256:30a1ef058c698a4d6338716d308c205f03c5a4ea14caec50f98db4ac6fe09129", size = 177847, upload-time = "2026-08-04T18:54:28.314Z" }, -] [[package]] name = "reflex-components-dataeditor" -version = "0.9.1" -source = { registry = "https://pypi.org/simple" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/13/a0cbecb1ea03d79b3943dcb236e07778da7b83917ad34149aa43e8b20480/reflex_components_dataeditor-0.9.1.tar.gz", hash = "sha256:eedb9a0807286f4c870bea92a0e94cac10e2c6decc6f495600810bfeb1b0c329", size = 8219, upload-time = "2026-04-27T20:56:49.894Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/dc/c7e5ac166f23a10899e44d4133f260a0be906279b1698dd63846a21d3324/reflex_components_dataeditor-0.9.1-py3-none-any.whl", hash = "sha256:8d2dec22fe0c227c44f45226624ffa87227e8ab81acab1f5f17c6dd1efee9372", size = 10776, upload-time = "2026-04-27T20:56:50.576Z" }, -] [[package]] name = "reflex-components-gridjs" -version = "0.9.1" -source = { registry = "https://pypi.org/simple" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/ec/785dec6e349525596ffe2a0cdc2c866c4dd7586aaffe5a90b4ef72829267/reflex_components_gridjs-0.9.1.tar.gz", hash = "sha256:78f9aa0a63b06dc919b4e7b3cb23764558199aeefd3aa59fa556c6430ead9a64", size = 3226, upload-time = "2026-04-27T20:56:39.165Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/08/fac0660cc51a705552e91270c3df39e3a4292ce570e10f64cf39a1bed960/reflex_components_gridjs-0.9.1-py3-none-any.whl", hash = "sha256:02582137af88ce7c3983a3fc494b746cfb9fee5bc69a96ae03027c8a102a4b13", size = 4303, upload-time = "2026-04-27T20:56:40.302Z" }, -] [[package]] name = "reflex-components-lucide" -version = "1.0.3" -source = { registry = "https://pypi.org/simple" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/e9/89ddcbaf1b80252e7e21864a5d71de949808e7265aaff6dbaf2972a936c2/reflex_components_lucide-1.0.3.tar.gz", hash = "sha256:3e58801dc5a1355d7cf09181b315005af52cc8936c9c937249b76c33734310e6", size = 20367, upload-time = "2026-08-04T18:54:17.804Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/32/d7eb13de1d9e79e01bd1bbe4453674bf5300b771de8e3f39ccf6a9d55661/reflex_components_lucide-1.0.3-py3-none-any.whl", hash = "sha256:c50ae0557252339a592407d6472e9b67c1a3519a875a39db6674e8efa8aded21", size = 20463, upload-time = "2026-08-04T18:54:18.603Z" }, -] [[package]] name = "reflex-components-markdown" -version = "0.9.3" -source = { registry = "https://pypi.org/simple" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-code" }, { name = "reflex-components-core" }, { name = "reflex-components-radix" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/86/28ccdc6ba418b48539257bf794932711ccd37b4d911ea420dfc018d2a564/reflex_components_markdown-0.9.3.tar.gz", hash = "sha256:cec631217e893b21a202cb86734d204a9a268d4f5cac8c6711c19d90e09b62be", size = 8089, upload-time = "2026-06-04T01:44:13.45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/c7/72720fb15d02681466b56c5af7ca6b84831cea8f776446bfbf95af7feb9b/reflex_components_markdown-0.9.3-py3-none-any.whl", hash = "sha256:4bd8a6d0be5c4b715e855936e49ca513caf7ba470d2b07d87f60d11c53edde1d", size = 9795, upload-time = "2026-06-04T01:44:12.692Z" }, -] [[package]] name = "reflex-components-moment" -version = "0.9.3" -source = { registry = "https://pypi.org/simple" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/0d/65d07250c6a61a9f9c006cfc23b8b4fde4a70ef5f1270a53961ee467f691/reflex_components_moment-0.9.3.tar.gz", hash = "sha256:e470f3eed6905c15189215c11bee775ddbe58b943e4d1a5aebff9a2ca1ef5dae", size = 4224, upload-time = "2026-08-04T18:54:22.486Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/28/7477b9663118d9cf602a1393a182d4e02be85e60523dda4bd859e978e811/reflex_components_moment-0.9.3-py3-none-any.whl", hash = "sha256:1645b2c67f5d753508cf732a17c270ecd41464139c1acec04b3232c8f1d4f3f8", size = 5287, upload-time = "2026-08-04T18:54:23.206Z" }, -] [[package]] name = "reflex-components-plotly" -version = "0.9.4" -source = { registry = "https://pypi.org/simple" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/e8/45cd231f3225239ee215c7f701fc5ad4d57d31f598292ff2512e629672e4/reflex_components_plotly-0.9.4.tar.gz", hash = "sha256:bef809afc62be80e592a21dfe2e0d006f5ec86bd6b341cac43424dfdff41319b", size = 7254, upload-time = "2026-08-04T18:54:25.53Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/d9/0f0fcd90ec077779e2c030b2abd6685d52b46848730dec32996048b9245a/reflex_components_plotly-0.9.4-py3-none-any.whl", hash = "sha256:c99cc9d259231d15ebe73f4070108309e19868a74f4b1ecd776ad7f92f56d2a2", size = 8312, upload-time = "2026-08-04T18:54:24.624Z" }, -] [[package]] name = "reflex-components-radix" -version = "0.9.7" -source = { registry = "https://pypi.org/simple" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, { name = "reflex-components-lucide" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/7b/8df7e7462e9837e1b11e1b73c34dcf1640b8b4a97ce37d7a609a31163961/reflex_components_radix-0.9.7.tar.gz", hash = "sha256:e50a5f71ac65c52a1868041225d5dc21c2d4276c036fc9b9d8685fb62bbd939a", size = 114585, upload-time = "2026-08-04T18:54:24.521Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/8b/aea526621cb00ad861f2a9b57349807674d358d21c06678241af2376ce7e/reflex_components_radix-0.9.7-py3-none-any.whl", hash = "sha256:3e9b8215b9a5f2e219926362ed807030cbf82fecfcef83450232bddcb5553ec7", size = 271699, upload-time = "2026-08-04T18:54:23.034Z" }, -] [[package]] name = "reflex-components-react-player" -version = "0.9.1" -source = { registry = "https://pypi.org/simple" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/6c/d8baefacba10e2a91f13eadc027e17ba38a7b6d2ed083e121f5dde4e8e59/reflex_components_react_player-0.9.1.tar.gz", hash = "sha256:5ba227e1c8601cf206923d4095ad4f241fcc176937dc627e1c2deaced3803199", size = 5004, upload-time = "2026-04-27T20:56:48.576Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/96/cc6c29bc4e85c3de8915c2a2469d8b82e463f4e311486f7fded5a7399ac6/reflex_components_react_player-0.9.1-py3-none-any.whl", hash = "sha256:f9c3e4f9d8bd7e3cf56a18d9e8d83bd2980d9c57db81e7325ef1f0654a8c1847", size = 11276, upload-time = "2026-04-27T20:56:47.721Z" }, -] [[package]] name = "reflex-components-recharts" -version = "0.9.2" -source = { registry = "https://pypi.org/simple" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/de/d987e3c68973c6f818dc73ae1d13910b70113e3221fdf8d61866d6f3582a/reflex_components_recharts-0.9.2.tar.gz", hash = "sha256:d79866b90b9968abd0998da318e8842a218f807c151de8d2b58648074efbb662", size = 32968, upload-time = "2026-08-04T18:54:25.737Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/49/dc5c98fbf1a8ea9828388df9e6561ed443ce4b217f1df93b7d3c3e82530e/reflex_components_recharts-0.9.2-py3-none-any.whl", hash = "sha256:fba108ad9ab049131c407dbf376b8716fb34ac03738d62ba40e02555607017b3", size = 44583, upload-time = "2026-08-04T18:54:26.618Z" }, -] [[package]] name = "reflex-components-sonner" -version = "0.9.1" -source = { registry = "https://pypi.org/simple" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-lucide" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/e8/831fc0ad0074df749642da94c8baa02e47719110bdbf862a9c44fbd3c016/reflex_components_sonner-0.9.1.tar.gz", hash = "sha256:1c8a45db24a1beced3d4d90048c415811022bfae6a3b95dc8925e064edfaf1cc", size = 5628, upload-time = "2026-04-27T20:56:43.453Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/a6/9cb6672f7cf6d0d21d803ee6f22446cc5e3ab01448d01e42b7b42023dcb0/reflex_components_sonner-0.9.1-py3-none-any.whl", hash = "sha256:a6051e0f178fe5e644f936e55833bb9149126e08c6f41550b1f488ece68d794e", size = 7433, upload-time = "2026-04-27T20:56:42.148Z" }, -] [[package]] name = "reflex-hosting-cli" -version = "0.1.68" -source = { registry = "https://pypi.org/simple" } +version = "0.0.0.post3495.dev0+02dae560" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "click" }, { name = "httpx" }, @@ -1818,10 +1600,6 @@ dependencies = [ { name = "platformdirs" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/ea/fbbaf062b201929ff76ddbdf57107546de2466a4f4b83bb1a438b34d7c42/reflex_hosting_cli-0.1.68.tar.gz", hash = "sha256:c5341578134b2f7c486ec3e4a675338e2985f0ffaaa2f9a30f9a0233a0791d08", size = 50968, upload-time = "2026-08-03T19:43:17.311Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/7d/5fe9e6b2682faa21c0a3f2b86582d6c4bae6654fa145f9b552e9eed424c6/reflex_hosting_cli-0.1.68-py3-none-any.whl", hash = "sha256:1f4d3405c611da7374516dbc6837040ae0812de39483d1aa3133692a3df3836e", size = 61159, upload-time = "2026-08-03T19:43:16.227Z" }, -] [[package]] name = "rich" @@ -1861,18 +1639,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, ] -[[package]] -name = "simple-websocket" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wsproto" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, -] - [[package]] name = "sortedcontainers" version = "2.4.0" @@ -1952,18 +1718,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - [[package]] name = "uvicorn" version = "0.52.1" @@ -2090,6 +1844,161 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, ] +[[package]] +name = "websockets" +version = "17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/72/fba934cb3dff7a85d811820efffcd141ddd52b5a2a01637f64551373ff4d/websockets-17.1.tar.gz", hash = "sha256:acfea4c20bf54384883ea33b1240fc1db4f52e190823a4e2b334bc3e8bfca96a", size = 187520, upload-time = "2026-08-26T17:25:33.063Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ad/66a74d42fb537bd44056483eae6cbb7ebb10b742c300a0bf8cee427556d4/websockets-17.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:88b882764ef65147a7a5ae13168dedbe225a04e2ff4858fe543f2c402f093e9c", size = 216984, upload-time = "2026-08-26T14:55:20.747Z" }, + { url = "https://files.pythonhosted.org/packages/70/1b/344ab22cea729e872f759b926441f7b822ab6cd106db527736afc066927f/websockets-17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98a5b2589a56a4b4f098b0a958099a4356ab904a7844f1da3841efca469af7e9", size = 214667, upload-time = "2026-08-26T14:55:22.298Z" }, + { url = "https://files.pythonhosted.org/packages/2e/42/bace574b6ae80e1a8d6935b8c5f03fb67236233ec572e976fe826ff719cf/websockets-17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:020e271205f8ab3406d7a59cd00de6dec722315924411c421bd00642f18bad86", size = 214944, upload-time = "2026-08-26T14:55:23.618Z" }, + { url = "https://files.pythonhosted.org/packages/ee/87/08e35ca4a0ffafb500a16ff461bf9561ad2b755362adb5d077d4dba9affc/websockets-17.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:65be6bda2b537fefa4b3a5ccd6ab386533ce39dd8fe62433ec90901fdc81752d", size = 224004, upload-time = "2026-08-26T14:55:24.748Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/00ae2e147eaa086fe8bdddd36f57216ce72b9a9dfc0b17c717005ebdacaf/websockets-17.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c84bdef916556cbe1d5a43b423398be4dd3cba6522b463e53d848578b920695", size = 224278, upload-time = "2026-08-26T14:55:26.016Z" }, + { url = "https://files.pythonhosted.org/packages/98/e2/7aeb4e00defa68826f449392922a382ce7fdf542fe52190558dc1714e284/websockets-17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47a62d6045c6eaa0d8f97bc2fb68b8cf90077a0cbfd4e83d6f2d2145611ee134", size = 225511, upload-time = "2026-08-26T14:55:27.183Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e4/655be3d93c3edbe1a51606073b5454ea6b1b32d87aa26253a6df952417b7/websockets-17.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34879e19bb0a3c44f9317679435aea5327fac993933a704cbf353bf1234b10c7", size = 228802, upload-time = "2026-08-26T14:55:28.431Z" }, + { url = "https://files.pythonhosted.org/packages/e4/33/98549a2afa9d68fe1b5a8e0a61cd461a43ea1ab7209bce675eea67c79190/websockets-17.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2d72879819f5145a342d0030c418702496c65a4b913ef81f5ae944dd91dd50f6", size = 226075, upload-time = "2026-08-26T14:55:29.695Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6b/cbc27e014d6c292b9b2709cfd32781a2b61eb30cd4c9130e7c57e41a204a/websockets-17.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f25e099fdfe3b09f953d84698f729a1f7d1e99101b2787d7a28ed77b323750", size = 224846, upload-time = "2026-08-26T14:55:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/10/a6/e57925f7a423d90f24559e85bac21a7f0b44c0cf4a5c0babc0759ca54bab/websockets-17.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:469355ab1af100b9380f1afb09985019f4a4b94fa1dd0e9396db4361626d7ab8", size = 222136, upload-time = "2026-08-26T14:55:32.376Z" }, + { url = "https://files.pythonhosted.org/packages/32/07/b9de0400addb542ba7c819022abc3afa46cd7e518068881bceadac69d995/websockets-17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00679b7468b4c2b12b0757118174e8eabac56bb2f579a928a104d9554a56e098", size = 225000, upload-time = "2026-08-26T14:55:33.527Z" }, + { url = "https://files.pythonhosted.org/packages/4c/bb/af2828a1d7f2beb792af6ba56d7b02d56262070266b95d2af9ef391fbfb0/websockets-17.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a9fe648abd1d9b89aebfa30407bfdd08a0271ec5dc7d44a4c6ccd1ce22cf562a", size = 223592, upload-time = "2026-08-26T14:55:34.636Z" }, + { url = "https://files.pythonhosted.org/packages/fe/51/7379f254730c1dc7d8e4dd8d686868d8f7be55bdc94c6d3a44538840f639/websockets-17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f47aafd92aa28b941180e6da8a42b0f711851b14b81a5b6bb28dbbb1fa35152c", size = 224360, upload-time = "2026-08-26T14:55:35.777Z" }, + { url = "https://files.pythonhosted.org/packages/88/c6/fcd91320dd71dda7046df9bc60f60c70ee15c052dee21e31ed6221dc8b5d/websockets-17.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c89406fa3dcd4aa8662c6406cc5c0de1790e9614d2c3aaf03ca53a8a8ccf3405", size = 225404, upload-time = "2026-08-26T14:55:36.85Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b2/655a4f939388079f80f1b3f8a1b9d40783e70a376e7423988cc9a590a09f/websockets-17.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b3b451fd2723ad3191a209afe6f3f4bc86c83e9a85bdc255353b91803ee6aa66", size = 222982, upload-time = "2026-08-26T14:55:38.006Z" }, + { url = "https://files.pythonhosted.org/packages/87/75/37c84c4371c6aa668910d7841036c4397ea10554c07030603ccd5e44b02a/websockets-17.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:054c28db2dcec0e857e3b705d8c28012613e555b38c765d6a4f75340a4fc06a0", size = 224017, upload-time = "2026-08-26T14:55:39.39Z" }, + { url = "https://files.pythonhosted.org/packages/5e/20/8a9a94323bfcfe03bde3f9d98926bea4855856359702fe3ca0d07051ef5d/websockets-17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f8e822efd54137d8cc8310eb64635ab827a4a6c72ff08691f38aa624776d8ecb", size = 224252, upload-time = "2026-08-26T14:55:40.498Z" }, + { url = "https://files.pythonhosted.org/packages/3d/dd/aa66e6500188cd40306abeb92c9a738ca6dd7029d8d8532c538055ab5daf/websockets-17.1-cp311-cp311-win32.whl", hash = "sha256:dcb8d5f7edef7a399d322cf28d4c4e6f98dab64d301c8f50581a1080e5198142", size = 217484, upload-time = "2026-08-26T14:55:41.763Z" }, + { url = "https://files.pythonhosted.org/packages/01/a2/cdf3b551f0b9177023afd3a45d3b431a0d4064951008c4321a8b42ac2288/websockets-17.1-cp311-cp311-win_amd64.whl", hash = "sha256:b1bc819c6db90e8f91a38250a1ab4c058261871aa52d2fe36382eddedf146dee", size = 217779, upload-time = "2026-08-26T14:55:42.942Z" }, + { url = "https://files.pythonhosted.org/packages/e0/13/51253dbed7d16a4bb87b05110ad3bf12165f410e915f6da1edd4186d8dc1/websockets-17.1-cp311-cp311-win_arm64.whl", hash = "sha256:edadce7a22052056fd4384543019856b34850363c9d387929f677ae01d79709c", size = 217710, upload-time = "2026-08-26T14:55:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0d/098f23c4c858e5de9459ffc554fa07d5493fbcfca7f040b5800cf1cecc35/websockets-17.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:76dd004f59115087c7b700474cb18f01325e37250032e19396c08ae41448e4b3", size = 217015, upload-time = "2026-08-26T14:55:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/13/86/bc1317b1a4d8c4688e2a7e564b5e004dab44c2534d7ca05de6ae9a863fca/websockets-17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:581fa678ef46f4277cc8491312468e582f8ad609dbab907ba6096a08c6a0ff98", size = 214692, upload-time = "2026-08-26T14:55:46.366Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e7/df821761772beaa48c211ee0e234930b35c1473778470773823f56d3911b/websockets-17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87f0d5e77548b0c40c8464cdb6108792e7e53f487c6400028a4ec28a8afbe5ab", size = 214959, upload-time = "2026-08-26T14:55:47.885Z" }, + { url = "https://files.pythonhosted.org/packages/3e/92/c3fb72f11764812fc648bf3838d224972427b348e8b3989d9e0a9df87da3/websockets-17.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:882af300d2c6a092b93767d5de03c7bb56dfb06314140c8e872d3f48e09f7b74", size = 224278, upload-time = "2026-08-26T14:55:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/fb/05/9f82d090c8d2d861604147ef6dfb938a90b039f9358d5193f1df62558593/websockets-17.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c863507ada5805517ca6dff1c524dcd42942efe6304dacf06700878398d21a6", size = 224557, upload-time = "2026-08-26T14:55:50.348Z" }, + { url = "https://files.pythonhosted.org/packages/8a/50/5cbf677b865290fe36819ff00615826e7edc1df38786f770123ff39a933d/websockets-17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d41ef69d5416fbc1d98cf96c37be6192d10fd101c3e0f8b3ddc36e09432b3c08", size = 225791, upload-time = "2026-08-26T14:55:51.75Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1c/eb8a032285243381b09a221ae384c972d5000453ad136add4d1595cec798/websockets-17.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5aefe78e6a3077fe22b5e64b04666a85a3eb8b934d40e8595a693adcbceb6f11", size = 228574, upload-time = "2026-08-26T14:55:52.922Z" }, + { url = "https://files.pythonhosted.org/packages/69/85/413736251cb3ac04ce84cbd90e893d9a36a9698d4820b323aff3aa187e50/websockets-17.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f64e001bb7fa89b9f32cfa600bf8e9ac8ca26759d9b92ae01453ee303d9cd7b4", size = 226428, upload-time = "2026-08-26T14:55:54.263Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2b/a08bcc7fa1ca81a10f84ba32b6e6edd73a913f4b0c2640eed1fd626efacd/websockets-17.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:677014a073bcb1fbaa7e21144786864f16c08f856d66834f611eceb9006cbab8", size = 225184, upload-time = "2026-08-26T14:55:55.943Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8a/3bd2d0cf6b148c8c866d5d9fdcde30c04bfd81fdfac86813e69377eb4448/websockets-17.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0de501b7f2db11e83739ac20e2d33d46da4604b829f506c24be80e7def069391", size = 222430, upload-time = "2026-08-26T14:55:57.103Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c9/8e891ae342668735eabbbc669895e15195e4b45f24a4beeb58af76f414c7/websockets-17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f62114a54117e4948a1e414e89521f7fe1e3c2f83f2a571a06a4fc6718b0900a", size = 225227, upload-time = "2026-08-26T14:55:58.375Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6f/c816f332dca11425e9bda7c07f7573eb5c5f8a735849d02b0d81e8ee20fa/websockets-17.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eec113a5b41d124ef42ff56b0d74a6da3fd986400038eab9e58ee42a4024e837", size = 223831, upload-time = "2026-08-26T14:55:59.664Z" }, + { url = "https://files.pythonhosted.org/packages/53/67/5e91d5308ce24fc1ec74f56536c12f4888bad45ff5ea50f3180f8c518c57/websockets-17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5f051f8030a51815dc00e24bd2e5f1435af095c1cc111d747ac6e2a3620d7641", size = 224600, upload-time = "2026-08-26T14:56:00.873Z" }, + { url = "https://files.pythonhosted.org/packages/bb/96/faa298ecf2570d35b0eb37caddf4992178d907e108ed74bfffb6bc092c29/websockets-17.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:655a8e28010f09fd6fa317e857afab3af7647f33e41dee88fa421e92086d1090", size = 225707, upload-time = "2026-08-26T14:56:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/0b/12/5710d2482ca5061c1eec5eb46f6313837c760d4115b1795c85b6c08be4e3/websockets-17.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dc2b79afc074d2f3e64b26539350f697fe1b85ea1c49ea24eb588f247b053ce1", size = 223263, upload-time = "2026-08-26T14:56:03.092Z" }, + { url = "https://files.pythonhosted.org/packages/27/47/0c30f4eebfd1d93fae779d268f678d48847fb98516f5200849574eee8820/websockets-17.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e4bd7eacb87d8cf3ed70d6392c770a0d92441f05d7d2a3efafb5bc171d5e3067", size = 224244, upload-time = "2026-08-26T14:56:04.321Z" }, + { url = "https://files.pythonhosted.org/packages/41/33/46c256195a1255079ae23d1b1267b2e1843dc5f46a67f973cdf2a3523dff/websockets-17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ccbf3f4a9890d50b3a08ee04029fde30a03bfdeffaa19977628bf17251764e60", size = 224520, upload-time = "2026-08-26T14:56:05.521Z" }, + { url = "https://files.pythonhosted.org/packages/06/9a/aef0792731df4352e5f417369b532b3325fe434765ca90c193f594ae1e67/websockets-17.1-cp312-cp312-win32.whl", hash = "sha256:7e724f843fa6a0614aece65a7c73e51d0f4412ca41dccac13c3caf98e69536bb", size = 217485, upload-time = "2026-08-26T14:56:06.715Z" }, + { url = "https://files.pythonhosted.org/packages/50/23/493ecfdaf32898e5ea24dc900e33e5e317f9662d5d9ab2d44b2e111b4e1c/websockets-17.1-cp312-cp312-win_amd64.whl", hash = "sha256:617243e19a0992095956f406ee9cd3bc4ba92862d83cb1d83bb59ce574412bec", size = 217786, upload-time = "2026-08-26T14:56:08.055Z" }, + { url = "https://files.pythonhosted.org/packages/97/3d/91954e2f7876f74ce1213e9b92c65a63b559cc4b942a931ebeb351cd9932/websockets-17.1-cp312-cp312-win_arm64.whl", hash = "sha256:9f4a08ff7cb68c27b18e09223cc6304e01d0f82d5a240d251266dfd2e6e44729", size = 217711, upload-time = "2026-08-26T14:56:09.267Z" }, + { url = "https://files.pythonhosted.org/packages/1d/31/5f6450a7879f4f063ef08897cc385ea3ce3f1fe17f08b11e3fd959abdf27/websockets-17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a0162a6372110a5601cb5c9fd826635cedf69f3e110c545dd19774e040b970e", size = 217006, upload-time = "2026-08-26T14:56:10.509Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2a/c1b006fc861695d2aa4e35327b842015ce1d98cf8f99241829b3d6460bfc/websockets-17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:829dba1bc049779de9b332088c1a6a9858e96bd67e50b6b644a95e02b67836bc", size = 214690, upload-time = "2026-08-26T14:56:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/46/69/66e5b7d01445e0eeb1d4ab419c30315f2c90cf7a8a8cd4ecc47f894dba54/websockets-17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd8f47dbf2e8adb15c847215f83436de3fdb120b51fdae0fbbdf69fd97a3ad80", size = 214947, upload-time = "2026-08-26T14:56:12.923Z" }, + { url = "https://files.pythonhosted.org/packages/07/ce/033cafe2d2538562efa876b9149a2c7a0f7787870a4b1bb6e28adc9ceb6b/websockets-17.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f4c0377a83e163a303514fdfab501dbe379bdc13e5b9312a91d112658b29dce", size = 224329, upload-time = "2026-08-26T14:56:14.212Z" }, + { url = "https://files.pythonhosted.org/packages/34/c7/e1c2e8a67f6cc0aa43abe0046fb3b7a020980649e6a843751dc7ce9eb170/websockets-17.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c3241d684a76eaaef8b2dc789afde4343cd3aad55ea81e4e8ab3605b529bae51", size = 224611, upload-time = "2026-08-26T14:56:15.702Z" }, + { url = "https://files.pythonhosted.org/packages/be/de/07c6d48eb3d2069709410c851e7de10ab83d752c4bd09862899627c2729b/websockets-17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5f5c7a893507d0e83a80b88aefd6522f7e882cd53f9722c6f23f5a020c9557c", size = 225848, upload-time = "2026-08-26T14:56:16.962Z" }, + { url = "https://files.pythonhosted.org/packages/f3/dd/3c68572d20509648cc2fb6f50ccf3deeb4b87270f2c8966e99476e278ea3/websockets-17.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00bf34b64501e3477e81fc281532ff3cbf4da26633c10b63979d5085d46602d3", size = 227290, upload-time = "2026-08-26T14:56:18.204Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4a/8f6651c8a22093539c9215af0c5bbf217b87b382c99d2112039b92d593c2/websockets-17.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ce0305b702b20d1e1d60a9aaace6bc89970e1753565543f310d549eab22c2435", size = 226476, upload-time = "2026-08-26T14:56:19.459Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/f6fc33cea86b1127fd1297b18c107e81580ab55a73a39f9a934441ef321f/websockets-17.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29176d8b429cfa0fa443c473878d37a5c06cfd0cb36b71ba4314accc71e05906", size = 225233, upload-time = "2026-08-26T14:56:20.939Z" }, + { url = "https://files.pythonhosted.org/packages/cb/83/65edaf05f7c9b1dea82f4d252fdc37706a84571646f06119a27b0a16fe19/websockets-17.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3709a1ab30b4b922027d22f68d2b61a0656a91680ac894a537624e6be7dd7f7c", size = 222488, upload-time = "2026-08-26T14:56:22.208Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/d1169c2f7f1f0032b0d4b0c00f0711a070cd7c735de37bfeb876bc0f9606/websockets-17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:43bd0c1ceb924d67f5c1a5254d8361dd9d94246e6331a726064dfa2917880780", size = 225295, upload-time = "2026-08-26T14:56:23.445Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f4/64e2a386c3899b917c2933225c9b47887874229d159797f3bf1a11c20d51/websockets-17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1fce0f43e0d41422e0b2cad6561e1970df22f212f4c7e884967df7cf591b031c", size = 223891, upload-time = "2026-08-26T14:56:24.647Z" }, + { url = "https://files.pythonhosted.org/packages/26/b3/dfb5c482f7e310a3432fdbb045ddfe6d34114680e89a233d4ff900a32961/websockets-17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4031152769179ab8dcdeafc7b0e58052a49117560a28671700b47b2c7b717aad", size = 224661, upload-time = "2026-08-26T14:56:26.027Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cf/94865130a336029f46412adc127c4fbe380f46172b90ce251369e35c4302/websockets-17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a06f3b5085176763182449559e20391d7ce616a8972a9f7a33deda87ea6d4f3c", size = 225766, upload-time = "2026-08-26T14:56:27.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/34/eb8c658f86dfe562ed49a887a27424bfe9e618c26ea6f865b093d075d3a6/websockets-17.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:77b37cceca17291897c3c73bd30a7c7c7909593554b5da574ec852af83c1742a", size = 223323, upload-time = "2026-08-26T14:56:28.807Z" }, + { url = "https://files.pythonhosted.org/packages/1b/7e/2629609652ece5ca0c7ac235927dd4511b08131e3a5d53439b798fddf002/websockets-17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d8e83333385cac6030a5167fd18bf96cc6c58b914c308e683f05b0cf94bc8dd0", size = 224276, upload-time = "2026-08-26T14:56:29.991Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/8525737fe840b38e5f40956c198fb586a4fac1e07144d41a5b949b989cf8/websockets-17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:073c5c3f7e127041fa9d34a9e29ceefee8c3cafbd267ed2927318f425144380d", size = 224558, upload-time = "2026-08-26T14:56:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/74/ab/3a958c6cbcf74b118f601c20a80ac8bd5e8dfec0bcf7345116feaeefb121/websockets-17.1-cp313-cp313-win32.whl", hash = "sha256:2afb58c7ba48b329d56769f8dfd89f394efe587b65ef806bae810a484d6d3608", size = 217475, upload-time = "2026-08-26T14:56:32.431Z" }, + { url = "https://files.pythonhosted.org/packages/22/36/fb521f0f2994c25509651f169efe5582dddd8713d57a0757ba87859372ef/websockets-17.1-cp313-cp313-win_amd64.whl", hash = "sha256:0340bbef6bfbe16da888b3983d666a4db4954ac3253c38f13bc7aba0c7db5a2f", size = 217784, upload-time = "2026-08-26T14:56:33.608Z" }, + { url = "https://files.pythonhosted.org/packages/68/92/9b8419584681a12a7534b746dfb2737c466efe2455483e2fbf8b941a04ec/websockets-17.1-cp313-cp313-win_arm64.whl", hash = "sha256:7a72efa3bf4fa3a6669a54420a472ad056da3973d827f10e3a536da463f926c2", size = 217715, upload-time = "2026-08-26T14:56:34.865Z" }, + { url = "https://files.pythonhosted.org/packages/90/0d/500cf5daea09d4669dff3a7d67159094a0bd6c4ef130381404f6edd3eb5f/websockets-17.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0c9982938980e086da59f70d05f9418cd143401a601a0faac10fa48f7bb1cd3e", size = 217048, upload-time = "2026-08-26T14:56:36.03Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/5b12c6168aa269cffbfd24d177cd492b130120403a418c7e89462e27b4ac/websockets-17.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:57b39dc8541cf7ed3f639da82bf7451060483967f9e733da1f8173e4095f0642", size = 214737, upload-time = "2026-08-26T14:56:37.43Z" }, + { url = "https://files.pythonhosted.org/packages/0c/36/e453e5106e4e2416f008ac222837c2f1637a063b08008afcd1088889b631/websockets-17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:96abdecbaae746851b87c3a36cb4a661df93ca3d92f114270f79228bf1d00de6", size = 214955, upload-time = "2026-08-26T14:56:38.71Z" }, + { url = "https://files.pythonhosted.org/packages/dd/30/0204bb86176db02cdfc678ce65ed808a66fab87d250ce61a8790800a60b0/websockets-17.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9fc873e239c5abeb150bc24dbd1a7af23a9254526383ce0a077f5e20adbeb19", size = 224331, upload-time = "2026-08-26T14:56:39.924Z" }, + { url = "https://files.pythonhosted.org/packages/46/c8/d8372256e00c4e3cab1115c45075d1eeedb642a3f2b42bd70c4deae03f06/websockets-17.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f42912fa9eb4cb7c7ec9fde9b3332ba339eb8a8811981043d4029599f3d950b", size = 224685, upload-time = "2026-08-26T14:56:41.169Z" }, + { url = "https://files.pythonhosted.org/packages/12/7d/650355b8f67f908ff99603351d4458d1a0b787d627950a47c38db7e25308/websockets-17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f98bf378d7a5be047a044a1a27c987a8f355e10e3b5754617dbe756248cbc5ce", size = 225927, upload-time = "2026-08-26T14:56:42.359Z" }, + { url = "https://files.pythonhosted.org/packages/34/6c/a9ffa5b903579eed76017870f055d75ecc73988d9d0c9b65a92ba0bf2a27/websockets-17.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d334d11398086bb5559606cb42d51c013ea7c061c7db701521392373d3c087f5", size = 227300, upload-time = "2026-08-26T14:56:43.538Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5d/4551c2269066af7481ee44605a0813770961615b5b5da3e87a8f5cb859ea/websockets-17.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c27336b1a0ac56569493e858497870347854372395f50483725f8cdacc5a45c", size = 226533, upload-time = "2026-08-26T14:56:44.669Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/237a99233e5c445759a613831b3a92e91905afc064dc3bd0ad33c35fd1e2/websockets-17.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67258b00302a5aaf0b267771c7014b13429abd7ea17eebc4c55bd935ff101555", size = 225280, upload-time = "2026-08-26T14:56:45.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b5/e9407a91613d1d1cd932414143a1012096b26674a782fc55a0bd23217ee4/websockets-17.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:455ffeea0879d313205df1e745e5883e1feb7f31ecd26be882f5f0babd3db04f", size = 222540, upload-time = "2026-08-26T14:56:47.053Z" }, + { url = "https://files.pythonhosted.org/packages/db/d2/db76628db0577b783205d9779f64d8e373416b04c62d1546be4b75dc8540/websockets-17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f7233eaf441a345a5943a929fd4b5ea3278f11aed35a9ed0f3106b8cb3ca846a", size = 225354, upload-time = "2026-08-26T14:56:48.32Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4c/2174181c067b89a74ae18e2650c2ac29959f4b796afe876ab3f4d30d642c/websockets-17.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c65da239a5ad553619804c1f9d65c1a0b3005381c6158ee14da2c7444cbd0c78", size = 223867, upload-time = "2026-08-26T14:56:49.579Z" }, + { url = "https://files.pythonhosted.org/packages/df/75/274decb9a8253561b5be3261e02a6676fc8ecdf31e95b722e53d5bfb8fd2/websockets-17.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fa1ffa08c81a4f809cdab6129f8e55bee4650b9d6d3461019dda73aacd146b6", size = 224652, upload-time = "2026-08-26T14:56:50.885Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e6/49824f1fb4db7656d2f7492b1d8be16147b759d909490e32f4776843ee64/websockets-17.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:406b8107943a43ef4649b1e0cb0cdc052bbf08fe1c8905a623c4af9586e5cebb", size = 225822, upload-time = "2026-08-26T14:56:52.356Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/5dc43838c0b02a95f42c47a0de33c5ddd7767a9feeb4d0d8777ac1cfefe4/websockets-17.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4e8ffcb486c8490a34a4cef5e4409d8da5a1cb1681e5bf7d786ce5e84aa8540d", size = 223379, upload-time = "2026-08-26T14:56:53.699Z" }, + { url = "https://files.pythonhosted.org/packages/c2/62/585637cf06d6b321232f79c55dc14d65518d12cf87c94c44f5864068810e/websockets-17.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fb88076df585b69c5761c387c0081aa87d7b9eb1b205a6535ca4777e25650d81", size = 224330, upload-time = "2026-08-26T14:56:55.184Z" }, + { url = "https://files.pythonhosted.org/packages/de/68/c3b234a6a1366b6ab5bbfaa4434a1b946e1dc4e8ddd6824bfd93a8835b7f/websockets-17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5d4724255fb8398acd9e583b97eb2279cec20e0bd0f9a94bf75f6056ef9f13da", size = 224622, upload-time = "2026-08-26T14:56:56.393Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d4/84cf3d1376f5d8207f55f43c1c818babd6b89447f5dcd01f18a6d5526796/websockets-17.1-cp314-cp314-win32.whl", hash = "sha256:be3f0129c5654517b2abf07dcb75bb1d9479759a4ccfb569e8293579e9fc029a", size = 217036, upload-time = "2026-08-26T14:56:57.652Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0f/9e7ac63c5d7cb642952200814f584318e65146df008b7d375d5d9c6b2c97/websockets-17.1-cp314-cp314-win_amd64.whl", hash = "sha256:2a4dc6ef83f4559e0d05f313a375cb38f63c986096a9da99fe94fdd779d313e5", size = 217382, upload-time = "2026-08-26T14:56:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/1ae6b91f7f3ac05f5c9f14a72dc2181c115ff370bcd8a7f10f02c174adfd/websockets-17.1-cp314-cp314-win_arm64.whl", hash = "sha256:46c0331c9eaaf73a559f3a9e388466be0df96eb83d40f06f1ca6ab6613b35c82", size = 217268, upload-time = "2026-08-26T14:57:00.654Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f0/f65644d0e0b2b90918a8c41503841cc4072a58f2bf76c09bc36e751fc0dd/websockets-17.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d411ea5ca18ac1b12c0c94be88b60c18ca641ac43bcdfdf1c9f79d46cdbe1603", size = 217379, upload-time = "2026-08-26T14:57:02.181Z" }, + { url = "https://files.pythonhosted.org/packages/ff/35/4c46d1f620ac1a30f92b6eae78ee40a772a93f568647ca7ccdc5ea283cf8/websockets-17.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:07fa3e7c30e2c577928d359b56bf872a3e0cbcc15553eaa0907c1ee86344b56f", size = 214911, upload-time = "2026-08-26T14:57:03.478Z" }, + { url = "https://files.pythonhosted.org/packages/04/6e/4587e8406d7c1188e97b9cf466c081e93399380d447f885bfce81626cd37/websockets-17.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de9acef07e3a78e9567fcd26c29011a4da8f050b13004bbf880a0fd82a6eea5", size = 215115, upload-time = "2026-08-26T14:57:04.692Z" }, + { url = "https://files.pythonhosted.org/packages/ec/06/1381c8fff525041025909eb80ace32489194a00ba22a0a8d428030afcc84/websockets-17.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ea0ed9373b880115911d9d39634bccc95b8ce590c9c42e8589f5cacc3ef3cee2", size = 224696, upload-time = "2026-08-26T14:57:05.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/9d/9034e867dc85340be058619751742b895f722326e83100d110063461ca07/websockets-17.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50903d335bfda026c2fa11dd9aed09d8cbee0c451e3a85122a9acb041b7dc69b", size = 224975, upload-time = "2026-08-26T14:57:07.262Z" }, + { url = "https://files.pythonhosted.org/packages/40/eb/ed03aa3cae748ebf6397e5d44028f433f746bad09dc568ff754fda3a3c9b/websockets-17.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a74531ce81af587f906ab42f194032388fcff8fc7938402e5917c9147a39441", size = 226151, upload-time = "2026-08-26T14:57:08.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c9/cc1964a096d16f3b73cb1ee5f14f277f5a3bcac07c6e8f9a1dcded99f4c8/websockets-17.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8fbf28e639544503b7d1c96452a5e5e043e4108d89b1f3fa02910603622d19db", size = 228292, upload-time = "2026-08-26T14:57:09.846Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/46da6dd0363c2db2e4876fd59a40fd40c1943a82d7018d0a33afbce47d52/websockets-17.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f612dc57f00c07cf4aa2673f7cbceabd654ad2457b7e639f061b794d6e11f9fd", size = 226722, upload-time = "2026-08-26T14:57:11.118Z" }, + { url = "https://files.pythonhosted.org/packages/78/98/ecd8f5e1c5d0e54c08ebc5c66852271112166db68107cb0e17ca1bf25009/websockets-17.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c7ac77401227212dc6e849182feee50d57cf456ec6329ffd6979c94bb136c5c", size = 225451, upload-time = "2026-08-26T14:57:12.601Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/da8d2760db53e17aae763738b6ba834b1fcf16813d3632f3edb6951e1ec8/websockets-17.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32a2a68d989d6e5b74a9d5095415c51189ebae29fceb7cf2b64a1c0318a81256", size = 223003, upload-time = "2026-08-26T14:57:13.875Z" }, + { url = "https://files.pythonhosted.org/packages/a4/40/ea401c141a79c5b1d0021a0dab9d0df2051c108f1620fbb39a6e7c714c3b/websockets-17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:aec00f018d34c67500ff0438dc314b40277be4a1b983cbacbf53ccf7db63e257", size = 225704, upload-time = "2026-08-26T14:57:15.091Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8e/07ab3f44215d89840d5385fdcaaab1fed8caeffa67c6899e15062957c12c/websockets-17.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0014eaff8ad5b3b43feda2279f9d34bf2eaae040720b9fbbb55944b10f40b14d", size = 224192, upload-time = "2026-08-26T14:57:16.3Z" }, + { url = "https://files.pythonhosted.org/packages/58/93/ccf1af0a23e5748d4e22292a377d78d15cf294d7e707bbb11a8990ae6bd5/websockets-17.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:db9d7ee47f3ba531e278be539af39e2c7c7d28fb94897b6cd1120d63b0ef5922", size = 225082, upload-time = "2026-08-26T14:57:17.531Z" }, + { url = "https://files.pythonhosted.org/packages/e2/db/e32200f99ce282e728d2929f2c429db353cf3282db7d0eba99eb32c9fec1/websockets-17.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:ff3e2ba7a9f0a110b0555452e9b5a03a34e11662544e01beea15f144b48ba7b7", size = 226101, upload-time = "2026-08-26T14:57:18.802Z" }, + { url = "https://files.pythonhosted.org/packages/28/3d/e7a6e9777b29433620167c98f3caaff0d6b08b1239a273ef7f7fd1393349/websockets-17.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6da17fc94bd270f5987b10bee113461ac36a36a98b0481ddcc98056e5a90001a", size = 223794, upload-time = "2026-08-26T14:57:20.313Z" }, + { url = "https://files.pythonhosted.org/packages/48/05/ac569090726dedd6656f3ee28b0c02dfb1ba76e898dceaccc2987a237cef/websockets-17.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:e8dc3fa6d6b7ead3f9de57895f41b116a28787548e066365d9d90f7356bcaad2", size = 224567, upload-time = "2026-08-26T14:57:21.634Z" }, + { url = "https://files.pythonhosted.org/packages/14/50/4ef62941111db6b31193f4fabbb65f845a5177579040cb8fe0d774d25034/websockets-17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b65d5fe48219dc2d5e158de9e6514e75600f379cc7e37108d35f31764c155566", size = 224993, upload-time = "2026-08-26T14:57:22.86Z" }, + { url = "https://files.pythonhosted.org/packages/28/42/2b95ada4ea19bf3a2072b68669ce4f4afb212690b727d31640576287fd68/websockets-17.1-cp314-cp314t-win32.whl", hash = "sha256:2cce251f3e2469b99b6802b55435bcdd07123b41870f54c87b336183af9d7e68", size = 217168, upload-time = "2026-08-26T14:57:24.466Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/67d5ee08dd8060a37d612fd40a625b5376ad19ae48fe1c8ad428c278b817/websockets-17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f6c38cdcaf98a911d7acc25577f2f9e710f3a2fc2bde1563556784320196b51", size = 217508, upload-time = "2026-08-26T14:57:25.983Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/822005d0c674451d2411027b878cdc128a2b7ea5a30d337d9e279da22eba/websockets-17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:d1e2f5fa2b6d01f0d85b4f223fea7ed1d504be282a02a81bd2be4817ef7a2f03", size = 217425, upload-time = "2026-08-26T14:57:27.324Z" }, + { url = "https://files.pythonhosted.org/packages/de/d5/99a6c6a1eb5d5ae9f45f59a3c97f4e3b21f310eb404a547fb3e7d2fc054c/websockets-17.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:88381602e379165b66244b2ebc29f9b23ea0851fbe63ae157f91ca324f072d6f", size = 216970, upload-time = "2026-08-26T14:57:28.575Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/1e7f6e833728193958d3ed3d67b5d57c3c7cfa948abf94d4bc553257c954/websockets-17.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:88bc5138e53903a85c354e59df7ba73ce306f7b09724cef74dba121e60a88ce2", size = 214699, upload-time = "2026-08-26T14:57:29.862Z" }, + { url = "https://files.pythonhosted.org/packages/07/00/95d39549f86e34425a0412bcbe61708dd1fc46af654e2134a6c4389102ad/websockets-17.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3546ef55b3a074494106508bc6505c73825970d2d9505f7bf53882b3e88b0d1e", size = 214927, upload-time = "2026-08-26T14:57:31.148Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ff/b442415fc4f7f9943b0fc8e8eebaa13923ca73361e167c439ba634eecbd9/websockets-17.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9ae55d24241fc055f22aea3ac924559069848bd0ad4ea065fdd72d2194685fe8", size = 224373, upload-time = "2026-08-26T14:57:32.833Z" }, + { url = "https://files.pythonhosted.org/packages/a8/dd/b83537aae4cf61615b9d8b2dbb235c0030ba85457a6d934798273814600f/websockets-17.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7b349265fad6244013eecd99df8d83c12bf3013943e431f4fadd5bffc37db42", size = 224801, upload-time = "2026-08-26T14:57:34.041Z" }, + { url = "https://files.pythonhosted.org/packages/76/83/5ab0abed58454909e8dbab45086ac68ee4556d7a8ada26735addc909b903/websockets-17.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc5789e5ea182b77a38881383ada5347202a6c66f4857d054e075290e80b604b", size = 225967, upload-time = "2026-08-26T14:57:35.292Z" }, + { url = "https://files.pythonhosted.org/packages/4b/26/e2412f2b998a8c1dfc00c0709ff6ee0c634dd0b0b4f92bdfe9667876b71c/websockets-17.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce13c7d233239e739600a57d4a73c1192ad8259e655a4d55aa1a454242bc809d", size = 227664, upload-time = "2026-08-26T14:57:36.493Z" }, + { url = "https://files.pythonhosted.org/packages/ec/25/0dd4495df3c0e02f6db705312ba85ab9b2dd42257dc23eb0da10066e4844/websockets-17.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1036189bd34b0bc1b10a4679321e2c7968af317efe6e8e4c1c5141c4254fb5bb", size = 226447, upload-time = "2026-08-26T14:57:37.781Z" }, + { url = "https://files.pythonhosted.org/packages/be/67/6df3f63ffc48f08126ed0cd2fd2a41092967c3e364f8ec100deae90b6d77/websockets-17.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e78fd4b7b2c5086a38671c9c882c1e643385eccea360b5b1fda4a105e590087e", size = 225343, upload-time = "2026-08-26T14:57:39.133Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8d/a8479bbb09ff054907d141123d8f52fb6ae5ac39c6dbe39e6a02a8408309/websockets-17.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:46e7a10bf04318c7b0c0273791925ae5e1cbe4a11e34aa934d2ef27862058a80", size = 222748, upload-time = "2026-08-26T14:57:40.478Z" }, + { url = "https://files.pythonhosted.org/packages/40/fb/4c3d2a3269cde3f3087916de9c3d9fc5d7196b46846d8c3a9ae59ad0a884/websockets-17.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:33e45c7ea38428e740a7f233555d71df0b875cef7fc080acebc9654475e35335", size = 225453, upload-time = "2026-08-26T14:57:41.859Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1c/6467b401d19408f34e1c7389c222c2c7e1dfdf08c551190269b5eabc726c/websockets-17.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6e63c01803be425ff062b7f7fc201a74def1d49fc94a2410dd17375df75936e9", size = 224112, upload-time = "2026-08-26T14:57:43.136Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5f/744e032ac80e11039a7447657ebabb46e9b5c2dbcec83be571335212932f/websockets-17.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:722ec21717eec6477bce582147a28acdfe034e604239466a6a95daedb863e774", size = 224646, upload-time = "2026-08-26T14:57:44.871Z" }, + { url = "https://files.pythonhosted.org/packages/9f/47/bcb9128d9afc4d0934d9192e2a24897ca2f7a63df2654904915349c6c46d/websockets-17.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:e74e41f0ad12ff1e8983e349daef79d37cc8280c743ce9d134d6c74c18dab5d6", size = 225797, upload-time = "2026-08-26T14:57:46.338Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e0/b058047b7cf565e1105b10ef6b6b24a6ebe3575678c7dc75a645334705a7/websockets-17.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:12fe8984a32dbfd084e0603f1a8d740c0180cb85b3174585c54a80d2455a8394", size = 223605, upload-time = "2026-08-26T14:57:48.175Z" }, + { url = "https://files.pythonhosted.org/packages/b9/69/fc1555bff884de363f1bf9eebf2836dbeb29fa7e4f957debb7bbcf43abba/websockets-17.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:01dcb47deebc40b38fd4a493b9b9f4d0a704b7bec6f35e4d34085b329abce71a", size = 224508, upload-time = "2026-08-26T14:57:49.407Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/648d4e68621688b19093b06f7b497d520952e68cdea1c1b54371fe9491de/websockets-17.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f4c45ee2512d3757b5e6c67c5a34e435143f2ecb7df3324f9fd888688c45c0f4", size = 224767, upload-time = "2026-08-26T14:57:50.799Z" }, + { url = "https://files.pythonhosted.org/packages/58/93/f8342b55864f71df13eb8e9ef7dce691b87a87f04f75bb8a1385b3336e7c/websockets-17.1-cp315-cp315-win32.whl", hash = "sha256:0f4f50dfe2cc810fc4e2de979b35e83bf8bb4bccdc6fe472d93762ea7b1d5927", size = 217003, upload-time = "2026-08-26T14:57:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f0/7b5fdb774c245e0b6217009e2a24d2105c1a64923949f33be41aa7959302/websockets-17.1-cp315-cp315-win_amd64.whl", hash = "sha256:4af784f3e436f65b355c117c6497320f2b5cf6a559295cb1c4c7338e335d45cc", size = 217300, upload-time = "2026-08-26T14:57:53.492Z" }, + { url = "https://files.pythonhosted.org/packages/76/33/1fe6ed1b5087516115ca451b2c240314b010647071f8fc3bd78a21e4dddb/websockets-17.1-cp315-cp315-win_arm64.whl", hash = "sha256:d58159af7835fde09c462394293c0d7aaf8fb4557d8f8e5699f5e722ccae013d", size = 217214, upload-time = "2026-08-26T14:57:54.88Z" }, + { url = "https://files.pythonhosted.org/packages/94/ca/ed02e75996a266d76c5fcb5dd9b930db4cf2b388ca5fa3d2a72086f81568/websockets-17.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1a5cf4e7bbe3ca499e6a289206cb4fcb7444b09919e129bd517f57d5fa192c13", size = 217282, upload-time = "2026-08-26T14:57:56.108Z" }, + { url = "https://files.pythonhosted.org/packages/bd/7d/d536f5bc89ea5b52fd1c1727c59fabafee6bc41f5ce92c3bd2f83047908c/websockets-17.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:416b4bc8789a1865a3ff643ec4ee073a5f52402d0dbeafd27b1798d5dd6b6a51", size = 214863, upload-time = "2026-08-26T14:57:57.355Z" }, + { url = "https://files.pythonhosted.org/packages/37/37/944cf17bad668e9be1247e6314f88a48b9faf7c250e383410db8b38af0b9/websockets-17.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:259f45358c76d3b18489e3e80636cdbe807e05ecf1b10fdf1a779106d23d0c8e", size = 215073, upload-time = "2026-08-26T14:57:58.719Z" }, + { url = "https://files.pythonhosted.org/packages/74/bf/3267966cc1bbc2b8fa62fd329651b0af502df1f5d1c0eed027ff339d6aa8/websockets-17.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9d01e8ede41fea4f5a847dad9d628355f74905f437a5b6856d67aa66d193800", size = 225229, upload-time = "2026-08-26T14:58:00.235Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d8/85ea722f483510abb39fc71aafb4465d17cf9051a275ab036874ff3c300c/websockets-17.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7b35181a14cbfcae163b4de545d22abfd07d06c2c41ca69cfcd99251d6888ab", size = 225500, upload-time = "2026-08-26T14:58:01.994Z" }, + { url = "https://files.pythonhosted.org/packages/50/ce/64c7d00005bd0d15ecb5c5fcb7fb2597b6b92ddd16c4fa6bbc3d2835ad63/websockets-17.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a8e768a048c2220697477ce2e67e4345dc9f693d0ee6af53945b5e30227c6a7", size = 226829, upload-time = "2026-08-26T14:58:03.327Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/096c67940fb957e667ca3c542818150434eb0388c6fdc90b3a502f3c3e96/websockets-17.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:880069d21cc33a558dcf180924a546d1ecf8ada5be3e4e70acee87019d706a24", size = 228457, upload-time = "2026-08-26T14:58:04.78Z" }, + { url = "https://files.pythonhosted.org/packages/51/fe/f2331b6b7ccc67589891da354fa46a5cb79e95f83b9fd0e734d77f1f2140/websockets-17.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cec1bb8f22abccc8d20f8ca63df9be41600c26c190f4b97ee86c675fd4a863a6", size = 227265, upload-time = "2026-08-26T14:58:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/47/a5/fb1642302f8ec77ca922203074f155a9831a5128ad75e725059a476d1227/websockets-17.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f3a1d577e081667dda7f8e5b4796e6e32f9713c93e2a3d930669519840a3c623", size = 226143, upload-time = "2026-08-26T14:58:07.464Z" }, + { url = "https://files.pythonhosted.org/packages/d7/41/7133fcfb63f5562750b269d6a845c689dde6a2c6407286da395beea19ddd/websockets-17.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc053f9e95a76213c5eb7ed95779f7daf0d2bf0e4e03073629ebfa43a033f151", size = 223501, upload-time = "2026-08-26T14:58:08.766Z" }, + { url = "https://files.pythonhosted.org/packages/64/b1/82b36bfabc79ff2d383a1fc043cee6a13f794ef4f6bf1b4810ad6988cf6f/websockets-17.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:bb0efe019480a1c93e168ce96479273aaebd672fc8c350d5eed1e507ababb1b8", size = 226330, upload-time = "2026-08-26T14:58:09.987Z" }, + { url = "https://files.pythonhosted.org/packages/41/7d/5b511b9bf6e9ad331e6ff902fcbcc71c3794d10ef3b5efe80ccb8f0a7861/websockets-17.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:615746b12b26a3fd4077bc6fbeb277a1c192a45dd57b531d07ad9ed5c52a9a7a", size = 224980, upload-time = "2026-08-26T14:58:11.303Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/aed08f25301f8eef23be903ff9319fcf35630ca2bdec9d226f7d804dd5b3/websockets-17.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:1a20136d61f9ca3a31493732762661fafc2c20e8861930214e21afc6a8a692a2", size = 225478, upload-time = "2026-08-26T14:58:12.543Z" }, + { url = "https://files.pythonhosted.org/packages/3e/47/0d63d4168536b4682c9d19b7399443b1176f25dbb68878374fa716670230/websockets-17.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:2786cbd273ab69c22612db8a41229ddf2c158060b17b5928884bf388d07887f3", size = 226588, upload-time = "2026-08-26T14:58:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/844bd0b6386fc81ed6a55f4b6dd26f01c6987eda205afa10175ea12b2164/websockets-17.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b1c323fc3be1dc3f87f6c59458cb7d9e13dcbbf971d6c3f3e2bbaf58d3bfcdfe", size = 224336, upload-time = "2026-08-26T14:58:15.778Z" }, + { url = "https://files.pythonhosted.org/packages/96/18/03709c84bc88ec4dcea68d4be4ccd07d611073dec111203a5bf45af8809d/websockets-17.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12c8e2b25df59755954a04dfa09c990b96691025aaf7eafd19ed6da24b09c18d", size = 225197, upload-time = "2026-08-26T14:58:17.141Z" }, + { url = "https://files.pythonhosted.org/packages/27/cf/0d1c694b6466c89e875b85b32b51312c472cf6708eee91914866f5087dde/websockets-17.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f58f58b4b29bbea2a3635e2c56eff4a3adab011fe383802a9e542e31b97085fc", size = 225493, upload-time = "2026-08-26T14:58:18.521Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f5/99857c3dd9676749f33e3668665a34ad6099505fb8d75eb084f49f7807a9/websockets-17.1-cp315-cp315t-win32.whl", hash = "sha256:f78a3ffb1994304db2c0c4588e4d1a518079b557054fa3bb985a6f5e50ff49a3", size = 217130, upload-time = "2026-08-26T14:58:20.037Z" }, + { url = "https://files.pythonhosted.org/packages/2c/84/77599922ab441bfe61508f97dab2c71f8e114d31793993ea54011db16199/websockets-17.1-cp315-cp315t-win_amd64.whl", hash = "sha256:ad68c28a27246fed109a4409393d677b7e1388345cbbd2f5aee5c182d8506110", size = 217448, upload-time = "2026-08-26T14:58:21.382Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3c/8b9a225b523f06a9389be81f1b0ab07c49bec6014742e6aa359c1f920f1f/websockets-17.1-cp315-cp315t-win_arm64.whl", hash = "sha256:e552e0037230ac16e5f568de7012041344d1b18c9feed30ec2891b8eba55af81", size = 217372, upload-time = "2026-08-26T14:58:22.807Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e4/af4abbcf07eac6a725ec6f865611526b2b0c23d482723de551bec667880d/websockets-17.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10ecb38ffc05e1841b619d99c725307a223ef9ad58e7b1ed33311d472dc43918", size = 214602, upload-time = "2026-08-26T14:58:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/4d/fe/819fba7ba35f92b639333da7355041c07dd50048f9c76fba0b8e292a6483/websockets-17.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17aa424ab61620aad21b36b2240efc87b500cc496e7d0e999a5c2ae99395e886", size = 214874, upload-time = "2026-08-26T14:58:26.689Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a7/d370ab794f47fbeea648d17ad08caf0bb50131d6c04b7ad83e6af63c405a/websockets-17.1-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:764cf7bfa149365f32b7a0fd9fed32debdac29dd06295d5635cde1745b446cd8", size = 215821, upload-time = "2026-08-26T17:25:23.616Z" }, + { url = "https://files.pythonhosted.org/packages/9b/6b/251b00fe634e2a9c2cb5d6390e0e97cec55e3d18dd09b4b976620eed5d7b/websockets-17.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d1b108bd8f5f6a8b90801f6db3b3858d5deca889acfdb8ac497bbb24e4b0edf", size = 215714, upload-time = "2026-08-26T17:25:26.295Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b1/37fe0c96c206b4208a072c3a74add6a72af4b8228be3f5435163c5a6d099/websockets-17.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a62d8c424383c9dc769ff3672018df822603117e32686e567d452ed035b6fb2e", size = 216608, upload-time = "2026-08-26T17:25:28.134Z" }, + { url = "https://files.pythonhosted.org/packages/be/7e/75a0a491b512412e08333b9f8412757af6186fe1c598186261002de1a793/websockets-17.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8196d217eeca52b9235ee1f8a684a09885a5f953d5a31e80ef915bf2c5c94f9d", size = 217870, upload-time = "2026-08-26T17:25:29.745Z" }, + { url = "https://files.pythonhosted.org/packages/41/63/23572870e01836a98346075b9e17a8bc24a6ddd9800a3204ceee58677f3c/websockets-17.1-py3-none-any.whl", hash = "sha256:f221081107b8c48184d99f7019604486376e7ef826037e70aad6b02540732c23", size = 211134, upload-time = "2026-08-26T17:25:31.397Z" }, +] + [[package]] name = "widgetsnbextension" version = "4.0.15" @@ -2174,18 +2083,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, ] -[[package]] -name = "wsproto" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, -] - [[package]] name = "xy" source = { editable = "." } @@ -2215,13 +2112,14 @@ dev = [ { name = "starlette" }, { name = "ty" }, { name = "uvicorn" }, + { name = "websockets" }, ] [package.metadata] requires-dist = [ { name = "anywidget", specifier = ">=0.9" }, { name = "numpy", specifier = ">=1.24" }, - { name = "reflex", marker = "extra == 'reflex'", specifier = ">=0.9.6" }, + { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=make-sio-optional" }, ] provides-extras = ["reflex"] @@ -2238,6 +2136,7 @@ dev = [ { name = "starlette", specifier = ">=0.36" }, { name = "ty" }, { name = "uvicorn", specifier = ">=0.23" }, + { name = "websockets", specifier = ">=12" }, ] [[package]] From 7c7de9a32a1de9fdeb909a0542d7800c5bf38397 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 12 Sep 2026 22:36:05 +0200 Subject: [PATCH 02/10] Address PR review: per-app channel attachment, pinned Reflex commit, runnable browser test --- .github/workflows/ci.yml | 29 +++- .gitignore | 4 + docs/app/pyproject.toml | 15 +- docs/app/uv.lock | 48 +++--- news/505.bugfix.md | 9 ++ pyproject.toml | 30 +++- python/reflex_xy/app.py | 99 +++++++++--- python/reflex_xy/assets/XYChart.jsx | 40 +++-- python/reflex_xy/data_plane.py | 45 ++++-- python/reflex_xy/state_bridge.py | 33 +++- scripts/reflex_ws_smoke.py | 23 ++- scripts/verify_ci_workflow.py | 12 +- spec/design/reflex-integration.md | 44 ++++-- spec/design/view-state.md | 6 +- spec/design/wire-protocol.md | 6 +- tests/reflex_adapter/test_assets.py | 26 +++- tests/reflex_adapter/test_channel_browser.py | 102 ++++++++----- tests/reflex_adapter/test_data_plane.py | 26 +++- tests/reflex_adapter/test_setup_attachment.py | 135 +++++++++++++++++ tests/test_dependencies.py | 44 +++++- uv.lock | 142 ++++++++++++++++-- 21 files changed, 730 insertions(+), 188 deletions(-) create mode 100644 news/505.bugfix.md create mode 100644 tests/reflex_adapter/test_setup_attachment.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ff632478..1653cef40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,6 +139,15 @@ jobs: run: | uv sync --locked --extra reflex --group dev + # The Chromium installed above belongs to the *npm* playwright, which xy's + # own probes only use for an executable path. The Reflex channel browser + # test drives the Python bindings, and those pin their own browser + # revision — so install through them too, or the one test that proves the + # chart shares the app's websocket fails to launch. + - name: Install Chromium (Playwright, Python bindings) + run: | + .venv/bin/playwright install --with-deps chromium chromium-headless-shell + - name: Verify bundled Reflex integration import run: | .venv/bin/python -c "import importlib.metadata as m, reflex_xy; assert reflex_xy.__version__ == m.version('xy')" @@ -191,8 +200,19 @@ jobs: # defect: fail rather than skip, so the browser layer cannot pass by # absence. (A crash or non-zero exit already fails unconditionally.) export XY_REQUIRE_BROWSER=1 + # Keep the painted-chart screenshot from the Reflex channel browser + # test; it is uploaded below as the visual evidence for that run. + export XY_BROWSER_EVIDENCE="$PWD/browser-evidence" .venv/bin/pytest -q + - name: Upload browser evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: browser-evidence + if-no-files-found: warn + path: browser-evidence/ + # End-to-end: a real Figure (line decimation + scatter) through to_html's # exact payload path, rendered in Chromium, pixels read back. Complements # the stdlib smoke, which hand-builds payloads. (Was dead code — audit @@ -645,7 +665,12 @@ jobs: XY_REQUIRE_CARGO: "1" run: | uv venv smoke-native - uv pip install --no-cache -p smoke-native dist/*.tar.gz numpy anywidget "reflex>=0.9.6" + # The adapter needs Reflex's channel transport, unreleased today, so + # there is no version floor to name here — and these smokes only + # `import reflex_xy` (which pulls no Reflex), so installing the + # framework at all would be theatre. Add `[reflex]` back once + # channels ship and the extra names a real floor again. + uv pip install --no-cache -p smoke-native dist/*.tar.gz numpy anywidget ./smoke-native/bin/python - <<'PY' import importlib.metadata as metadata @@ -666,7 +691,7 @@ jobs: XY_SKIP_CARGO: "1" run: | uv venv smoke-no-rust - uv pip install --no-cache -p smoke-no-rust dist/*.tar.gz numpy anywidget "reflex>=0.9.6" + uv pip install --no-cache -p smoke-no-rust dist/*.tar.gz numpy anywidget ./smoke-no-rust/bin/python - <<'PY' import importlib.metadata as metadata diff --git a/.gitignore b/.gitignore index b4238b74f..7e44cc842 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ node_modules/ python/xy/static/index.js python/xy/static/standalone.js **/assets/external/ + +# Browser evidence kept by the Reflex channel test when XY_BROWSER_EVIDENCE is +# set (CI uploads it as an artifact); local runs default to pytest's tmp_path. +/browser-evidence/ diff --git a/docs/app/pyproject.toml b/docs/app/pyproject.toml index adb1f7a8f..dba6e95ef 100644 --- a/docs/app/pyproject.toml +++ b/docs/app/pyproject.toml @@ -5,16 +5,17 @@ description = "Documentation site for XY." requires-python = ">=3.11" # TEMPORARY: these four normally track reflex-dev/reflex@main, but `xy[reflex]` # requires the unreleased channel transport (reflex-dev/reflex#6932) and uv -# refuses two URLs for one package. They therefore point at the same branch the -# root pyproject does — all from one repo, so the framework is never half -# branch and half main. Restore `@main` when channels land there. +# refuses two URLs for one package. They therefore pin the same immutable +# commit the root pyproject does — all from one repo, so the framework is never +# half fork and half main, and no install can pick up unreviewed framework +# code. Restore `@main` when channels land there. dependencies = [ "numpy", "pillow", - "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@make-sio-optional", - "reflex-components-internal @ git+https://github.com/benedikt-bartscher/reflex.git@make-sio-optional#subdirectory=packages/reflex-components-internal", - "reflex-docgen @ git+https://github.com/benedikt-bartscher/reflex.git@make-sio-optional#subdirectory=packages/reflex-docgen", - "reflex-site-shared @ git+https://github.com/benedikt-bartscher/reflex.git@make-sio-optional#subdirectory=packages/reflex-site-shared", + "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@02dae5601eed0b678703a049db21634f83b625e7", + "reflex-components-internal @ git+https://github.com/benedikt-bartscher/reflex.git@02dae5601eed0b678703a049db21634f83b625e7#subdirectory=packages/reflex-components-internal", + "reflex-docgen @ git+https://github.com/benedikt-bartscher/reflex.git@02dae5601eed0b678703a049db21634f83b625e7#subdirectory=packages/reflex-docgen", + "reflex-site-shared @ git+https://github.com/benedikt-bartscher/reflex.git@02dae5601eed0b678703a049db21634f83b625e7#subdirectory=packages/reflex-site-shared", "xy[reflex]", ] diff --git a/docs/app/uv.lock b/docs/app/uv.lock index 16bf81cf8..0613a7b5d 100644 --- a/docs/app/uv.lock +++ b/docs/app/uv.lock @@ -1064,7 +1064,7 @@ wheels = [ [[package]] name = "reflex" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, @@ -1096,7 +1096,7 @@ dependencies = [ [[package]] name = "reflex-base" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "packaging" }, { name = "platformdirs" }, @@ -1107,7 +1107,7 @@ dependencies = [ [[package]] name = "reflex-components-code" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1118,7 +1118,7 @@ dependencies = [ [[package]] name = "reflex-components-core" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "python-multipart" }, { name = "reflex-base" }, @@ -1131,7 +1131,7 @@ dependencies = [ [[package]] name = "reflex-components-dataeditor" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1140,7 +1140,7 @@ dependencies = [ [[package]] name = "reflex-components-gridjs" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] @@ -1148,7 +1148,7 @@ dependencies = [ [[package]] name = "reflex-components-internal" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex" }, ] @@ -1156,7 +1156,7 @@ dependencies = [ [[package]] name = "reflex-components-lucide" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] @@ -1164,7 +1164,7 @@ dependencies = [ [[package]] name = "reflex-components-markdown" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-code" }, @@ -1175,7 +1175,7 @@ dependencies = [ [[package]] name = "reflex-components-moment" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] @@ -1183,7 +1183,7 @@ dependencies = [ [[package]] name = "reflex-components-plotly" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1192,7 +1192,7 @@ dependencies = [ [[package]] name = "reflex-components-radix" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1202,7 +1202,7 @@ dependencies = [ [[package]] name = "reflex-components-react-player" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1211,7 +1211,7 @@ dependencies = [ [[package]] name = "reflex-components-recharts" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] @@ -1219,7 +1219,7 @@ dependencies = [ [[package]] name = "reflex-components-sonner" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-lucide" }, @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "reflex-docgen" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "griffelib" }, { name = "mistletoe" }, @@ -1240,7 +1240,7 @@ dependencies = [ [[package]] name = "reflex-hosting-cli" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "click" }, { name = "httpx" }, @@ -1252,12 +1252,12 @@ dependencies = [ [[package]] name = "reflex-integrations-docs" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Fintegrations-docs&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Fintegrations-docs&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } [[package]] name = "reflex-site-shared" version = "0.0.3.post248.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "email-validator" }, { name = "httpx" }, @@ -1632,7 +1632,7 @@ reflex = [ requires-dist = [ { name = "anywidget", specifier = ">=0.9" }, { name = "numpy", specifier = ">=1.24" }, - { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=make-sio-optional" }, + { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=02dae5601eed0b678703a049db21634f83b625e7" }, ] provides-extras = ["reflex"] @@ -1680,10 +1680,10 @@ dev = [ requires-dist = [ { name = "numpy" }, { name = "pillow" }, - { name = "reflex", git = "https://github.com/benedikt-bartscher/reflex.git?rev=make-sio-optional" }, - { name = "reflex-components-internal", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=make-sio-optional" }, - { name = "reflex-docgen", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=make-sio-optional" }, - { name = "reflex-site-shared", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=make-sio-optional" }, + { name = "reflex", git = "https://github.com/benedikt-bartscher/reflex.git?rev=02dae5601eed0b678703a049db21634f83b625e7" }, + { name = "reflex-components-internal", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=02dae5601eed0b678703a049db21634f83b625e7" }, + { name = "reflex-docgen", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=02dae5601eed0b678703a049db21634f83b625e7" }, + { name = "reflex-site-shared", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=02dae5601eed0b678703a049db21634f83b625e7" }, { name = "xy", extras = ["reflex"], editable = "../../" }, ] diff --git a/news/505.bugfix.md b/news/505.bugfix.md new file mode 100644 index 000000000..3094f0a62 --- /dev/null +++ b/news/505.bugfix.md @@ -0,0 +1,9 @@ +`reflex_xy.setup(app)` is now idempotent per *app* rather than per process. +With more than one `rx.App` alive in a process — a hot reload, an AppHarness +test, two apps built side by side — setting up a second app used to leave the +first one's subscribers stranded (registry fan-out pointed only at the newest +channel), and setting the first one up again raised +`RuntimeError: A channel named '/_xy' is already registered`. Attachment is now +keyed on the app, and publishes, pushes and errors fan out to every attached +app. The app is held weakly throughout, so a superseded one is collected rather +than kept alive by the data plane. diff --git a/pyproject.toml b/pyproject.toml index 14dae9655..bfd05247e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,15 +25,21 @@ dependencies = [ # more, so a version specifier here would be a lie in either direction: # `>=0.9.6` admits versions where every chart stays blank, and a guessed # `>=0.10` names a release that does not exist. The git requirement is the -# honest statement of what this integration needs, it keeps `uv sync` working -# for contributors, and uv resolves the rest of that repo's workspace -# (reflex-base, the component packages) from the same branch, so the framework -# is never half branch and half release. `docs/app/pyproject.toml` points its -# own Reflex requirements at the same branch for the same reason. Replace all -# of it with the real floor the moment channels are released (see the Status -# note in the design doc). +# honest statement of what this integration needs, and it keeps `uv sync` +# working for contributors. +# +# It names an immutable *commit*, not the branch: the lockfile only protects +# `uv sync --locked`, so a branch ref here would let a bare +# `pip install xy[reflex]` — or any `uv lock` regeneration — resolve framework +# code nobody reviewed. uv resolves the rest of that repo's workspace +# (reflex-base, the component packages) from the same commit, so the framework +# is never half branch and half release, and `docs/app/pyproject.toml` pins the +# same commit for the same reason. Bumping means editing all six requirement +# strings plus both lockfiles — that friction is the point. Replace the lot +# with the real floor the moment channels are released (see the design doc's +# Status note). reflex = [ - "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@make-sio-optional", + "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@02dae5601eed0b678703a049db21634f83b625e7", ] [project.urls] @@ -68,6 +74,14 @@ dev = [ "aiohttp>=3.14.3", "uvicorn>=0.23", "websockets>=12", + # Drives a real browser at a real Reflex app in + # tests/reflex_adapter/test_channel_browser.py: the only place the + # single-shared-websocket claim is checked end to end. xy's other browser + # probes speak CDP directly and only borrow playwright's *Node* package for + # a Chromium path, which is why the Python one has to be named here — left + # out, the test silently skips and nothing verifies the transport. + # Pinned with docs/app so one `playwright install` serves both. + "playwright==1.61.0", # Parses .binder/environment.yml in tests/test_binder_config.py so the # Binder contract is asserted on structure, not string transcription. "pyyaml>=6", diff --git a/python/reflex_xy/app.py b/python/reflex_xy/app.py index 16bbcb5c0..edfd6b0ea 100644 --- a/python/reflex_xy/app.py +++ b/python/reflex_xy/app.py @@ -24,7 +24,7 @@ import contextlib import inspect import warnings -from collections.abc import Coroutine +from collections.abc import Callable, Coroutine, Sequence from typing import Any, Optional, cast from reflex.plugins import Plugin @@ -32,7 +32,7 @@ from .data_plane import XYChannel from .handles import FigureHandle, token_of from .registry import _figure_of, registry -from .state_bridge import make_rebuild_hook +from .state_bridge import app_ref, make_rebuild_hook from .tokens import BUILDER_ATTR, PROBE_ATTR from .vars import AsyncFigureVar, FigureVar @@ -48,27 +48,46 @@ "setup", ] -_channel: Optional[XYChannel] = None -# The app the current channel is attached to. Idempotency is per app, not -# per process: a hot reload (and AppHarness) re-imports the app module, and -# the new App gets its own websocket that must be attached to. -_setup_app: Any = None +# Attached (app accessor, channel) pairs. Attachment is per *app*, not per +# process: a hot reload (and AppHarness) re-imports the app module, and the new +# App gets its own websocket that must be attached to. An App is unhashable, so +# this is an identity list rather than a dict, and `app_ref` keeps the app +# weakly so a replaced one does not outlive its reload. +_attached: "list[tuple[Callable[[], Any], XYChannel]]" = [] + + +def _live_planes() -> list[XYChannel]: + """Attached channels whose app is still alive (forgetting the rest).""" + live: list[XYChannel] = [] + for pair in list(_attached): + resolve_app, channel = pair + if resolve_app() is None: + _attached.remove(pair) + else: + live.append(channel) + return live def setup(app: Any) -> XYChannel: - """Attach the xy data plane to a Reflex app (idempotent). + """Attach the xy data plane to a Reflex app (idempotent *per app*). The plane is a Reflex channel on the app's own event websocket. Reflex's `register_channel` refuses an app that cannot carry one — state disabled, or a non-WebSocket transport — so a misconfiguration fails here at startup rather than as a blank chart in the browser. + + Idempotency is keyed on the app itself, which matters once more than one + App exists in a process: keying on "the most recent app" instead would hand + an already-attached app a second channel, and Reflex rejects a duplicate + channel name outright. """ - global _channel, _setup_app - if _channel is not None and _setup_app is app: - return _channel + for resolve_app, channel in _attached: + if resolve_app() is app: + return channel channel = XYChannel(registry, rebuild=make_rebuild_hook(app)) app.register_channel(channel) - wire(channel) + _attached.append((app_ref(app), channel)) + wire_attached() def _lifespan() -> Coroutine[Any, Any, None]: # Deliberately a *sync* function returning the sweep coroutine, not an @@ -83,8 +102,6 @@ def _lifespan() -> Coroutine[Any, Any, None]: return _xy_lifespan() app.register_lifespan_task(_lifespan) - _channel = channel - _setup_app = app return channel @@ -134,12 +151,58 @@ def _ensure_page_plans(app: Any) -> None: def wire(channel: XYChannel) -> None: - """Point the registry's fan-out seams at the data plane (setup and tests).""" + """Point the registry's fan-out seams at one data plane (tests).""" registry.on_publish(channel.broadcast_payload) registry.on_push(channel.broadcast_message) registry.on_error(channel.broadcast_error) +def wire_attached() -> None: + """Point the registry's fan-out seams at *every* attached data plane. + + The registry is process-global; apps are not. A hot reload, an AppHarness + test, or simply two Apps built in one process each get their own channel, + and pointing the seams at the newest one alone would silently strand the + subscribers of every other live app — their figures would keep updating + server-side and never push. Fanning out costs one extra payload build per + additional *live* app, which is exactly the case that would otherwise be + broken; with the single app of a normal deployment it is the same work as + `wire`. + """ + registry.on_publish(_fan_out_payload) + registry.on_push(_fan_out_message) + registry.on_error(_fan_out_error) + + +async def _fan_out(calls: "Sequence[Coroutine[Any, Any, None]]") -> None: + """Await every fan-out, then re-raise the first failure. + + One app's transport failing must not swallow the others' deliveries — that + is the whole point of fanning out — but the error still has to surface the + way a single-plane send does, so it is raised after the rest have run. + """ + results = await asyncio.gather(*calls, return_exceptions=True) + for result in results: + if isinstance(result, BaseException): + raise result + + +async def _fan_out_payload(token: str, entry: Any) -> None: + await _fan_out([plane.broadcast_payload(token, entry) for plane in _live_planes()]) + + +async def _fan_out_message( + token: str, message: dict, buffers: Any = None, version: Optional[int] = None +) -> None: + await _fan_out( + [plane.broadcast_message(token, message, buffers, version) for plane in _live_planes()] + ) + + +async def _fan_out_error(token: str, error: str, resync: bool = False) -> None: + await _fan_out([plane.broadcast_error(token, error, resync) for plane in _live_planes()]) + + async def _xy_lifespan() -> None: """Capture the serving loop, then sweep idle figures forever.""" registry.attach_loop(asyncio.get_running_loop()) @@ -335,7 +398,5 @@ def clear_selection(token: "str | FigureHandle") -> None: def reset_setup_for_tests() -> None: - """Forget the wired data plane (test isolation only).""" - global _channel, _setup_app - _channel = None - _setup_app = None + """Forget every attached data plane (test isolation only).""" + _attached.clear() diff --git a/python/reflex_xy/assets/XYChart.jsx b/python/reflex_xy/assets/XYChart.jsx index 7aebe4bad..05bc79937 100644 --- a/python/reflex_xy/assets/XYChart.jsx +++ b/python/reflex_xy/assets/XYChart.jsx @@ -61,8 +61,20 @@ let sharedPlane = null; // room membership is per-connection, not per-mount). const subCounts = new Map(); -// The data plane's channel name, matching XY_PLANE in data_plane.py. +// The wire vocabulary. Every one of these mirrors a constant of the same name +// in data_plane.py, and tests/reflex_adapter/test_assets.py asserts the two +// declarations agree — so a rename on either side fails there rather than +// silently producing a client that talks past the server. const XY_PLANE = "/_xy"; +const EVENT_SUB = "sub"; +const EVENT_UNSUB = "unsub"; +const EVENT_MSG = "msg"; +const EVENT_PAYLOAD = "payload"; +const EVENT_ERR = "err"; +// Connection lifecycle, owned by Reflex's channel handle rather than by the +// data plane protocol, so these have no server-side counterpart. +const EVENT_CONNECT = "connect"; +const EVENT_DISCONNECT = "disconnect"; /** * The shared xy data plane: one Reflex channel on the app's own websocket. @@ -473,7 +485,7 @@ export function XYChart(props) { // A reconnect can land on a fresh worker whose rebuilt figure starts at // version 1. Versions are monotonic only within this subscription epoch. resetEpoch(); - plane.emit("sub", { fig: liveToken, px: el.clientWidth || null, mid }); + plane.emit(EVENT_SUB, { fig: liveToken, px: el.clientWidth || null, mid }); }; const emitMessage = (m) => { @@ -482,7 +494,7 @@ export function XYChart(props) { if (awaitingPayload || !plane.connected) return; const envelope = { fig: liveToken, mid, m }; if (payloadVersion !== null) envelope.v = payloadVersion; - plane.emit("msg", envelope); + plane.emit(EVENT_MSG, envelope); }; const withSelectionSeq = (m) => ({ @@ -933,10 +945,10 @@ export function XYChart(props) { resetEpoch(); }; - plane.on("payload", onPayload); - plane.on("msg", onMsg); - plane.on("err", onErr); - plane.on("disconnect", onDisconnect); + plane.on(EVENT_PAYLOAD, onPayload); + plane.on(EVENT_MSG, onMsg); + plane.on(EVENT_ERR, onErr); + plane.on(EVENT_DISCONNECT, onDisconnect); // Resubscribe on every (re)connect: after the app plane reconnects the // shared manager, rooms are gone and — on another backend node — the // figure itself may need a state-driven rebuild. `sub` triggers both. @@ -947,7 +959,7 @@ export function XYChart(props) { errResyncs = 0; subscribe(); }; - plane.on("connect", onConnect); + plane.on(EVENT_CONNECT, onConnect); subCounts.set(liveToken, (subCounts.get(liveToken) || 0) + 1); if (plane.connected) subscribe(); @@ -974,15 +986,15 @@ export function XYChart(props) { destroyed = true; if (tracksClickInput) el.removeEventListener("click", rememberClick, true); resetEpoch(); - plane.off("payload", onPayload); - plane.off("msg", onMsg); - plane.off("err", onErr); - plane.off("disconnect", onDisconnect); - plane.off("connect", onConnect); + plane.off(EVENT_PAYLOAD, onPayload); + plane.off(EVENT_MSG, onMsg); + plane.off(EVENT_ERR, onErr); + plane.off(EVENT_DISCONNECT, onDisconnect); + plane.off(EVENT_CONNECT, onConnect); const remaining = (subCounts.get(liveToken) || 1) - 1; if (remaining <= 0) { subCounts.delete(liveToken); - if (plane.connected) plane.emit("unsub", { fig: liveToken, mid }); + if (plane.connected) plane.emit(EVENT_UNSUB, { fig: liveToken, mid }); } else { subCounts.set(liveToken, remaining); } diff --git a/python/reflex_xy/data_plane.py b/python/reflex_xy/data_plane.py index 889ca0b46..7762dab17 100644 --- a/python/reflex_xy/data_plane.py +++ b/python/reflex_xy/data_plane.py @@ -54,13 +54,31 @@ if TYPE_CHECKING: from xy._figure import Figure -__all__ = ["XY_PLANE", "XYChannel"] +__all__ = [ + "EVENT_ERR", + "EVENT_MSG", + "EVENT_PAYLOAD", + "EVENT_SUB", + "EVENT_UNSUB", + "XY_PLANE", + "XYChannel", +] #: Channel name for the chart data plane. A channel name is part of Reflex's #: websocket protocol, not a URL: it needs no route, no mount, and no #: reverse-proxy entry beyond what the app's websocket already has. XY_PLANE = "/_xy" +#: The wire vocabulary, named on both sides of it: `XYChart.jsx` declares the +#: same five strings, and `tests/reflex_adapter/test_assets.py` asserts the two +#: declarations match — so a rename here fails there rather than silently +#: producing a client that talks past the server. +EVENT_SUB = "sub" # client -> server: subscribe, join the figure room +EVENT_UNSUB = "unsub" # client -> server: leave the room +EVENT_MSG = "msg" # both ways: one kernel dispatch, its reply, or a push +EVENT_PAYLOAD = "payload" # server -> client: first paint / full refresh +EVENT_ERR = "err" # server -> client: failure, optionally asking for a resync + # One payload/message is screen-bounded by construction (§29); these caps are # the transport's fail-closed backstop, not a tuning knob. _MAX_PX_HINT = 8192 @@ -241,9 +259,9 @@ async def on_message( ) -> None: """Dispatch one inbound message to the data plane handlers.""" handler = { - "sub": self.on_sub, - "unsub": self.on_unsub, - "msg": self.on_msg, + EVENT_SUB: self.on_sub, + EVENT_UNSUB: self.on_unsub, + EVENT_MSG: self.on_msg, }.get(event) if handler is not None: await handler(session.sid, data) @@ -320,7 +338,7 @@ async def _send_payload( } if mid is not None: envelope["mid"] = mid - await self._send("payload", envelope, _buffer_bytes(raw), to=sid) + await self._send(EVENT_PAYLOAD, envelope, _buffer_bytes(raw), to=sid) async def on_unsub(self, sid: str, data: Any) -> None: token = self._token_of(data) @@ -378,7 +396,10 @@ async def on_msg(self, sid: str, data: Any) -> None: # Channel replies are bounded by construction, so this is a # contract check. await self._send( - "err", {"fig": token, "error": "reply exceeds wire attachment limit"}, [], to=sid + EVENT_ERR, + {"fig": token, "error": "reply exceeds wire attachment limit"}, + [], + to=sid, ) return envelope: dict[str, Any] = { @@ -391,7 +412,7 @@ async def on_msg(self, sid: str, data: Any) -> None: mid = self._mid_of(data) if mid is not None: envelope["mid"] = mid - await self._send("msg", envelope, wire_buffers, to=sid) + await self._send(EVENT_MSG, envelope, wire_buffers, to=sid) # -- server-side pushes (append/refresh fan-out) --------------------------- @@ -411,7 +432,7 @@ async def broadcast_message( # advanced; view-state pushes carry a generation stamp but do not # mutate the figure payload. await self._send( - "err", + EVENT_ERR, { "fig": token, "error": "push exceeds wire attachment limit", @@ -427,7 +448,7 @@ async def broadcast_message( } if version is not None: envelope["version"] = version - await self._send("msg", envelope, wire_buffers, room=self._room(token)) + await self._send(EVENT_MSG, envelope, wire_buffers, room=self._room(token)) async def broadcast_payload(self, token: str, entry: FigureEntry) -> None: """Push a full refreshed payload (figure rebuilt) to subscribers.""" @@ -436,7 +457,7 @@ async def broadcast_payload(self, token: str, entry: FigureEntry) -> None: if not self.registry.is_current(token, entry): return await self._send( - "payload", + EVENT_PAYLOAD, { "fig": token, "version": entry.version, @@ -682,7 +703,7 @@ async def _err( envelope: dict[str, Any] = {"fig": token, "error": error} if resync: envelope["resync"] = True - await self._send("err", envelope, [], to=sid) + await self._send(EVENT_ERR, envelope, [], to=sid) async def broadcast_error(self, token: str, error: str, resync: bool = False) -> None: """Room-wide err frame for server-side failures with no request to @@ -690,4 +711,4 @@ async def broadcast_error(self, token: str, error: str, resync: bool = False) -> envelope: dict[str, Any] = {"fig": token, "error": error} if resync: envelope["resync"] = True - await self._send("err", envelope, [], room=self._room(token)) + await self._send(EVENT_ERR, envelope, [], room=self._room(token)) diff --git a/python/reflex_xy/state_bridge.py b/python/reflex_xy/state_bridge.py index 775999b14..c2c2d38f3 100644 --- a/python/reflex_xy/state_bridge.py +++ b/python/reflex_xy/state_bridge.py @@ -25,6 +25,8 @@ from __future__ import annotations import inspect +import weakref +from collections.abc import Callable from typing import TYPE_CHECKING, Any, Optional from .data_vars import validate_columns @@ -115,10 +117,39 @@ async def rebuild_plan_figure(app: Any, composite: ParsedPlanToken) -> Optional[ return plan.bind(columns, source=source).figure() +def app_ref(app: Any) -> Callable[[], Any]: + """A zero-argument accessor for ``app`` that does not keep it alive. + + The data plane holds on to the app for as long as it is attached, and a + strong capture would make every hot reload's App — and the state manager + behind it — immortal. Objects that refuse weak references (test doubles, + mostly; a real ``rx.App`` supports them) fall back to a strong capture + rather than making ``setup`` fail on them. + + Args: + app: The Reflex app to reference. + + Returns: + A callable returning the app, or None once it has been collected. + """ + try: + return weakref.ref(app) + except TypeError: + return lambda: app + + def make_rebuild_hook(app: Any) -> Any: - """The data plane's RebuildHook, bound to one app instance.""" + """The data plane's RebuildHook, bound weakly to one app instance. + + An App that has been collected can no longer rebuild anything from state, + so the hook fails closed exactly as it does for an unknown token. + """ + resolve_app = app_ref(app) async def _rebuild(token_str: str) -> Optional["Figure"]: + app = resolve_app() + if app is None: + return None composite = parse_plan_token(token_str) if composite is not None: return await rebuild_plan_figure(app, composite) diff --git a/scripts/reflex_ws_smoke.py b/scripts/reflex_ws_smoke.py index 1a7da1d2c..1edc14187 100644 --- a/scripts/reflex_ws_smoke.py +++ b/scripts/reflex_ws_smoke.py @@ -184,16 +184,18 @@ def wait_for(self, expression: str, *, timeout_s: float = 60.0, label: str = "co time.sleep(0.25) raise SystemExit(f"timeout waiting for {label}; last={last!r}") - def backend_websockets(self) -> list[str]: - """Websockets to the app backend (excludes vite's dev-mode HMR socket).""" + def websockets(self) -> list[str]: + """Every websocket the page opened, including vite's dev-mode HMR one.""" self.eval("1") # pump queued CDP events urls: list[str] = [] for (sid, method), events in self.s._events.items(): if sid == self.sid and method == "Network.webSocketCreated": urls.extend(e.get("url", "") for e in events) - # The data plane is a channel *inside* this socket, so there is one - # URL to match — a second one would mean the chart opened its own. - return [u for u in urls if "/_event" in u] + return urls + + def backend_websockets(self) -> list[str]: + """Websockets to the app backend (excludes vite's dev-mode HMR socket).""" + return [u for u in self.websockets() if "/_event" in u] def sent_ws_frames(self, needle: str) -> list[str]: """Payloads of sent websocket frames containing `needle`.""" @@ -261,10 +263,17 @@ def main() -> None: ) print("mounted views:", probe.eval("Array.from(window.__xy_views.keys()).sort()")) - # 2) exactly one physical websocket to the backend for both planes + # 2) exactly one physical websocket to the backend for both planes. + # Checked in two parts, because the `/_event` filter alone cannot + # see a regression: a chart that opened its own data plane would + # land on some other URL and simply drop out of the count. So name + # the plane explicitly first, then count what is left. time.sleep(1.5) + own = [u for u in probe.websockets() if "/_xy" in u] + if own: + failures.append(f"the chart opened its own data plane socket: {own}") ws = probe.backend_websockets() - print(f"backend websockets: {len(ws)}") + print(f"backend websockets: {len(ws)} (of {len(probe.websockets())} total)") if len(ws) != 1: failures.append(f"expected exactly 1 backend websocket (shared transport), got {ws}") diff --git a/scripts/verify_ci_workflow.py b/scripts/verify_ci_workflow.py index 0b7a9073f..8d9977eaf 100644 --- a/scripts/verify_ci_workflow.py +++ b/scripts/verify_ci_workflow.py @@ -815,8 +815,11 @@ def validate_ci_workflow(path: Path = DEFAULT_CI_WORKFLOW) -> list[str]: "scripts/verify_benchmark_report.py transport.json --kind transport-loopback", "scripts/check_regressions.py --scatter scatter.json --kernel kernel.json", "--transport transport.json --emit-md spec/benchmarks/metrics.md", - "Upload regression benchmark report", - "if: always()", + # Name and condition paired, not two independent needles: this job now + # has a second `if: always()` upload (the browser evidence), which a + # bare needle would satisfy on its behalf — letting *this* step lose + # its condition and stop uploading after a failed gate, silently. + "- name: Upload regression benchmark report\n if: always()", "actions/upload-artifact@", "regression-benchmark-report", "if-no-files-found: warn", @@ -1081,7 +1084,10 @@ def validate_ci_workflow(path: Path = DEFAULT_CI_WORKFLOW) -> list[str]: "Rust-backed sdist install contract", "XY_REQUIRE_CARGO", "uv pip install --no-cache", - '"reflex>=0.9.6"', + # No Reflex requirement: the adapter needs the unreleased channel + # transport, so there is no floor to name, and this smoke only + # `import reflex_xy` — which pulls no Reflex at all. Restore the + # `xy[reflex]` install (and assert on it here) once channels ship. "import reflex_xy", "import xy.kernels as kernels", 'kernels.BACKEND == "native"', diff --git a/spec/design/reflex-integration.md b/spec/design/reflex-integration.md index 0fac3bafc..0e0f77c38 100644 --- a/spec/design/reflex-integration.md +++ b/spec/design/reflex-integration.md @@ -11,13 +11,19 @@ same performance contract as the notebook path: screen-bounded binary wire > **Unreleased dependency (temporary).** The data plane is a Reflex *channel* > (§2), which no released Reflex ships yet — it exists on > reflex-dev/reflex#6932. Until that lands, the `xy[reflex]` extra carries a -> git requirement for the branch and `[tool.uv.sources]` pins the whole Reflex -> workspace to it, with `[tool.hatch.metadata] allow-direct-references` to make -> the build accept that. This is deliberately unpublishable: PyPI refuses a -> direct reference, so a release cut from this tree fails at upload rather than +> git requirement naming one immutable commit on that branch, with +> `[tool.hatch.metadata] allow-direct-references` to make the build accept it. +> Two properties are deliberate. It is **unpublishable**: PyPI refuses a direct +> reference, so a release cut from this tree fails at upload rather than > shipping a version specifier that would install a Reflex where every chart -> stays blank. Replace all three with the real floor when channels are -> released. +> stays blank. And it is a **commit, not the branch**: the lockfiles only bind +> `uv sync --locked`, so a branch ref would let a bare `pip install xy[reflex]` +> or any `uv lock` regeneration pull framework code nobody reviewed. The same +> commit is pinned by `docs/app/pyproject.toml`'s four Reflex requirements — uv +> refuses two URLs for one package — and +> `tests/test_dependencies.py::test_docs_app_pins_the_same_reflex_commit` keeps +> the six strings in step. Replace the lot with the real floor when channels +> are released. Two decisions define this revision (superseding the HTTP-routes draft — see §8 for the audit trail): @@ -161,6 +167,14 @@ The attachments are not a field of the envelope: `XYChannel._send` passes them as the frame's attachment list, and on the client every handler takes `(data, buffers)`. +The five event names are declared as constants on both sides — +`EVENT_SUB`/`EVENT_UNSUB`/`EVENT_MSG`/`EVENT_PAYLOAD`/`EVENT_ERR` in +`data_plane.py` and again in `XYChart.jsx` — and +`tests/reflex_adapter/test_assets.py` reads the Python ones and asserts the +JavaScript declarations match. This is the one seam where the two halves could +drift without any other test noticing: a renamed event on one side alone +produces a client that talks past the server, with no error anywhere. + `mid` is a validated optional per-mount id: several charts on a page share the socket, so direct interaction replies and direct subscription payloads echo it and other mounts ignore the addressed envelope. Pushes and full-payload @@ -211,7 +225,7 @@ totality contract). This section records only what is specific to this host. **`view_change` does not reach the kernel here.** The wrapper intercepts the outgoing message and invokes the Reflex `on_view_change` prop directly (`dispatchView` in `python/reflex_xy/assets/XYChart.jsx`), because -the namespace registers no Python-side view callback (§5). Every other request +the data plane registers no Python-side view callback (§5). Every other request type crosses the socket unchanged and is dispatched by the shared `handle_message`. @@ -345,8 +359,8 @@ replacement is never removed. ### 3.3 Access control -The connection's `?token=` (the Reflex client token) is captured at -namespace connect. A state token embeds the client token it was minted for, +The connection's `?token=` (the Reflex client token) is resolved by Reflex +and read off the channel session when it opens. A state token embeds the client token it was minted for, and `sub`/`msg` refuse a figure whose embedded client token differs from the connection's (`err: figure belongs to another session`). Tokens carry nothing their own client doesn't already know. When Reflex grows real @@ -438,10 +452,10 @@ figure/data memory, not correctness. While at least one rebuildable subscriber remains, both the sweep and an explicit release retain only the removed token's scalar version so a republish on the same worker stays monotonic; the figure and its data buffers are released. If an interaction is the first touch after -eviction, the namespace +eviction, the data plane rebuilds, sends every subscribed mount a replacement payload room-wide, and drops the old-generation interaction for the triggering client to retry. If a -new `sub` is first, the namespace broadcasts the rebuild to existing room +new `sub` is first, the data plane broadcasts the rebuild to existing room members before joining the requester, then sends that mount one `mid`-addressed payload built for its own `px` hint. The direct path re-reads the current entry after joining the room: a normal replacement that landed before the join is @@ -675,7 +689,7 @@ refused as a subscription outright. re-binds every mounted dependent through the `data token → {digests}` index — fresh figures, bumped versions, coalesced room broadcasts, exactly the figure-var republish machinery. The index is added to when a composite -binds (namespace `sub`) and pruned on **every transition that can end the +binds (a data-plane `sub`) and pruned on **every transition that can end the mount**: the last unsubscribe or disconnect for the composite token, its entry's release, failed-rebuild cleanup, the TTL sweep, and a republish that finds it unmounted (`_unbind_plan_if_unmounted_locked`). "Bounded by @@ -836,7 +850,7 @@ impossible — the JS that renders a payload is always the build that shipped with the Python that produced it. One renderer for notebooks, static export, and Reflex. -The wrapper: opens/reuses the shared namespace socket, `sub`s with the +The wrapper: takes the shared `/_xy` channel, `sub`s with the element's measured width, builds a `ChartView` for the first `payload`, and passes later full payloads to `ChartView.updatePayload` (preserving keyed animation state; destroy + rebuild is only the compatibility fallback), @@ -844,7 +858,7 @@ bridges `comm` to `msg` events, and forwards semantic events into Reflex's event system via the component's event-trigger props (`props.onPointHover(row)` → `addEvents(...)` → the user's handler). Client-side niceties: `view_change` resolves locally (no kernel round-trip; -the namespace registers no Python callbacks), `click` issues a tagged `pick` +the data plane registers no Python callbacks), `click` issues a tagged `pick` so `on_point_click` delivers the exact row, `selection` replies pair with the brush rect that produced them. @@ -976,7 +990,7 @@ def remember_view(self, event: dict): reflex_xy.chart(figure=Dash.cloud, on_view_change=Dash.remember_view) ``` -Every kernel request echoes the last payload version as `v`; the namespace +Every kernel request echoes the last payload version as `v`; the data plane silently rejects requests for another figure version and drops an explicitly malformed `v`. Omitted `v` remains accepted for compatibility. Replies echo the operation version; room-wide append pushes carry the newly bumped version, diff --git a/spec/design/view-state.md b/spec/design/view-state.md index cb8e90d46..4f92ade96 100644 --- a/spec/design/view-state.md +++ b/spec/design/view-state.md @@ -223,10 +223,10 @@ which does converge room-wide. Path: per-generation operation lease plus entry-local synchronous figure lock → one wire message (§8) → pushed room-wide as a `msg` event on the `/_xy` -namespace → every client in the room applies it through the §3 mutation path +channel → every client in the room applies it through the §3 mutation path with `source: "api"`. Message construction and version capture are atomic with -namespace payload/interaction kernels and an append's mutation and version -bump, while different figures remain independent. Namespace work that also +data-plane payload/interaction kernels and an append's mutation and version +bump, while different figures remain independent. Data-plane work that also needs the generation's async lock always acquires async then synchronous; caller-thread view writes acquire only the synchronous lock, so the order has no reverse edge. Multi-client semantics are therefore identical to `append` diff --git a/spec/design/wire-protocol.md b/spec/design/wire-protocol.md index b6dd6def9..3a5990297 100644 --- a/spec/design/wire-protocol.md +++ b/spec/design/wire-protocol.md @@ -106,7 +106,7 @@ gate. This is the one request type a host may withhold: on the Reflex host it never reaches the kernel, because `XYChart.jsx` intercepts the outgoing message and invokes the `on_view_change` prop directly -(`python/reflex_xy/assets/XYChart.jsx`) — that namespace +(`python/reflex_xy/assets/XYChart.jsx`) — that channel registers no Python-side view callback. **`select`** — box select. Edges are ordered by `lod.normalize_window` with @@ -305,7 +305,7 @@ non-atomically, the client listens to *both* change events, defers a torn pair — a column that no longer fits its buffer — without consuming the seq, and keys applied state on (seq, buffers identity), so the write that completes the pair re-fires the apply and repairs even a same-shape tear. -The `/_xy` namespace has no synced traits, so it wraps the +The `/_xy` channel has no synced traits, so it wraps the same spec and buffers in a room-wide `msg` push. In both cases the client reads the buffer layout from `spec.buffer_layout`, never from the shape of what arrived (§5). @@ -320,7 +320,7 @@ out of the client's history stack. A document the client cannot validate logged — never partially applied. Built by `Figure.state_patch_message`; senders are `FigureWidget.set_view`/`select`/`clear_selection` (anywidget comm) and `reflex_xy.set_view`/`select`/`clear_selection` (room-wide on the -`/_xy` namespace). +`/_xy` channel). **`view_nav`** — `{type, op: "reset", axes?}`, no buffers. Navigation to the home ranges, well-defined for every receiver because home ranges are diff --git a/tests/reflex_adapter/test_assets.py b/tests/reflex_adapter/test_assets.py index e3ac83803..b1a06ca79 100644 --- a/tests/reflex_adapter/test_assets.py +++ b/tests/reflex_adapter/test_assets.py @@ -8,6 +8,7 @@ import reflex_xy import xy +from reflex_xy import data_plane from reflex_xy.assets import _client_source, _link_client ADAPTER_ASSETS = pathlib.Path(reflex_xy.__file__).parent / "assets" @@ -56,19 +57,28 @@ def test_link_client_creates_and_repairs(tmp_path): def test_wrapper_speaks_the_data_plane_protocol(): """The JSX wrapper and data_plane.py must agree on event names and shapes.""" jsx = (ADAPTER_ASSETS / "XYChart.jsx").read_text(encoding="utf-8") - # transport identity: one channel name, matching XY_PLANE in data_plane.py, - # on the app's own websocket — no second connection to open or configure - assert 'const XY_PLANE = "/_xy"' in jsx + # transport identity: one channel name on the app's own websocket — no + # second connection to open or configure. The name itself comes from + # data_plane.py rather than a literal, so a rename there fails here. + assert f'const XY_PLANE = "{data_plane.XY_PLANE}"' in jsx assert "getChannel(XY_PLANE)" in jsx # socket.io is gone: no client, no namespace URL, no engine.io mount path for gone in ("socket.io-client", "nsUrl", "endpoint.pathname", "env.TRANSPORT", "io("): assert gone not in jsx, f"wrapper still carries socket.io machinery: {gone}" + # Both sides name the same five events, and take their names from the same + # place: this is the one point where the Python and JavaScript halves of + # the protocol can drift apart without any other test noticing. + for constant in ("EVENT_SUB", "EVENT_UNSUB", "EVENT_MSG", "EVENT_PAYLOAD", "EVENT_ERR"): + event = getattr(data_plane, constant) + assert f'const {constant} = "{event}"' in jsx, ( + f"XYChart.jsx does not declare {constant} = {event!r} the way data_plane.py does" + ) # client -> server events - for needle in ('"sub"', '"unsub"', '"msg"'): - assert f"plane.emit({needle}" in jsx + for constant in ("EVENT_SUB", "EVENT_UNSUB", "EVENT_MSG"): + assert f"plane.emit({constant}" in jsx # server -> client events - for needle in ('"payload"', '"msg"', '"err"'): - assert f"plane.on({needle}" in jsx + for constant in ("EVENT_PAYLOAD", "EVENT_MSG", "EVENT_ERR"): + assert f"plane.on({constant}" in jsx # binary columns go straight into typed arrays — never through JSON numbers assert "new Uint8Array(b)" in jsx assert "data.version < payloadVersion" in jsx @@ -99,7 +109,7 @@ def test_wrapper_speaks_the_data_plane_protocol(): assert jsx.count("resetEpoch();") == 3 # subscribe, disconnect, cleanup assert "awaitingPayload || !plane.connected" in jsx assert "awaitingPayload = false" in jsx - assert 'plane.on("disconnect", onDisconnect)' in jsx + assert "plane.on(EVENT_DISCONNECT, onDisconnect)" in jsx assert "wireVersion > expected && plane.connected" in jsx assert "data.resync === true && plane.connected" in jsx # Rejected replies and accepted generation advances both reclaim pending diff --git a/tests/reflex_adapter/test_channel_browser.py b/tests/reflex_adapter/test_channel_browser.py index 67581c27e..4e9168246 100644 --- a/tests/reflex_adapter/test_channel_browser.py +++ b/tests/reflex_adapter/test_channel_browser.py @@ -9,23 +9,38 @@ from __future__ import annotations import json +import os from collections.abc import Generator from pathlib import Path import pytest -pytest.importorskip("playwright") +# This is the only check of the single-shared-websocket claim, so a missing +# browser must not quietly turn it into a pass. `XY_REQUIRE_BROWSER=1` — what +# CI sets — makes both the Python package and the browser binary hard +# requirements; without it a bare checkout still skips. +REQUIRE_BROWSER = os.environ.get("XY_REQUIRE_BROWSER") == "1" +if not REQUIRE_BROWSER: + pytest.importorskip("playwright") from PIL import Image # noqa: E402 +from playwright.sync_api import Error as PlaywrightError # noqa: E402 from playwright.sync_api import WebSocket, sync_playwright # noqa: E402 from reflex.testing import AppHarness # noqa: E402 +from reflex_xy.data_plane import XY_PLANE # noqa: E402 + # The app's event websocket, the one the chart has to share. EVENT_PATH = "/_event" # Headless chromium has no GPU; xy's own browser probes use the same pair. CHROMIUM_ARGS = ("--use-angle=swiftshader", "--enable-unsafe-swiftshader") +# Where to keep the painted-chart screenshot. CI points this at an uploaded +# artifact directory so the evidence outlives the run; locally it defaults to +# pytest's tmp_path. +EVIDENCE_DIR = os.environ.get("XY_BROWSER_EVIDENCE") + def ChannelChartApp(): """A Reflex app with one live xy chart served over a channel.""" @@ -106,43 +121,59 @@ def test_chart_paints_from_channel_binary(chart_app: AppHarness, tmp_path: Path) sockets: list[WebSocket] = [] binary_frames: list[int] = [] + shot = Path(EVIDENCE_DIR or tmp_path) / "chart.png" + shot.parent.mkdir(parents=True, exist_ok=True) + with sync_playwright() as playwright: - browser = playwright.chromium.launch(args=list(CHROMIUM_ARGS)) - page = browser.new_page(viewport={"width": 1024, "height": 768}) - - def watch(socket: WebSocket) -> None: - sockets.append(socket) - if EVENT_PATH not in socket.url: - return # the dev server's hot-reload channel - socket.on( - "framereceived", - lambda payload: ( - binary_frames.append(len(payload)) if isinstance(payload, bytes) else None - ), + try: + browser = playwright.chromium.launch(args=list(CHROMIUM_ARGS)) + except PlaywrightError as error: # no browser binary installed + if REQUIRE_BROWSER: + raise + pytest.skip(f"chromium is not installed for playwright: {error}") + # Explicitly, rather than by leaving it to the sync_playwright exit: a + # failing assertion inside the block would otherwise leave the Chromium + # process up until the whole context unwinds, and those accumulate. + try: + page = browser.new_page(viewport={"width": 1024, "height": 768}) + + def watch(socket: WebSocket) -> None: + sockets.append(socket) + if EVENT_PATH not in socket.url: + return # the dev server's hot-reload channel + socket.on( + "framereceived", + lambda payload: ( + binary_frames.append(len(payload)) if isinstance(payload, bytes) else None + ), + ) + + page.on("websocket", watch) + page.goto(chart_app.frontend_url) + page.wait_for_selector("#ready") + # The chart only reaches "ready" once a payload has been applied. + # A cold dev server still has xy's WebGL client bundle to transform, + # which outlasts the default wait by a wide margin. + chart = page.locator('[data-xy-slot="root"]') + chart.wait_for(state="visible", timeout=120_000) + page.wait_for_function( + "!!document.querySelector('[data-xy-context-state=\"ready\"]')", + timeout=120_000, ) - - page.on("websocket", watch) - page.goto(chart_app.frontend_url) - page.wait_for_selector("#ready") - # The chart only reaches "ready" once a payload has been applied. - # A cold dev server still has xy's WebGL client bundle to transform, - # which outlasts the default wait by a wide margin. - chart = page.locator('[data-xy-slot="root"]') - chart.wait_for(state="visible", timeout=120_000) - page.wait_for_function( - "!!document.querySelector('[data-xy-context-state=\"ready\"]')", - timeout=120_000, - ) - # The accessible summary is computed from the decoded columns. - summary = page.locator('[id$="-summary"]').inner_text() - shot = tmp_path / "chart.png" - chart.screenshot(path=str(shot)) - browser.close() - + # The accessible summary is computed from the decoded columns. + summary = page.locator('[id$="-summary"]').inner_text() + chart.screenshot(path=str(shot)) + finally: + browser.close() + + # The chart opened nothing of its own. Checked first and by name, because + # the count below cannot see it: a self-opened data plane would land on + # some other URL and simply drop out of the filter. + own_sockets = [socket.url for socket in sockets if XY_PLANE in socket.url] + assert not own_sockets, f"the chart opened its own data plane socket: {own_sockets}" # One connection to the backend for the whole page: the chart multiplexes - # onto the app's event websocket instead of opening a data plane of its - # own. (The other socket is the dev server's hot-reload channel, which a - # built app does not have.) + # onto the app's event websocket. (The other socket is the dev server's + # hot-reload channel, which a built app does not have.) backend_sockets = [socket.url for socket in sockets if EVENT_PATH in socket.url] assert len(backend_sockets) == 1, backend_sockets # Columns travelled as binary frames on that socket, not as JSON numbers @@ -164,6 +195,7 @@ def watch(socket: WebSocket) -> None: "summary": summary.replace("\n", " ")[:120], "colours": colours, "binary_frames": binary_frames, + "screenshot": str(shot), } ) ) diff --git a/tests/reflex_adapter/test_data_plane.py b/tests/reflex_adapter/test_data_plane.py index 7a2910979..4030c4601 100644 --- a/tests/reflex_adapter/test_data_plane.py +++ b/tests/reflex_adapter/test_data_plane.py @@ -18,7 +18,7 @@ import socket import threading from types import SimpleNamespace -from typing import Any, TypedDict +from typing import Any, Optional, TypedDict import aiohttp import numpy as np @@ -103,6 +103,7 @@ def __init__(self, session: aiohttp.ClientSession, ws: aiohttp.ClientWebSocketRe self.messages: asyncio.Queue = asyncio.Queue() self.errors: asyncio.Queue = asyncio.Queue() self._opened = asyncio.Event() + self.reader_error: Optional[BaseException] = None self._queues = { "payload": self.payloads, "msg": self.messages, @@ -111,6 +112,19 @@ def __init__(self, session: aiohttp.ClientSession, ws: aiohttp.ClientWebSocketRe self._reader = asyncio.create_task(self._read_forever()) async def _read_forever(self) -> None: + try: + await self._read_frames() + except (asyncio.CancelledError, ConnectionResetError): + raise # ordinary teardown + except Exception as error: # noqa: BLE001 - the wire is what's under test + # A malformed frame is the failure this suite exists to catch, so + # it must not surface as "everything timed out". Hand it to every + # waiter instead of dying quietly with an unretrieved exception. + self.reader_error = error + for queue in self._queues.values(): + queue.put_nowait(error) + + async def _read_frames(self) -> None: with contextlib.suppress(asyncio.CancelledError, ConnectionResetError): async for frame in self._ws: if frame.type == aiohttp.WSMsgType.BINARY: @@ -146,7 +160,10 @@ async def disconnect(self) -> None: return self.connected = False self._reader.cancel() - with contextlib.suppress(asyncio.CancelledError): + # Any reader failure was already handed to the waiters, so teardown + # only has to retrieve it — re-raising here would replace a test's real + # assertion failure with a confusing one from the cleanup path. + with contextlib.suppress(BaseException): await self._reader await self._ws.close() await self._session.close() @@ -175,7 +192,10 @@ def __init__(self, client: PlaneClient) -> None: @staticmethod async def next(queue: asyncio.Queue, timeout: float = 5.0): - return await asyncio.wait_for(queue.get(), timeout) + frame = await asyncio.wait_for(queue.get(), timeout) + if isinstance(frame, BaseException): + raise AssertionError("the data plane reader failed") from frame + return frame class FakeSession: diff --git a/tests/reflex_adapter/test_setup_attachment.py b/tests/reflex_adapter/test_setup_attachment.py new file mode 100644 index 000000000..3fa466ae2 --- /dev/null +++ b/tests/reflex_adapter/test_setup_attachment.py @@ -0,0 +1,135 @@ +"""`setup(app)` attaches per app, not per process. + +The registry is a process-global cache while Apps are not: a hot reload +re-imports the app module, AppHarness builds one per test, and nothing stops +two Apps coexisting in one interpreter. These tests pin the two consequences — +`setup` must never hand an already-attached app a second channel (Reflex +rejects a duplicate channel name outright), and registry fan-out must reach +every live app rather than only the most recent one. +""" + +from __future__ import annotations + +import asyncio +import gc + +import numpy as np +import pytest +import reflex as rx +from reflex.istate.manager.memory import StateManagerMemory +from reflex_base.registry import RegistrationContext + +import xy +from reflex_xy import app as adapter_app +from reflex_xy.app import setup + + +def make_app(): + """A real `rx.App`. + + An App claims the registration context it is built in and refuses to share + it, so every App after the first is built in a fork — which is exactly the + shape this module is about: several Apps, one process. + """ + try: + context = RegistrationContext.get() + except LookupError: + app = rx.App() # the first App in this process claims the context + else: + with context.fork(): + app = rx.App() + app._state_manager = StateManagerMemory() + return app + + +def make_figure(n: int = 8): + xs = np.linspace(0.0, 1.0, n) + return xy.scatter_chart(xy.scatter(xs, xs * 3.0), width=640, height=400).figure() + + +def test_setup_is_idempotent_for_one_app(): + app = make_app() + first = setup(app) + assert setup(app) is first + assert list(app._channels) == [first.name] + + +def test_revisiting_an_app_does_not_register_a_second_channel(): + """A → B → A. Keying idempotency on "the most recent app" would build a + second channel for A here, and `App.register_channel` raises on the + duplicate name — turning an ordinary multi-app process into a crash.""" + app_a, app_b = make_app(), make_app() + channel_a = setup(app_a) + channel_b = setup(app_b) + + assert channel_a is not channel_b + assert setup(app_a) is channel_a # must not raise + assert list(app_a._channels) == [channel_a.name] + assert list(app_b._channels) == [channel_b.name] + + +def test_publish_fans_out_to_every_live_app(_fresh_registry, monkeypatch): + """The second app must not silently strand the first app's subscribers.""" + registry = _fresh_registry + app_a, app_b = make_app(), make_app() + channel_a = setup(app_a) + channel_b = setup(app_b) + delivered: list[tuple[str, object]] = [] + + for channel in (channel_a, channel_b): + + async def broadcast(token, entry, _channel=channel): + delivered.append((token, _channel)) + + monkeypatch.setattr(channel, "broadcast_payload", broadcast) + + async def main(): + registry.attach_loop(asyncio.get_running_loop()) + token = registry.register(make_figure()) + registry.publish(token, make_figure(12)) + await asyncio.sleep(0) # let the scheduled fan-out task run + await asyncio.sleep(0) + return token + + token = asyncio.run(main()) + assert sorted(channel is channel_b for _, channel in delivered) == [False, True] + assert {name for name, _ in delivered} == {token} + + +def test_one_failing_plane_does_not_swallow_the_others(_fresh_registry, monkeypatch): + registry = _fresh_registry + app_a, app_b = make_app(), make_app() + channel_a, channel_b = setup(app_a), setup(app_b) + reached = [] + + async def boom(token, entry): + raise RuntimeError("transport failed") + + async def ok(token, entry): + reached.append(token) + + monkeypatch.setattr(channel_a, "broadcast_payload", boom) + monkeypatch.setattr(channel_b, "broadcast_payload", ok) + + async def main(): + entry = registry.publish("tok", make_figure(), broadcast=False) + with pytest.raises(RuntimeError, match="transport failed"): + await adapter_app._fan_out_payload("tok", entry) + + asyncio.run(main()) + assert reached == ["tok"], "the healthy plane must still have received the payload" + + +def test_a_collected_app_stops_receiving_fan_out(): + """A hot reload replaces the App; the old one must not be kept alive by + the attachment list, nor keep receiving broadcasts once collected.""" + app_a, app_b = make_app(), make_app() + setup(app_a) + setup(app_b) + assert len(adapter_app._live_planes()) == 2 + + del app_a + gc.collect() + + assert len(adapter_app._live_planes()) == 1 + assert len(adapter_app._attached) == 1 diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py index d7e192975..4261e9f08 100644 --- a/tests/test_dependencies.py +++ b/tests/test_dependencies.py @@ -8,6 +8,19 @@ ROOT = Path(__file__).resolve().parents[1] CORE_PACKAGE = ROOT / "python" / "xy" +# TEMPORARY, tracked in spec/design/reflex-integration.md § Status: the data +# plane needs Reflex's channel transport, which is unreleased, so `xy[reflex]` +# names an immutable commit instead of a version floor. A direct reference is +# unpublishable by design — a release cut from this tree fails at upload rather +# than shipping a floor that installs a Reflex where every chart stays blank. +# It is a commit and not the branch so that an install which bypasses the +# lockfile still gets the reviewed framework code. Restore the `>=` +# assertion, and drop `test_docs_app_pins_the_same_reflex_commit`, when +# channels are released. +REFLEX_REQUIREMENT = ( + "git+https://github.com/benedikt-bartscher/reflex.git@02dae5601eed0b678703a049db21634f83b625e7" +) + def _dependency_name(requirement: str) -> str: requirement = requirement.split(";", 1)[0].strip() @@ -35,15 +48,8 @@ def test_core_publishes_only_the_reflex_optional_dependency() -> None: extras = project.get("optional-dependencies") or {} assert set(extras) == {"reflex"} - # TEMPORARY, tracked in spec/design/reflex-integration.md § Status: the - # data plane needs Reflex's channel transport, which is unreleased, so the - # extra names the branch instead of a floor. A direct reference is - # unpublishable by design — the release fails at upload rather than - # shipping a floor that installs a Reflex where every chart stays blank. - # Restore the `>=` assertion when channels are released. assert any( - _dependency_name(requirement) == "reflex" - and "git+https://github.com/benedikt-bartscher/reflex.git@make-sio-optional" in requirement + _dependency_name(requirement) == "reflex" and REFLEX_REQUIREMENT in requirement for requirement in extras["reflex"] ), ( "xy[reflex] must select the Reflex the adapter actually needs while the " @@ -62,6 +68,28 @@ def test_core_publishes_only_the_reflex_optional_dependency() -> None: ) +def test_docs_app_pins_the_same_reflex_commit() -> None: + """Six requirement strings name this commit; they have to agree. + + uv refuses two URLs for one package, so the docs app cannot resolve at all + if its Reflex requirements drift from the root extra's — and a half-updated + pin would otherwise only surface as a resolution error in a separate CI job. + """ + docs = tomllib.loads(ROOT.joinpath("docs/app/pyproject.toml").read_text(encoding="utf-8")) + requirements = (docs.get("project") or {}).get("dependencies") or [] + reflex_requirements = [ + requirement + for requirement in requirements + if _dependency_name(requirement).startswith("reflex") + ] + + assert reflex_requirements, "the docs app must depend on Reflex" + for requirement in reflex_requirements: + assert REFLEX_REQUIREMENT in requirement, ( + f"docs/app pins a different Reflex than xy[reflex]: {requirement}" + ) + + def test_core_package_does_not_import_reflex() -> None: forbidden = ("reflex", "reflex_core", "reflex_base") violations: list[str] = [] diff --git a/uv.lock b/uv.lock index 2e1a8f416..09be1a8da 100644 --- a/uv.lock +++ b/uv.lock @@ -447,6 +447,83 @@ reload = [ { name = "watchfiles" }, ] +[[package]] +name = "greenlet" +version = "3.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/a3/07297917485ee2ca85bc3c8dc6ed85ad3fffcf424047fba62671dba68e97/greenlet-3.5.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61", size = 294165, upload-time = "2026-08-10T13:25:17.987Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/6f732f9314cda54c5fd48a7620c7160f4f286967e8045ad94b9d66ce80b7/greenlet-3.5.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c", size = 613610, upload-time = "2026-08-10T14:14:33.829Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c0/b27589e25d220289edcd4d582b2b17b83058d1a56d53d971b6ea1a34f10d/greenlet-3.5.5-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da", size = 625481, upload-time = "2026-08-10T14:27:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/39/82/5c873dbb4fb001d22fbbd50e80d4c1b0181ddae106856132160f84b94e88/greenlet-3.5.5-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:abc8bc8d9f935cd685457545b6a53863a877fdc12c2c0f5ee9beee18d9db139c", size = 633329, upload-time = "2026-08-10T14:30:06.062Z" }, + { url = "https://files.pythonhosted.org/packages/51/2d/f2c928218ac52f26d7a2c188c171d1b7e728b23782cb3347e7b4fce1493a/greenlet-3.5.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864", size = 624562, upload-time = "2026-08-10T13:40:48.064Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/f7df98e72a8eb4a7edfec5a08d6d1a4ab53a52c95ba0b2ea6c10b8dd9bd0/greenlet-3.5.5-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:3134291427bb0f3526e9d90311988caf336eb43730e95244997a4fb15f45144f", size = 428145, upload-time = "2026-08-10T14:30:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4a/92fc51d5d35912f4f06eec037ba347985defd0be47463a010a325634d9d2/greenlet-3.5.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db", size = 1584909, upload-time = "2026-08-10T14:15:04.343Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/ed98b80ac5738c149a5258544843c45601ade1fd70f61740cdaead6351b3/greenlet-3.5.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39", size = 1651184, upload-time = "2026-08-10T13:40:28.879Z" }, + { url = "https://files.pythonhosted.org/packages/d8/be/b582ceb80cefdf9d8da34078714e4b12b3d16f509dee0f65e40a5cc8fc7d/greenlet-3.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53", size = 323280, upload-time = "2026-08-10T13:26:07.495Z" }, + { url = "https://files.pythonhosted.org/packages/4d/18/5313c4c58598c38b0373c013e4ff2b3e6d258aaaa338f373335ebecdaddd/greenlet-3.5.5-cp311-cp311-win_arm64.whl", hash = "sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5", size = 307785, upload-time = "2026-08-10T13:28:34.874Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7e/9ecd0285e3153532ae07aeb88063c43c72b4221cf0d4d123b02f3682e3ff/greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380", size = 295809, upload-time = "2026-08-10T13:25:34.023Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/60e4bbcc89252037b18087f2ec16405d5b2d5be42dde191bbf3667e96102/greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053", size = 611910, upload-time = "2026-08-10T14:14:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/cd5134be659cd4a443e7a61ae670dabec165a814c51162916d637b6dd38e/greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95", size = 624198, upload-time = "2026-08-10T14:27:25.229Z" }, + { url = "https://files.pythonhosted.org/packages/9b/30/87c212b5c684d0e72974f1063b7a9687631e8985902c06e1016542c874e7/greenlet-3.5.5-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f", size = 629504, upload-time = "2026-08-10T14:30:07.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/ac/5c5b959999b6f09c3026b5dfe171575bc3121c5236ce74f495096f25b203/greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d", size = 621439, upload-time = "2026-08-10T13:40:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/63/2c/eb487fafc9f50ffff2b1e0b697f70fb34bf150821c08ab225aacf5583a7e/greenlet-3.5.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759", size = 432462, upload-time = "2026-08-10T14:30:02.309Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6acf112ed8aee499f25b4d6949820fb02ac950ff9c1f3d793bd5be0599f2/greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b", size = 1581342, upload-time = "2026-08-10T14:15:05.653Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/734e5f198888876b42d7616ff6644c075baf6b8a2412deadd6b0e1b8b20c/greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2", size = 1645744, upload-time = "2026-08-10T13:40:30.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/30/1f42b88dc587b5899ee50616ad56ee40cafaf225df4fb829f10183c62a5c/greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18", size = 324171, upload-time = "2026-08-10T13:28:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/76/e5/4dee4d8d2e603fe5fdd7b444e63219f7b9bd852c60c6214511c7157cbe88/greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52", size = 308362, upload-time = "2026-08-10T13:26:46.839Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" }, + { url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/eaa476d6bf3816828d0d70e80dcc36bf30a058233bd889e707e693f6e860/greenlet-3.5.5-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42", size = 632726, upload-time = "2026-08-10T14:30:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f2/0cc2849ede68579291e9c59b3ab6ec1958f98681cca5b14d8fc75bf674a4/greenlet-3.5.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b", size = 434966, upload-time = "2026-08-10T14:30:03.729Z" }, + { url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" }, + { url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" }, + { url = "https://files.pythonhosted.org/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" }, + { url = "https://files.pythonhosted.org/packages/eb/52/f005d579acde46c3d1cc3cab1c9f3d5708c8a3006a4120e8cf5da801afe9/greenlet-3.5.5-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8", size = 677863, upload-time = "2026-08-10T14:30:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8a/a75f8a2bdcef3c358a3147cdc9db3aa83755f0a038f766ab0bedb66f512c/greenlet-3.5.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53", size = 480554, upload-time = "2026-08-10T14:30:05.171Z" }, + { url = "https://files.pythonhosted.org/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" }, + { url = "https://files.pythonhosted.org/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/594fa2de7fae7629168a404a4305d7d7e31a5742c50a801b1839543cb93d/greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07", size = 311146, upload-time = "2026-08-10T13:27:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" }, + { url = "https://files.pythonhosted.org/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/0d7887aa4bbfc9eba075cc428244dfc96f623478454d5ec81180d0d6bd5a/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387", size = 681587, upload-time = "2026-08-10T14:30:13.519Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" }, + { url = "https://files.pythonhosted.org/packages/4f/18/8d58ba1c429b0383e3219a3d0e0bba241d0444d8ed05b73349953c7d7c7b/greenlet-3.5.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41", size = 510175, upload-time = "2026-08-10T14:30:07.047Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" }, + { url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" }, + { url = "https://files.pythonhosted.org/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" }, + { url = "https://files.pythonhosted.org/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" }, + { url = "https://files.pythonhosted.org/packages/65/53/4e13642efc4d7ad6554ecb2242a5be42666b2e1a067323e88dfc0124a04b/greenlet-3.5.5-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76", size = 681436, upload-time = "2026-08-10T14:30:14.839Z" }, + { url = "https://files.pythonhosted.org/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" }, + { url = "https://files.pythonhosted.org/packages/cd/32/188447c9a468d6977d2989397226b0c6b65ab6f4cf943f931643328512fc/greenlet-3.5.5-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474", size = 487404, upload-time = "2026-08-10T14:30:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" }, + { url = "https://files.pythonhosted.org/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/67/67/857e88a36301caa0e029870132c2478bd55d896630321432afab03a3115f/greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769", size = 311750, upload-time = "2026-08-10T13:34:08.815Z" }, + { url = "https://files.pythonhosted.org/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/e450a68a152f819491d8c7df6a8254e761d87e6a78759268961f8c5bd4dd/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1", size = 686022, upload-time = "2026-08-10T14:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/17e63d6bf3b9c9b9dbea981b7f643a71f79603bdfb4f1c3a9cf353e22aed/greenlet-3.5.5-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f", size = 516951, upload-time = "2026-08-10T14:30:10.907Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" }, + { url = "https://files.pythonhosted.org/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1048,6 +1125,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, ] +[[package]] +name = "playwright" +version = "1.61.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet" }, + { name = "pyee" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/ee/31e4e0db36588b817a10b299a0285082545fde7d36543c2abe498bb3d61a/playwright-1.61.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:ff138c3a604f69911e9d42fd036e55c2a171e5616edf04c1e7f60a2a285540b0", size = 43421877, upload-time = "2026-06-29T10:32:48.428Z" }, + { url = "https://files.pythonhosted.org/packages/42/35/71395dd3ecc798965be4a3ef8c443217d4abca168e7cb34536304f9489e6/playwright-1.61.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:009588c2a7e499bc5a8b425b61fa65490968bbda9cd69e0cf2cff10f8304659a", size = 42205016, upload-time = "2026-06-29T10:32:52.104Z" }, + { url = "https://files.pythonhosted.org/packages/f4/44/323164cf5cd1647bdefce76ffce27651aadb959d089b48f53ea40918276e/playwright-1.61.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:9f7de4536088d12037c13a52b7ea34b59270b78926bb56935070597ffac6b1af", size = 43421884, upload-time = "2026-06-29T10:32:55.773Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f8/a35bf179e4ba2522c1893635094a64e407572547bd61528820fc0abc87fe/playwright-1.61.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:54f3b39f6eab832e33458c1dd7da0b5682aedab3b09ae731b5c59fa12fd2024e", size = 47421381, upload-time = "2026-06-29T10:32:59.903Z" }, + { url = "https://files.pythonhosted.org/packages/b7/eb/e3f922348ec17c315f98c463f72faa1181a1c3de0bfe31a8d2edf6561723/playwright-1.61.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93454322ade8c11d5d6c211bfd91bdfb9ffb4810e3e026371bcbc4bec1b7ee4c", size = 47120545, upload-time = "2026-06-29T10:33:03.574Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a6/5be4e52b40a9c0c8a073e7c5b0785c05cf5a9ea8f8a7b5b260e32d970342/playwright-1.61.0-py3-none-win32.whl", hash = "sha256:372d55a6f1248fa1dd47599686980cb8fb5bbe6fcda59eab793eb657c11d8a9b", size = 37844841, upload-time = "2026-06-29T10:33:07.361Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fd/2b78036e5fbe9d5f5645bbe08a1eac7160c51243c0093963edbcf67c35d9/playwright-1.61.0-py3-none-win_amd64.whl", hash = "sha256:35c6cc4589a5d00964a59d7b3e59641e0aac0c02f15479a7af77d20f6bc79597", size = 37844846, upload-time = "2026-06-29T10:33:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/1b0f3c4ee4eb0514bc805b5c2f9a223e5b6de4f11a926f5235d51d0fc81b/playwright-1.61.0-py3-none-win_arm64.whl", hash = "sha256:e9fcbffcf557a8620fdedd92491eb59a32d18e23d6f3b4f6214b952be324fe51", size = 33955127, upload-time = "2026-06-29T10:33:14.008Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -1298,6 +1394,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/8d/d236e9c82fe315f9128885c8be3ec719f41965a1eb6b6f4b42470904cd41/pyarrow-25.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:13240f0d3dc5932ccd0bfa90cd76d835680b9d94a7661c635df4b703d40ce849", size = 28743657, upload-time = "2026-07-10T08:29:42.742Z" }, ] +[[package]] +name = "pyee" +version = "13.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -1436,7 +1544,7 @@ wheels = [ [[package]] name = "reflex" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, @@ -1468,7 +1576,7 @@ dependencies = [ [[package]] name = "reflex-base" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "packaging" }, { name = "platformdirs" }, @@ -1479,7 +1587,7 @@ dependencies = [ [[package]] name = "reflex-components-code" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1490,7 +1598,7 @@ dependencies = [ [[package]] name = "reflex-components-core" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "python-multipart" }, { name = "reflex-base" }, @@ -1503,7 +1611,7 @@ dependencies = [ [[package]] name = "reflex-components-dataeditor" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1512,7 +1620,7 @@ dependencies = [ [[package]] name = "reflex-components-gridjs" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] @@ -1520,7 +1628,7 @@ dependencies = [ [[package]] name = "reflex-components-lucide" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] @@ -1528,7 +1636,7 @@ dependencies = [ [[package]] name = "reflex-components-markdown" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-code" }, @@ -1539,7 +1647,7 @@ dependencies = [ [[package]] name = "reflex-components-moment" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] @@ -1547,7 +1655,7 @@ dependencies = [ [[package]] name = "reflex-components-plotly" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1556,7 +1664,7 @@ dependencies = [ [[package]] name = "reflex-components-radix" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1566,7 +1674,7 @@ dependencies = [ [[package]] name = "reflex-components-react-player" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1575,7 +1683,7 @@ dependencies = [ [[package]] name = "reflex-components-recharts" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, ] @@ -1583,7 +1691,7 @@ dependencies = [ [[package]] name = "reflex-components-sonner" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-lucide" }, @@ -1592,7 +1700,7 @@ dependencies = [ [[package]] name = "reflex-hosting-cli" version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=make-sio-optional#02dae5601eed0b678703a049db21634f83b625e7" } +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } dependencies = [ { name = "click" }, { name = "httpx" }, @@ -2105,6 +2213,7 @@ dev = [ { name = "aiohttp" }, { name = "hypothesis" }, { name = "pillow" }, + { name = "playwright" }, { name = "pyarrow" }, { name = "pytest" }, { name = "pyyaml" }, @@ -2119,7 +2228,7 @@ dev = [ requires-dist = [ { name = "anywidget", specifier = ">=0.9" }, { name = "numpy", specifier = ">=1.24" }, - { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=make-sio-optional" }, + { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=02dae5601eed0b678703a049db21634f83b625e7" }, ] provides-extras = ["reflex"] @@ -2129,6 +2238,7 @@ dev = [ { name = "aiohttp", specifier = ">=3.14.3" }, { name = "hypothesis", specifier = ">=6" }, { name = "pillow", specifier = ">=10" }, + { name = "playwright", specifier = "==1.61.0" }, { name = "pyarrow", specifier = ">=15" }, { name = "pytest", specifier = ">=8" }, { name = "pyyaml", specifier = ">=6" }, From 651e2c1ea3c181c2cf6de202894be767d0838fc5 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 12 Sep 2026 22:42:56 +0200 Subject: [PATCH 03/10] Bump the pinned Reflex channel commit to ea517ec4 --- docs/app/pyproject.toml | 8 ++-- docs/app/uv.lock | 87 +++++++++++++++++++------------------- pyproject.toml | 2 +- tests/test_dependencies.py | 2 +- uv.lock | 62 +++++++++++++-------------- 5 files changed, 81 insertions(+), 80 deletions(-) diff --git a/docs/app/pyproject.toml b/docs/app/pyproject.toml index dba6e95ef..7dee613e6 100644 --- a/docs/app/pyproject.toml +++ b/docs/app/pyproject.toml @@ -12,10 +12,10 @@ requires-python = ">=3.11" dependencies = [ "numpy", "pillow", - "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@02dae5601eed0b678703a049db21634f83b625e7", - "reflex-components-internal @ git+https://github.com/benedikt-bartscher/reflex.git@02dae5601eed0b678703a049db21634f83b625e7#subdirectory=packages/reflex-components-internal", - "reflex-docgen @ git+https://github.com/benedikt-bartscher/reflex.git@02dae5601eed0b678703a049db21634f83b625e7#subdirectory=packages/reflex-docgen", - "reflex-site-shared @ git+https://github.com/benedikt-bartscher/reflex.git@02dae5601eed0b678703a049db21634f83b625e7#subdirectory=packages/reflex-site-shared", + "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@ea517ec4d665aeddfab9f245aa155db0dfd5cd02", + "reflex-components-internal @ git+https://github.com/benedikt-bartscher/reflex.git@ea517ec4d665aeddfab9f245aa155db0dfd5cd02#subdirectory=packages/reflex-components-internal", + "reflex-docgen @ git+https://github.com/benedikt-bartscher/reflex.git@ea517ec4d665aeddfab9f245aa155db0dfd5cd02#subdirectory=packages/reflex-docgen", + "reflex-site-shared @ git+https://github.com/benedikt-bartscher/reflex.git@ea517ec4d665aeddfab9f245aa155db0dfd5cd02#subdirectory=packages/reflex-site-shared", "xy[reflex]", ] diff --git a/docs/app/uv.lock b/docs/app/uv.lock index 0613a7b5d..a97f6c8e5 100644 --- a/docs/app/uv.lock +++ b/docs/app/uv.lock @@ -1063,8 +1063,8 @@ wheels = [ [[package]] name = "reflex" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, @@ -1095,8 +1095,8 @@ dependencies = [ [[package]] name = "reflex-base" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "packaging" }, { name = "platformdirs" }, @@ -1106,8 +1106,8 @@ dependencies = [ [[package]] name = "reflex-components-code" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1117,8 +1117,8 @@ dependencies = [ [[package]] name = "reflex-components-core" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "python-multipart" }, { name = "reflex-base" }, @@ -1130,8 +1130,8 @@ dependencies = [ [[package]] name = "reflex-components-dataeditor" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1139,32 +1139,32 @@ dependencies = [ [[package]] name = "reflex-components-gridjs" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-internal" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex" }, ] [[package]] name = "reflex-components-lucide" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-markdown" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-code" }, @@ -1174,16 +1174,16 @@ dependencies = [ [[package]] name = "reflex-components-moment" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-plotly" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1191,8 +1191,8 @@ dependencies = [ [[package]] name = "reflex-components-radix" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1201,8 +1201,8 @@ dependencies = [ [[package]] name = "reflex-components-react-player" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1210,16 +1210,16 @@ dependencies = [ [[package]] name = "reflex-components-recharts" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-sonner" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-lucide" }, @@ -1227,8 +1227,8 @@ dependencies = [ [[package]] name = "reflex-docgen" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "griffelib" }, { name = "mistletoe" }, @@ -1239,8 +1239,8 @@ dependencies = [ [[package]] name = "reflex-hosting-cli" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "click" }, { name = "httpx" }, @@ -1251,13 +1251,13 @@ dependencies = [ [[package]] name = "reflex-integrations-docs" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Fintegrations-docs&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Fintegrations-docs&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } [[package]] name = "reflex-site-shared" -version = "0.0.3.post248.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.3.post252.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "email-validator" }, { name = "httpx" }, @@ -1632,7 +1632,7 @@ reflex = [ requires-dist = [ { name = "anywidget", specifier = ">=0.9" }, { name = "numpy", specifier = ">=1.24" }, - { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=02dae5601eed0b678703a049db21634f83b625e7" }, + { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02" }, ] provides-extras = ["reflex"] @@ -1642,6 +1642,7 @@ dev = [ { name = "aiohttp", specifier = ">=3.14.3" }, { name = "hypothesis", specifier = ">=6" }, { name = "pillow", specifier = ">=10" }, + { name = "playwright", specifier = "==1.61.0" }, { name = "pyarrow", specifier = ">=15" }, { name = "pytest", specifier = ">=8" }, { name = "pyyaml", specifier = ">=6" }, @@ -1680,10 +1681,10 @@ dev = [ requires-dist = [ { name = "numpy" }, { name = "pillow" }, - { name = "reflex", git = "https://github.com/benedikt-bartscher/reflex.git?rev=02dae5601eed0b678703a049db21634f83b625e7" }, - { name = "reflex-components-internal", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=02dae5601eed0b678703a049db21634f83b625e7" }, - { name = "reflex-docgen", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=02dae5601eed0b678703a049db21634f83b625e7" }, - { name = "reflex-site-shared", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=02dae5601eed0b678703a049db21634f83b625e7" }, + { name = "reflex", git = "https://github.com/benedikt-bartscher/reflex.git?rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02" }, + { name = "reflex-components-internal", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02" }, + { name = "reflex-docgen", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02" }, + { name = "reflex-site-shared", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02" }, { name = "xy", extras = ["reflex"], editable = "../../" }, ] diff --git a/pyproject.toml b/pyproject.toml index bfd05247e..6659723d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ dependencies = [ # with the real floor the moment channels are released (see the design doc's # Status note). reflex = [ - "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@02dae5601eed0b678703a049db21634f83b625e7", + "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@ea517ec4d665aeddfab9f245aa155db0dfd5cd02", ] [project.urls] diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py index 4261e9f08..713a6dbcc 100644 --- a/tests/test_dependencies.py +++ b/tests/test_dependencies.py @@ -18,7 +18,7 @@ # assertion, and drop `test_docs_app_pins_the_same_reflex_commit`, when # channels are released. REFLEX_REQUIREMENT = ( - "git+https://github.com/benedikt-bartscher/reflex.git@02dae5601eed0b678703a049db21634f83b625e7" + "git+https://github.com/benedikt-bartscher/reflex.git@ea517ec4d665aeddfab9f245aa155db0dfd5cd02" ) diff --git a/uv.lock b/uv.lock index 09be1a8da..14defe30b 100644 --- a/uv.lock +++ b/uv.lock @@ -1543,8 +1543,8 @@ wheels = [ [[package]] name = "reflex" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, @@ -1575,8 +1575,8 @@ dependencies = [ [[package]] name = "reflex-base" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "packaging" }, { name = "platformdirs" }, @@ -1586,8 +1586,8 @@ dependencies = [ [[package]] name = "reflex-components-code" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1597,8 +1597,8 @@ dependencies = [ [[package]] name = "reflex-components-core" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "python-multipart" }, { name = "reflex-base" }, @@ -1610,8 +1610,8 @@ dependencies = [ [[package]] name = "reflex-components-dataeditor" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1619,24 +1619,24 @@ dependencies = [ [[package]] name = "reflex-components-gridjs" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-lucide" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-markdown" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-code" }, @@ -1646,16 +1646,16 @@ dependencies = [ [[package]] name = "reflex-components-moment" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-plotly" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1663,8 +1663,8 @@ dependencies = [ [[package]] name = "reflex-components-radix" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1673,8 +1673,8 @@ dependencies = [ [[package]] name = "reflex-components-react-player" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1682,16 +1682,16 @@ dependencies = [ [[package]] name = "reflex-components-recharts" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-sonner" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-lucide" }, @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "reflex-hosting-cli" -version = "0.0.0.post3495.dev0+02dae560" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=02dae5601eed0b678703a049db21634f83b625e7#02dae5601eed0b678703a049db21634f83b625e7" } +version = "0.0.0.post3499.dev0+ea517ec4" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } dependencies = [ { name = "click" }, { name = "httpx" }, @@ -2228,7 +2228,7 @@ dev = [ requires-dist = [ { name = "anywidget", specifier = ">=0.9" }, { name = "numpy", specifier = ">=1.24" }, - { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=02dae5601eed0b678703a049db21634f83b625e7" }, + { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02" }, ] provides-extras = ["reflex"] From e8d11f603d312a4ce90ddb405b661dacf9f9727a Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 12 Sep 2026 22:57:11 +0200 Subject: [PATCH 04/10] Fail the data plane tests when the reader broke but nothing read the error --- tests/reflex_adapter/test_data_plane.py | 32 +++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/tests/reflex_adapter/test_data_plane.py b/tests/reflex_adapter/test_data_plane.py index 4030c4601..8958ac210 100644 --- a/tests/reflex_adapter/test_data_plane.py +++ b/tests/reflex_adapter/test_data_plane.py @@ -160,13 +160,19 @@ async def disconnect(self) -> None: return self.connected = False self._reader.cancel() - # Any reader failure was already handed to the waiters, so teardown - # only has to retrieve it — re-raising here would replace a test's real - # assertion failure with a confusing one from the cleanup path. - with contextlib.suppress(BaseException): + # Only the two outcomes teardown can legitimately produce: the + # cancellation just requested, and a reset from the socket going away. + # Anything else is a defect and should reach the test. + with contextlib.suppress(asyncio.CancelledError, ConnectionResetError): await self._reader await self._ws.close() await self._session.close() + if self.reader_error is not None: + # The failure was handed to every waiter, but a test that finished + # its assertions without reading again would never have popped it — + # and a broken wire must not leave the suite green. Raised after the + # socket is closed so a failing test still tears down cleanly. + raise AssertionError("the data plane reader failed") from self.reader_error async def connect_client(base_url: str, client_token: str = CLIENT_TOKEN) -> PlaneClient: @@ -389,6 +395,24 @@ async def main(): assert other.active_operations == 0 +def test_a_broken_frame_fails_the_test_even_if_nothing_reads_it(_fresh_registry): + """The harness must not let a broken wire pass as a green run. + + A malformed frame reaches waiters through the queues, but a test that has + finished its assertions never pops again — so teardown is the last place + the failure can still be noticed. + """ + + async def main(): + async with data_plane_server() as (url, _): + client = await connect_client(url) + client.reader_error = ValueError("frame did not decode") + with pytest.raises(AssertionError, match="reader failed"): + await client.disconnect() + + run(main()) + + def test_sub_delivers_spec_and_binary_columns(_fresh_registry): async def main(): token = registry.register(make_figure(64)) From c4154ca059175de82faf5a262f83e5f24c562932 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 12 Sep 2026 23:11:47 +0200 Subject: [PATCH 05/10] Bump the pinned Reflex channel commit to b8580ed1 --- docs/app/pyproject.toml | 8 ++-- docs/app/uv.lock | 86 +++++++++++++++++++------------------- pyproject.toml | 2 +- tests/test_dependencies.py | 2 +- uv.lock | 62 +++++++++++++-------------- 5 files changed, 80 insertions(+), 80 deletions(-) diff --git a/docs/app/pyproject.toml b/docs/app/pyproject.toml index 7dee613e6..6199cb51f 100644 --- a/docs/app/pyproject.toml +++ b/docs/app/pyproject.toml @@ -12,10 +12,10 @@ requires-python = ">=3.11" dependencies = [ "numpy", "pillow", - "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@ea517ec4d665aeddfab9f245aa155db0dfd5cd02", - "reflex-components-internal @ git+https://github.com/benedikt-bartscher/reflex.git@ea517ec4d665aeddfab9f245aa155db0dfd5cd02#subdirectory=packages/reflex-components-internal", - "reflex-docgen @ git+https://github.com/benedikt-bartscher/reflex.git@ea517ec4d665aeddfab9f245aa155db0dfd5cd02#subdirectory=packages/reflex-docgen", - "reflex-site-shared @ git+https://github.com/benedikt-bartscher/reflex.git@ea517ec4d665aeddfab9f245aa155db0dfd5cd02#subdirectory=packages/reflex-site-shared", + "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@b8580ed1337487671569d5964ea362296005d8fa", + "reflex-components-internal @ git+https://github.com/benedikt-bartscher/reflex.git@b8580ed1337487671569d5964ea362296005d8fa#subdirectory=packages/reflex-components-internal", + "reflex-docgen @ git+https://github.com/benedikt-bartscher/reflex.git@b8580ed1337487671569d5964ea362296005d8fa#subdirectory=packages/reflex-docgen", + "reflex-site-shared @ git+https://github.com/benedikt-bartscher/reflex.git@b8580ed1337487671569d5964ea362296005d8fa#subdirectory=packages/reflex-site-shared", "xy[reflex]", ] diff --git a/docs/app/uv.lock b/docs/app/uv.lock index a97f6c8e5..f69f5debb 100644 --- a/docs/app/uv.lock +++ b/docs/app/uv.lock @@ -1063,8 +1063,8 @@ wheels = [ [[package]] name = "reflex" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, @@ -1095,8 +1095,8 @@ dependencies = [ [[package]] name = "reflex-base" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "packaging" }, { name = "platformdirs" }, @@ -1106,8 +1106,8 @@ dependencies = [ [[package]] name = "reflex-components-code" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1117,8 +1117,8 @@ dependencies = [ [[package]] name = "reflex-components-core" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "python-multipart" }, { name = "reflex-base" }, @@ -1130,8 +1130,8 @@ dependencies = [ [[package]] name = "reflex-components-dataeditor" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1139,32 +1139,32 @@ dependencies = [ [[package]] name = "reflex-components-gridjs" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-internal" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex" }, ] [[package]] name = "reflex-components-lucide" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-markdown" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-code" }, @@ -1174,16 +1174,16 @@ dependencies = [ [[package]] name = "reflex-components-moment" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-plotly" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1191,8 +1191,8 @@ dependencies = [ [[package]] name = "reflex-components-radix" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1201,8 +1201,8 @@ dependencies = [ [[package]] name = "reflex-components-react-player" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1210,16 +1210,16 @@ dependencies = [ [[package]] name = "reflex-components-recharts" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-sonner" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-lucide" }, @@ -1227,8 +1227,8 @@ dependencies = [ [[package]] name = "reflex-docgen" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "griffelib" }, { name = "mistletoe" }, @@ -1239,8 +1239,8 @@ dependencies = [ [[package]] name = "reflex-hosting-cli" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "click" }, { name = "httpx" }, @@ -1251,13 +1251,13 @@ dependencies = [ [[package]] name = "reflex-integrations-docs" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Fintegrations-docs&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Fintegrations-docs&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } [[package]] name = "reflex-site-shared" -version = "0.0.3.post252.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.3.post253.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "email-validator" }, { name = "httpx" }, @@ -1632,7 +1632,7 @@ reflex = [ requires-dist = [ { name = "anywidget", specifier = ">=0.9" }, { name = "numpy", specifier = ">=1.24" }, - { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02" }, + { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=b8580ed1337487671569d5964ea362296005d8fa" }, ] provides-extras = ["reflex"] @@ -1681,10 +1681,10 @@ dev = [ requires-dist = [ { name = "numpy" }, { name = "pillow" }, - { name = "reflex", git = "https://github.com/benedikt-bartscher/reflex.git?rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02" }, - { name = "reflex-components-internal", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02" }, - { name = "reflex-docgen", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02" }, - { name = "reflex-site-shared", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02" }, + { name = "reflex", git = "https://github.com/benedikt-bartscher/reflex.git?rev=b8580ed1337487671569d5964ea362296005d8fa" }, + { name = "reflex-components-internal", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=b8580ed1337487671569d5964ea362296005d8fa" }, + { name = "reflex-docgen", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=b8580ed1337487671569d5964ea362296005d8fa" }, + { name = "reflex-site-shared", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=b8580ed1337487671569d5964ea362296005d8fa" }, { name = "xy", extras = ["reflex"], editable = "../../" }, ] diff --git a/pyproject.toml b/pyproject.toml index 6659723d1..2251575b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ dependencies = [ # with the real floor the moment channels are released (see the design doc's # Status note). reflex = [ - "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@ea517ec4d665aeddfab9f245aa155db0dfd5cd02", + "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@b8580ed1337487671569d5964ea362296005d8fa", ] [project.urls] diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py index 713a6dbcc..0f37b1953 100644 --- a/tests/test_dependencies.py +++ b/tests/test_dependencies.py @@ -18,7 +18,7 @@ # assertion, and drop `test_docs_app_pins_the_same_reflex_commit`, when # channels are released. REFLEX_REQUIREMENT = ( - "git+https://github.com/benedikt-bartscher/reflex.git@ea517ec4d665aeddfab9f245aa155db0dfd5cd02" + "git+https://github.com/benedikt-bartscher/reflex.git@b8580ed1337487671569d5964ea362296005d8fa" ) diff --git a/uv.lock b/uv.lock index 14defe30b..c88ae4933 100644 --- a/uv.lock +++ b/uv.lock @@ -1543,8 +1543,8 @@ wheels = [ [[package]] name = "reflex" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, @@ -1575,8 +1575,8 @@ dependencies = [ [[package]] name = "reflex-base" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "packaging" }, { name = "platformdirs" }, @@ -1586,8 +1586,8 @@ dependencies = [ [[package]] name = "reflex-components-code" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1597,8 +1597,8 @@ dependencies = [ [[package]] name = "reflex-components-core" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "python-multipart" }, { name = "reflex-base" }, @@ -1610,8 +1610,8 @@ dependencies = [ [[package]] name = "reflex-components-dataeditor" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1619,24 +1619,24 @@ dependencies = [ [[package]] name = "reflex-components-gridjs" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-lucide" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-markdown" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-code" }, @@ -1646,16 +1646,16 @@ dependencies = [ [[package]] name = "reflex-components-moment" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-plotly" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1663,8 +1663,8 @@ dependencies = [ [[package]] name = "reflex-components-radix" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1673,8 +1673,8 @@ dependencies = [ [[package]] name = "reflex-components-react-player" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1682,16 +1682,16 @@ dependencies = [ [[package]] name = "reflex-components-recharts" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-sonner" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-lucide" }, @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "reflex-hosting-cli" -version = "0.0.0.post3499.dev0+ea517ec4" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02#ea517ec4d665aeddfab9f245aa155db0dfd5cd02" } +version = "0.0.0.post3500.dev0+b8580ed1" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } dependencies = [ { name = "click" }, { name = "httpx" }, @@ -2228,7 +2228,7 @@ dev = [ requires-dist = [ { name = "anywidget", specifier = ">=0.9" }, { name = "numpy", specifier = ">=1.24" }, - { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=ea517ec4d665aeddfab9f245aa155db0dfd5cd02" }, + { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=b8580ed1337487671569d5964ea362296005d8fa" }, ] provides-extras = ["reflex"] From 875fa44c3d48ad606a4e140b47369f36cc7c71db Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 12 Sep 2026 23:35:17 +0200 Subject: [PATCH 06/10] Check upload conditions structurally, align the browser gate, and exercise the reader for real --- scripts/verify_ci_workflow.py | 40 ++++++++++++++--- tests/reflex_adapter/test_channel_browser.py | 11 +++-- tests/reflex_adapter/test_data_plane.py | 33 +++++++++++--- tests/test_verify_ci_workflow.py | 46 ++++++++++++++++++++ 4 files changed, 115 insertions(+), 15 deletions(-) diff --git a/scripts/verify_ci_workflow.py b/scripts/verify_ci_workflow.py index 8d9977eaf..e72cdc5f9 100644 --- a/scripts/verify_ci_workflow.py +++ b/scripts/verify_ci_workflow.py @@ -500,6 +500,30 @@ def _named_step_blocks(job_text: str) -> dict[str, str]: return blocks +def _require_step_condition( + errors: list[str], jobs: dict[str, str], job: str, step: str, condition: str, description: str +) -> None: + """Require one named step to carry ``if: `` as a real YAML key. + + Substring matching cannot do this job once a second step in the same job + uses the same condition: the needle is satisfied by the neighbour while the + step it was written for quietly loses its own. Reading the step's direct + ``if`` key answers the actual question, and ignores the condition appearing + in a comment (``_yaml_code_lines`` strips those). + """ + block = _named_step_blocks(jobs.get(job, "")).get(step) + if block is None: + errors.append(f"missing required CI step {step!r}") + return + values, unsafe = _step_direct_key_values(block, "if") + if unsafe or values != [condition]: + found = ", ".join(values) if values else "no direct `if` key" + errors.append( + f"CI {job} job step {step!r} missing {description}: expected a direct " + f"`if: {condition}` key, found {found}" + ) + + def _require_step_contains( errors: list[str], job_text: str, step: str, description: str, *needles: str ) -> None: @@ -815,17 +839,23 @@ def validate_ci_workflow(path: Path = DEFAULT_CI_WORKFLOW) -> list[str]: "scripts/verify_benchmark_report.py transport.json --kind transport-loopback", "scripts/check_regressions.py --scatter scatter.json --kernel kernel.json", "--transport transport.json --emit-md spec/benchmarks/metrics.md", - # Name and condition paired, not two independent needles: this job now - # has a second `if: always()` upload (the browser evidence), which a - # bare needle would satisfy on its behalf — letting *this* step lose - # its condition and stop uploading after a failed gate, silently. - "- name: Upload regression benchmark report\n if: always()", + "Upload regression benchmark report", "actions/upload-artifact@", "regression-benchmark-report", "if-no-files-found: warn", "spec/benchmarks/metrics.md", "transport.json", ) + # Both uploads in this job have to survive a failed gate — the regression + # report is the evidence for *why* it failed, the browser screenshot the + # evidence for what the chart looked like. Checked structurally, one step + # at a time, because they share a condition that a substring needle would + # let either of them borrow from the other. + for step, description in ( + ("Upload regression benchmark report", "artifact upload after a failed gate"), + ("Upload browser evidence", "screenshot upload after a failed gate"), + ): + _require_step_condition(errors, jobs, "test", step, "always()", description) test_job = jobs.get("test", "") _require_step_runs_exactly( errors, diff --git a/tests/reflex_adapter/test_channel_browser.py b/tests/reflex_adapter/test_channel_browser.py index 4e9168246..9f7a01074 100644 --- a/tests/reflex_adapter/test_channel_browser.py +++ b/tests/reflex_adapter/test_channel_browser.py @@ -16,10 +16,13 @@ import pytest # This is the only check of the single-shared-websocket claim, so a missing -# browser must not quietly turn it into a pass. `XY_REQUIRE_BROWSER=1` — what -# CI sets — makes both the Python package and the browser binary hard -# requirements; without it a bare checkout still skips. -REQUIRE_BROWSER = os.environ.get("XY_REQUIRE_BROWSER") == "1" +# browser must not quietly turn it into a pass. `XY_REQUIRE_BROWSER` — which CI +# sets — makes both the Python package and the browser binary hard +# requirements; without it a bare checkout still skips. Read for truthiness, +# not for the literal "1", so this gate opens on exactly the same values as the +# sibling one in tests/conftest.py: a mismatch there would fail every other +# browser probe while leaving this one skipping. +REQUIRE_BROWSER = bool(os.environ.get("XY_REQUIRE_BROWSER")) if not REQUIRE_BROWSER: pytest.importorskip("playwright") diff --git a/tests/reflex_adapter/test_data_plane.py b/tests/reflex_adapter/test_data_plane.py index 8958ac210..98a72d42e 100644 --- a/tests/reflex_adapter/test_data_plane.py +++ b/tests/reflex_adapter/test_data_plane.py @@ -16,6 +16,7 @@ import contextlib import json import socket +import sys import threading from types import SimpleNamespace from typing import Any, Optional, TypedDict @@ -395,18 +396,38 @@ async def main(): assert other.active_operations == 0 -def test_a_broken_frame_fails_the_test_even_if_nothing_reads_it(_fresh_registry): +def test_a_broken_frame_fails_the_test_even_if_nothing_reads_it(_fresh_registry, monkeypatch): """The harness must not let a broken wire pass as a green run. - A malformed frame reaches waiters through the queues, but a test that has - finished its assertions never pops again — so teardown is the last place - the failure can still be noticed. + Both halves have to hold: the reader has to *record* a decode failure + rather than die quietly, and teardown has to raise it. A test that has + finished its assertions never pops a queue again, so teardown is the last + place the failure can be noticed — and a reader that silently stopped + recording would make every other test in this file vacuous. + + The frame really is decoded here; only the decoder is replaced, because a + real server cannot be made to emit a frame its own encoder would refuse. """ + def explode(frame): + raise ValueError("frame did not decode") + async def main(): + token = registry.register(make_figure(8)) async with data_plane_server() as (url, _): - client = await connect_client(url) - client.reader_error = ValueError("frame did not decode") + client = await connect_client(url) # opened before the decoder breaks + # Patched on this module object: the reader resolves the decoder + # as a global here, and a dotted path would patch a second import + # of this file rather than the one running. + monkeypatch.setattr(sys.modules[__name__], "decode_channel_frame", explode) + await client.emit("sub", {"fig": token, "mid": "m1"}) + + # The error reaches the waiters rather than stranding them. + with pytest.raises(AssertionError, match="reader failed"): + await Collector.next(client.payloads) + assert isinstance(client.reader_error, ValueError) + + # And teardown raises it too, for the test that never reads again. with pytest.raises(AssertionError, match="reader failed"): await client.disconnect() diff --git a/tests/test_verify_ci_workflow.py b/tests/test_verify_ci_workflow.py index 929371093..f6fd44489 100644 --- a/tests/test_verify_ci_workflow.py +++ b/tests/test_verify_ci_workflow.py @@ -1321,6 +1321,52 @@ def test_ci_workflow_rejects_regression_upload_that_skips_after_failures( assert any("test job" in error and "if: always()" in error for error in errors) +def test_ci_workflow_rejects_browser_evidence_upload_that_skips_after_failures( + tmp_path: Path, +) -> None: + """The screenshot is only evidence if it survives the run that failed.""" + workflow = Path(".github/workflows/ci.yml").read_text(encoding="utf-8") + path = tmp_path / "ci.yml" + path.write_text( + workflow.replace( + " - name: Upload browser evidence\n if: always()\n", + " - name: Upload browser evidence\n", + ), + encoding="utf-8", + ) + + errors = verify_ci_workflow.validate_workflow(path) + + assert any("Upload browser evidence" in error and "if: always()" in error for error in errors) + + +def test_ci_workflow_rejects_upload_condition_that_only_survives_in_a_comment( + tmp_path: Path, +) -> None: + """The condition is read as a YAML key, not matched as text. + + Two steps in this job share `if: always()`, so a substring needle can be + satisfied by the neighbour — or by the condition lingering in a comment + after the real key is gone — while the upload silently stops running. + """ + workflow = Path(".github/workflows/ci.yml").read_text(encoding="utf-8") + path = tmp_path / "ci.yml" + path.write_text( + workflow.replace( + " - name: Upload regression benchmark report\n if: always()\n", + " - name: Upload regression benchmark report\n # if: always()\n", + ), + encoding="utf-8", + ) + + errors = verify_ci_workflow.validate_workflow(path) + + assert any( + "Upload regression benchmark report" in error and "if: always()" in error + for error in errors + ) + + def test_ci_workflow_rejects_missing_wheel_upload(tmp_path: Path) -> None: workflow = Path(".github/workflows/ci.yml").read_text(encoding="utf-8") path = tmp_path / "ci.yml" From ebcd09815d36487b239989ced3a1394ea6b03461 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sat, 12 Sep 2026 23:43:32 +0200 Subject: [PATCH 07/10] Keep the browser harness app in a self-deleting temp dir --- tests/reflex_adapter/test_channel_browser.py | 21 +++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/reflex_adapter/test_channel_browser.py b/tests/reflex_adapter/test_channel_browser.py index 9f7a01074..173de8d2a 100644 --- a/tests/reflex_adapter/test_channel_browser.py +++ b/tests/reflex_adapter/test_channel_browser.py @@ -10,6 +10,7 @@ import json import os +import tempfile from collections.abc import Generator from pathlib import Path @@ -94,21 +95,23 @@ def _fresh_registry(): @pytest.fixture(scope="module") -def chart_app( - tmp_path_factory: pytest.TempPathFactory, -) -> Generator[AppHarness, None, None]: +def chart_app() -> Generator[AppHarness, None, None]: """Build and serve the chart app. - Args: - tmp_path_factory: pytest fixture for creating temporary directories. + The root is a self-deleting temp dir rather than `tmp_path_factory`: a + compiled Reflex app is ~200 MB of `node_modules` and `.web`, and pytest + keeps its last three runs, so borrowing that retention would leave most of + a gigabyte behind per run — enough to fill a tmpfs `/tmp` after an + afternoon of local runs and make every *other* browser probe fail for want + of space. CI gets one run per job either way. Yields: The running harness. """ - with AppHarness.create( - root=tmp_path_factory.mktemp("channel_chart_app"), - app_source=ChannelChartApp, - ) as harness: + with ( + tempfile.TemporaryDirectory(prefix="xy-channel-chart-app-") as root, + AppHarness.create(root=Path(root), app_source=ChannelChartApp) as harness, + ): assert harness.app_instance is not None, "app is not running" yield harness From 62ebcf3f445acbacfa0912e0208361da783e5d47 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 13 Sep 2026 00:26:56 +0200 Subject: [PATCH 08/10] Reject duplicate workflow step names; bump the pinned Reflex commit to 46cecf59 --- docs/app/pyproject.toml | 8 +-- docs/app/uv.lock | 86 ++++++++++++++++---------------- pyproject.toml | 2 +- scripts/verify_ci_workflow.py | 46 ++++++++++++++++- tests/test_dependencies.py | 2 +- tests/test_verify_ci_workflow.py | 30 +++++++++++ uv.lock | 62 +++++++++++------------ 7 files changed, 154 insertions(+), 82 deletions(-) diff --git a/docs/app/pyproject.toml b/docs/app/pyproject.toml index 6199cb51f..64db70b11 100644 --- a/docs/app/pyproject.toml +++ b/docs/app/pyproject.toml @@ -12,10 +12,10 @@ requires-python = ">=3.11" dependencies = [ "numpy", "pillow", - "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@b8580ed1337487671569d5964ea362296005d8fa", - "reflex-components-internal @ git+https://github.com/benedikt-bartscher/reflex.git@b8580ed1337487671569d5964ea362296005d8fa#subdirectory=packages/reflex-components-internal", - "reflex-docgen @ git+https://github.com/benedikt-bartscher/reflex.git@b8580ed1337487671569d5964ea362296005d8fa#subdirectory=packages/reflex-docgen", - "reflex-site-shared @ git+https://github.com/benedikt-bartscher/reflex.git@b8580ed1337487671569d5964ea362296005d8fa#subdirectory=packages/reflex-site-shared", + "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@46cecf595bb48a6200bb0047b60acc19306133b0", + "reflex-components-internal @ git+https://github.com/benedikt-bartscher/reflex.git@46cecf595bb48a6200bb0047b60acc19306133b0#subdirectory=packages/reflex-components-internal", + "reflex-docgen @ git+https://github.com/benedikt-bartscher/reflex.git@46cecf595bb48a6200bb0047b60acc19306133b0#subdirectory=packages/reflex-docgen", + "reflex-site-shared @ git+https://github.com/benedikt-bartscher/reflex.git@46cecf595bb48a6200bb0047b60acc19306133b0#subdirectory=packages/reflex-site-shared", "xy[reflex]", ] diff --git a/docs/app/uv.lock b/docs/app/uv.lock index f69f5debb..82b1534d4 100644 --- a/docs/app/uv.lock +++ b/docs/app/uv.lock @@ -1063,8 +1063,8 @@ wheels = [ [[package]] name = "reflex" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, @@ -1095,8 +1095,8 @@ dependencies = [ [[package]] name = "reflex-base" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "packaging" }, { name = "platformdirs" }, @@ -1106,8 +1106,8 @@ dependencies = [ [[package]] name = "reflex-components-code" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1117,8 +1117,8 @@ dependencies = [ [[package]] name = "reflex-components-core" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "python-multipart" }, { name = "reflex-base" }, @@ -1130,8 +1130,8 @@ dependencies = [ [[package]] name = "reflex-components-dataeditor" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1139,32 +1139,32 @@ dependencies = [ [[package]] name = "reflex-components-gridjs" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-internal" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex" }, ] [[package]] name = "reflex-components-lucide" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-markdown" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-code" }, @@ -1174,16 +1174,16 @@ dependencies = [ [[package]] name = "reflex-components-moment" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-plotly" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1191,8 +1191,8 @@ dependencies = [ [[package]] name = "reflex-components-radix" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1201,8 +1201,8 @@ dependencies = [ [[package]] name = "reflex-components-react-player" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1210,16 +1210,16 @@ dependencies = [ [[package]] name = "reflex-components-recharts" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-sonner" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-lucide" }, @@ -1227,8 +1227,8 @@ dependencies = [ [[package]] name = "reflex-docgen" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "griffelib" }, { name = "mistletoe" }, @@ -1239,8 +1239,8 @@ dependencies = [ [[package]] name = "reflex-hosting-cli" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "click" }, { name = "httpx" }, @@ -1251,13 +1251,13 @@ dependencies = [ [[package]] name = "reflex-integrations-docs" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Fintegrations-docs&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Fintegrations-docs&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } [[package]] name = "reflex-site-shared" -version = "0.0.3.post253.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.3.post254.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "email-validator" }, { name = "httpx" }, @@ -1632,7 +1632,7 @@ reflex = [ requires-dist = [ { name = "anywidget", specifier = ">=0.9" }, { name = "numpy", specifier = ">=1.24" }, - { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=b8580ed1337487671569d5964ea362296005d8fa" }, + { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=46cecf595bb48a6200bb0047b60acc19306133b0" }, ] provides-extras = ["reflex"] @@ -1681,10 +1681,10 @@ dev = [ requires-dist = [ { name = "numpy" }, { name = "pillow" }, - { name = "reflex", git = "https://github.com/benedikt-bartscher/reflex.git?rev=b8580ed1337487671569d5964ea362296005d8fa" }, - { name = "reflex-components-internal", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=b8580ed1337487671569d5964ea362296005d8fa" }, - { name = "reflex-docgen", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=b8580ed1337487671569d5964ea362296005d8fa" }, - { name = "reflex-site-shared", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=b8580ed1337487671569d5964ea362296005d8fa" }, + { name = "reflex", git = "https://github.com/benedikt-bartscher/reflex.git?rev=46cecf595bb48a6200bb0047b60acc19306133b0" }, + { name = "reflex-components-internal", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=46cecf595bb48a6200bb0047b60acc19306133b0" }, + { name = "reflex-docgen", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=46cecf595bb48a6200bb0047b60acc19306133b0" }, + { name = "reflex-site-shared", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=46cecf595bb48a6200bb0047b60acc19306133b0" }, { name = "xy", extras = ["reflex"], editable = "../../" }, ] diff --git a/pyproject.toml b/pyproject.toml index 2251575b7..6c8301a65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ dependencies = [ # with the real floor the moment channels are released (see the design doc's # Status note). reflex = [ - "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@b8580ed1337487671569d5964ea362296005d8fa", + "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@46cecf595bb48a6200bb0047b60acc19306133b0", ] [project.urls] diff --git a/scripts/verify_ci_workflow.py b/scripts/verify_ci_workflow.py index e72cdc5f9..4f7500095 100644 --- a/scripts/verify_ci_workflow.py +++ b/scripts/verify_ci_workflow.py @@ -489,17 +489,57 @@ def _step_sequence_blocks(job_text: str) -> list[str]: return ["\n".join(block) for block in blocks] +def _step_names(job_text: str) -> list[str]: + """Return every step name in the job's ``steps:`` sequence, in order.""" + names: list[str] = [] + for block in _step_sequence_blocks(job_text): + first = _strip_yaml_comment(block.splitlines()[0]) + match = re.match(r"^ - name:\s*(.+?)\s*$", first) + if match is not None: + names.append(match.group(1)) + return names + + def _named_step_blocks(job_text: str) -> dict[str, str]: - """Return named blocks from the job's actual ``steps:`` sequence.""" + """Return *unambiguously* named blocks from the job's ``steps:`` sequence. + + Actions permits two steps to share a name, and a dict keyed by name would + silently keep the last one — so a step that lost a required `if` or `run` + could be vouched for by a later namesake that still has it. A duplicated + name is therefore dropped entirely: every `_require_step_*` lookup then + reports the step as missing instead of validating the wrong block. + `_require_unique_step_names` turns that into a message naming the clash. + """ blocks: dict[str, str] = {} + duplicated = {name for name in _step_names(job_text) if _step_names(job_text).count(name) > 1} for block in _step_sequence_blocks(job_text): first = _strip_yaml_comment(block.splitlines()[0]) match = re.match(r"^ - name:\s*(.+?)\s*$", first) - if match is not None: + if match is not None and match.group(1) not in duplicated: blocks[match.group(1)] = block return blocks +def _require_unique_step_names( + errors: list[str], jobs: dict[str, str], workflow_label: str +) -> None: + """Every step name in a job must identify exactly one step. + + Structural gates address steps by name, so a repeated name is an ambiguity + the gates cannot resolve: the duplicate may carry the condition or command + the original was required to have, leaving the real step unchecked. + """ + for job, block in jobs.items(): + names = _step_names(block) + duplicated = sorted({name for name in names if names.count(name) > 1}) + if duplicated: + errors.append( + f"{workflow_label} {job} job repeats step names {duplicated} — the " + "structural gates address steps by name, so a namesake can satisfy a " + "check the step it shadows no longer passes" + ) + + def _require_step_condition( errors: list[str], jobs: dict[str, str], job: str, step: str, condition: str, description: str ) -> None: @@ -759,6 +799,7 @@ def validate_ci_workflow(path: Path = DEFAULT_CI_WORKFLOW) -> list[str]: errors.append("CI workflow must not set shell-init environment variables") _require_docs_spec_pr_paths_ignored(errors, text, "CI") _require_unshallow_checkouts(errors, text, "CI") + _require_unique_step_names(errors, jobs, "CI") missing_jobs = sorted(REQUIRED_CI_JOBS - set(jobs)) if missing_jobs: errors.append(f"CI workflow missing required jobs: {missing_jobs}") @@ -1174,6 +1215,7 @@ def validate_codspeed_workflow(path: Path = DEFAULT_CODSPEED_WORKFLOW) -> list[s errors: list[str] = [] _require_unique_workflow_structure(errors, text, "CodSpeed", REQUIRED_CODSPEED_JOBS) _require_docs_spec_pr_paths_ignored(errors, text, "CodSpeed") + _require_unique_step_names(errors, jobs, "CodSpeed") _require_trigger_with_direct_option( errors, text, diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py index 0f37b1953..b5d9173e3 100644 --- a/tests/test_dependencies.py +++ b/tests/test_dependencies.py @@ -18,7 +18,7 @@ # assertion, and drop `test_docs_app_pins_the_same_reflex_commit`, when # channels are released. REFLEX_REQUIREMENT = ( - "git+https://github.com/benedikt-bartscher/reflex.git@b8580ed1337487671569d5964ea362296005d8fa" + "git+https://github.com/benedikt-bartscher/reflex.git@46cecf595bb48a6200bb0047b60acc19306133b0" ) diff --git a/tests/test_verify_ci_workflow.py b/tests/test_verify_ci_workflow.py index f6fd44489..b22ce34b5 100644 --- a/tests/test_verify_ci_workflow.py +++ b/tests/test_verify_ci_workflow.py @@ -1367,6 +1367,36 @@ def test_ci_workflow_rejects_upload_condition_that_only_survives_in_a_comment( ) +def test_ci_workflow_rejects_a_same_named_step_vouching_for_its_shadow( + tmp_path: Path, +) -> None: + """A namesake must not satisfy a check the step it shadows no longer passes. + + Actions allows two steps to share a name, so addressing steps by name is + ambiguous: strip the condition from the real upload, add a conditioned + step of the same name after it, and a last-one-wins lookup would validate + the decoy while the artifact silently stops uploading after a failed gate. + """ + workflow = Path(".github/workflows/ci.yml").read_text(encoding="utf-8") + jobs = verify_ci_workflow._job_blocks(workflow) + real = verify_ci_workflow._named_step_blocks(jobs["test"])["Upload regression benchmark report"] + decoy = ( + " - name: Upload regression benchmark report\n" + " if: always()\n" + " run: echo decoy\n" + ) + path = tmp_path / "ci.yml" + path.write_text( + workflow.replace(real, real.replace(" if: always()\n", "", 1) + decoy), + encoding="utf-8", + ) + + errors = verify_ci_workflow.validate_workflow(path) + + assert any("repeats step names" in error for error in errors) + assert any("missing required CI step" in error for error in errors) + + def test_ci_workflow_rejects_missing_wheel_upload(tmp_path: Path) -> None: workflow = Path(".github/workflows/ci.yml").read_text(encoding="utf-8") path = tmp_path / "ci.yml" diff --git a/uv.lock b/uv.lock index c88ae4933..6d288d699 100644 --- a/uv.lock +++ b/uv.lock @@ -1543,8 +1543,8 @@ wheels = [ [[package]] name = "reflex" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, @@ -1575,8 +1575,8 @@ dependencies = [ [[package]] name = "reflex-base" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "packaging" }, { name = "platformdirs" }, @@ -1586,8 +1586,8 @@ dependencies = [ [[package]] name = "reflex-components-code" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1597,8 +1597,8 @@ dependencies = [ [[package]] name = "reflex-components-core" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "python-multipart" }, { name = "reflex-base" }, @@ -1610,8 +1610,8 @@ dependencies = [ [[package]] name = "reflex-components-dataeditor" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1619,24 +1619,24 @@ dependencies = [ [[package]] name = "reflex-components-gridjs" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-lucide" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-markdown" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-code" }, @@ -1646,16 +1646,16 @@ dependencies = [ [[package]] name = "reflex-components-moment" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-plotly" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1663,8 +1663,8 @@ dependencies = [ [[package]] name = "reflex-components-radix" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1673,8 +1673,8 @@ dependencies = [ [[package]] name = "reflex-components-react-player" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1682,16 +1682,16 @@ dependencies = [ [[package]] name = "reflex-components-recharts" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-sonner" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-lucide" }, @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "reflex-hosting-cli" -version = "0.0.0.post3500.dev0+b8580ed1" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=b8580ed1337487671569d5964ea362296005d8fa#b8580ed1337487671569d5964ea362296005d8fa" } +version = "0.0.0.post3501.dev0+46cecf59" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } dependencies = [ { name = "click" }, { name = "httpx" }, @@ -2228,7 +2228,7 @@ dev = [ requires-dist = [ { name = "anywidget", specifier = ">=0.9" }, { name = "numpy", specifier = ">=1.24" }, - { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=b8580ed1337487671569d5964ea362296005d8fa" }, + { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=46cecf595bb48a6200bb0047b60acc19306133b0" }, ] provides-extras = ["reflex"] From 353f32fc215339ad590006b4a5aee8bc30776507 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 13 Sep 2026 00:51:30 +0200 Subject: [PATCH 09/10] Normalize workflow step names before comparing them --- scripts/verify_ci_workflow.py | 56 +++++++++++++++++++++++++++----- tests/test_verify_ci_workflow.py | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 8 deletions(-) diff --git a/scripts/verify_ci_workflow.py b/scripts/verify_ci_workflow.py index 4f7500095..899bff9f3 100644 --- a/scripts/verify_ci_workflow.py +++ b/scripts/verify_ci_workflow.py @@ -489,14 +489,35 @@ def _step_sequence_blocks(job_text: str) -> list[str]: return ["\n".join(block) for block in blocks] -def _step_names(job_text: str) -> list[str]: - """Return every step name in the job's ``steps:`` sequence, in order.""" - names: list[str] = [] +def _decode_step_name(raw: str) -> tuple[Optional[str], bool]: + """Return a step's name as YAML reads it, and whether the spelling is safe. + + Two steps named ``Upload it`` and ``"Upload it"`` are the same step name to + Actions but different strings to a lexer, so comparing raw text would let a + quoted namesake slip past both the duplicate check and the by-name lookup. + Quoted spellings are therefore normalized exactly as mapping keys are. + Anything a line-local lexer cannot resolve — a block scalar, node + properties, an empty name — fails closed instead of being guessed at. + + Args: + raw: The text following ``name:`` on the step's first line. + + Returns: + The decoded name, or None with a True flag when it is unsupported. + """ + if not raw or raw[0] in {"|", ">", "&", "!", "*"}: + return None, True + return _decode_yaml_key(raw) + + +def _step_names(job_text: str) -> list[tuple[Optional[str], bool]]: + """Return ``(name, unsafe)`` for every named step, in sequence order.""" + names: list[tuple[Optional[str], bool]] = [] for block in _step_sequence_blocks(job_text): first = _strip_yaml_comment(block.splitlines()[0]) match = re.match(r"^ - name:\s*(.+?)\s*$", first) if match is not None: - names.append(match.group(1)) + names.append(_decode_step_name(match.group(1))) return names @@ -509,14 +530,26 @@ def _named_step_blocks(job_text: str) -> dict[str, str]: name is therefore dropped entirely: every `_require_step_*` lookup then reports the step as missing instead of validating the wrong block. `_require_unique_step_names` turns that into a message naming the clash. + + Names are compared after decoding, so quoting one of a pair cannot hide the + collision, and a name whose spelling cannot be decoded is dropped too. """ + decoded = _step_names(job_text) + unusable = { + name + for name, unsafe in decoded + if unsafe or [candidate for candidate, _ in decoded].count(name) > 1 + } blocks: dict[str, str] = {} - duplicated = {name for name in _step_names(job_text) if _step_names(job_text).count(name) > 1} for block in _step_sequence_blocks(job_text): first = _strip_yaml_comment(block.splitlines()[0]) match = re.match(r"^ - name:\s*(.+?)\s*$", first) - if match is not None and match.group(1) not in duplicated: - blocks[match.group(1)] = block + if match is None: + continue + name, unsafe = _decode_step_name(match.group(1)) + if unsafe or name is None or name in unusable: + continue + blocks[name] = block return blocks @@ -530,7 +563,14 @@ def _require_unique_step_names( the original was required to have, leaving the real step unchecked. """ for job, block in jobs.items(): - names = _step_names(block) + decoded = _step_names(block) + if any(unsafe for _name, unsafe in decoded): + errors.append( + f"{workflow_label} {job} job has a step name this checker cannot resolve " + "(block scalar, node property, or unsupported escape) — the structural " + "gates address steps by name and cannot verify one they cannot read" + ) + names = [name for name, unsafe in decoded if not unsafe and name is not None] duplicated = sorted({name for name in names if names.count(name) > 1}) if duplicated: errors.append( diff --git a/tests/test_verify_ci_workflow.py b/tests/test_verify_ci_workflow.py index b22ce34b5..2ba863d20 100644 --- a/tests/test_verify_ci_workflow.py +++ b/tests/test_verify_ci_workflow.py @@ -1397,6 +1397,62 @@ def test_ci_workflow_rejects_a_same_named_step_vouching_for_its_shadow( assert any("missing required CI step" in error for error in errors) +def test_ci_workflow_rejects_a_quoted_namesake_shadowing_a_step(tmp_path: Path) -> None: + """`"Upload it"` and `Upload it` are one name to Actions, two to a lexer. + + Comparing raw text would miss the collision entirely: the duplicate check + would not fire and the by-name lookup would find whichever spelling the + gate happens to ask for, leaving its namesake unguarded. + """ + workflow = Path(".github/workflows/ci.yml").read_text(encoding="utf-8") + name = "Upload regression benchmark report" + jobs = verify_ci_workflow._job_blocks(workflow) + real = verify_ci_workflow._named_step_blocks(jobs["test"])[name] + quoted_real = real.replace(f"- name: {name}", f'- name: "{name}"', 1) + decoy = f" - name: {name}\n if: always()\n run: echo decoy\n" + path = tmp_path / "ci.yml" + path.write_text( + workflow.replace(real, quoted_real.replace(" if: always()\n", "", 1) + decoy), + encoding="utf-8", + ) + + errors = verify_ci_workflow.validate_workflow(path) + + assert any("repeats step names" in error and name in error for error in errors) + + +def test_ci_workflow_rejects_a_step_name_it_cannot_resolve(tmp_path: Path) -> None: + """A name spelled as a block scalar is unreadable to a line-local lexer. + + Guessing at it would be worse than refusing: the gates address steps by + name, so a name they cannot read is a step they cannot verify. + """ + workflow = Path(".github/workflows/ci.yml").read_text(encoding="utf-8") + path = tmp_path / "ci.yml" + path.write_text( + workflow.replace( + "- name: Upload regression benchmark report", + "- name: >-\n Upload regression benchmark report", + 1, + ), + encoding="utf-8", + ) + + errors = verify_ci_workflow.validate_workflow(path) + + assert any("cannot resolve" in error for error in errors) + + +def test_ci_workflow_accepts_a_quoted_step_name_with_no_namesake(tmp_path: Path) -> None: + """Normalizing names must not make ordinary quoting a failure.""" + workflow = Path(".github/workflows/ci.yml").read_text(encoding="utf-8") + name = "Upload regression benchmark report" + path = tmp_path / "ci.yml" + path.write_text(workflow.replace(f"- name: {name}", f'- name: "{name}"', 1), encoding="utf-8") + + assert verify_ci_workflow.validate_workflow(path) == [] + + def test_ci_workflow_rejects_missing_wheel_upload(tmp_path: Path) -> None: workflow = Path(".github/workflows/ci.yml").read_text(encoding="utf-8") path = tmp_path / "ci.yml" From 16ec8fec0e03c0d72cd62e2f9fcf7cc782b9e4ef Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 13 Sep 2026 01:17:01 +0200 Subject: [PATCH 10/10] Bump the pinned Reflex channel commit to 609b2f51 --- docs/app/pyproject.toml | 8 ++-- docs/app/uv.lock | 86 +++++++++++++++++++------------------- pyproject.toml | 2 +- tests/test_dependencies.py | 2 +- uv.lock | 62 +++++++++++++-------------- 5 files changed, 80 insertions(+), 80 deletions(-) diff --git a/docs/app/pyproject.toml b/docs/app/pyproject.toml index 64db70b11..8579b24f2 100644 --- a/docs/app/pyproject.toml +++ b/docs/app/pyproject.toml @@ -12,10 +12,10 @@ requires-python = ">=3.11" dependencies = [ "numpy", "pillow", - "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@46cecf595bb48a6200bb0047b60acc19306133b0", - "reflex-components-internal @ git+https://github.com/benedikt-bartscher/reflex.git@46cecf595bb48a6200bb0047b60acc19306133b0#subdirectory=packages/reflex-components-internal", - "reflex-docgen @ git+https://github.com/benedikt-bartscher/reflex.git@46cecf595bb48a6200bb0047b60acc19306133b0#subdirectory=packages/reflex-docgen", - "reflex-site-shared @ git+https://github.com/benedikt-bartscher/reflex.git@46cecf595bb48a6200bb0047b60acc19306133b0#subdirectory=packages/reflex-site-shared", + "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@609b2f515fc9d00e9a63a056d00a7782c77323b0", + "reflex-components-internal @ git+https://github.com/benedikt-bartscher/reflex.git@609b2f515fc9d00e9a63a056d00a7782c77323b0#subdirectory=packages/reflex-components-internal", + "reflex-docgen @ git+https://github.com/benedikt-bartscher/reflex.git@609b2f515fc9d00e9a63a056d00a7782c77323b0#subdirectory=packages/reflex-docgen", + "reflex-site-shared @ git+https://github.com/benedikt-bartscher/reflex.git@609b2f515fc9d00e9a63a056d00a7782c77323b0#subdirectory=packages/reflex-site-shared", "xy[reflex]", ] diff --git a/docs/app/uv.lock b/docs/app/uv.lock index 82b1534d4..af72b9a47 100644 --- a/docs/app/uv.lock +++ b/docs/app/uv.lock @@ -1063,8 +1063,8 @@ wheels = [ [[package]] name = "reflex" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, @@ -1095,8 +1095,8 @@ dependencies = [ [[package]] name = "reflex-base" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "packaging" }, { name = "platformdirs" }, @@ -1106,8 +1106,8 @@ dependencies = [ [[package]] name = "reflex-components-code" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1117,8 +1117,8 @@ dependencies = [ [[package]] name = "reflex-components-core" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "python-multipart" }, { name = "reflex-base" }, @@ -1130,8 +1130,8 @@ dependencies = [ [[package]] name = "reflex-components-dataeditor" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1139,32 +1139,32 @@ dependencies = [ [[package]] name = "reflex-components-gridjs" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-internal" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex" }, ] [[package]] name = "reflex-components-lucide" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-markdown" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-code" }, @@ -1174,16 +1174,16 @@ dependencies = [ [[package]] name = "reflex-components-moment" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-plotly" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1191,8 +1191,8 @@ dependencies = [ [[package]] name = "reflex-components-radix" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1201,8 +1201,8 @@ dependencies = [ [[package]] name = "reflex-components-react-player" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1210,16 +1210,16 @@ dependencies = [ [[package]] name = "reflex-components-recharts" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-sonner" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-lucide" }, @@ -1227,8 +1227,8 @@ dependencies = [ [[package]] name = "reflex-docgen" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "griffelib" }, { name = "mistletoe" }, @@ -1239,8 +1239,8 @@ dependencies = [ [[package]] name = "reflex-hosting-cli" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "click" }, { name = "httpx" }, @@ -1251,13 +1251,13 @@ dependencies = [ [[package]] name = "reflex-integrations-docs" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Fintegrations-docs&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Fintegrations-docs&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } [[package]] name = "reflex-site-shared" -version = "0.0.3.post254.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.3.post255.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "email-validator" }, { name = "httpx" }, @@ -1632,7 +1632,7 @@ reflex = [ requires-dist = [ { name = "anywidget", specifier = ">=0.9" }, { name = "numpy", specifier = ">=1.24" }, - { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=46cecf595bb48a6200bb0047b60acc19306133b0" }, + { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=609b2f515fc9d00e9a63a056d00a7782c77323b0" }, ] provides-extras = ["reflex"] @@ -1681,10 +1681,10 @@ dev = [ requires-dist = [ { name = "numpy" }, { name = "pillow" }, - { name = "reflex", git = "https://github.com/benedikt-bartscher/reflex.git?rev=46cecf595bb48a6200bb0047b60acc19306133b0" }, - { name = "reflex-components-internal", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=46cecf595bb48a6200bb0047b60acc19306133b0" }, - { name = "reflex-docgen", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=46cecf595bb48a6200bb0047b60acc19306133b0" }, - { name = "reflex-site-shared", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=46cecf595bb48a6200bb0047b60acc19306133b0" }, + { name = "reflex", git = "https://github.com/benedikt-bartscher/reflex.git?rev=609b2f515fc9d00e9a63a056d00a7782c77323b0" }, + { name = "reflex-components-internal", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-internal&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0" }, + { name = "reflex-docgen", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-docgen&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0" }, + { name = "reflex-site-shared", git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-site-shared&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0" }, { name = "xy", extras = ["reflex"], editable = "../../" }, ] diff --git a/pyproject.toml b/pyproject.toml index 6c8301a65..be4daba09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ dependencies = [ # with the real floor the moment channels are released (see the design doc's # Status note). reflex = [ - "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@46cecf595bb48a6200bb0047b60acc19306133b0", + "reflex @ git+https://github.com/benedikt-bartscher/reflex.git@609b2f515fc9d00e9a63a056d00a7782c77323b0", ] [project.urls] diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py index b5d9173e3..1a9e047bb 100644 --- a/tests/test_dependencies.py +++ b/tests/test_dependencies.py @@ -18,7 +18,7 @@ # assertion, and drop `test_docs_app_pins_the_same_reflex_commit`, when # channels are released. REFLEX_REQUIREMENT = ( - "git+https://github.com/benedikt-bartscher/reflex.git@46cecf595bb48a6200bb0047b60acc19306133b0" + "git+https://github.com/benedikt-bartscher/reflex.git@609b2f515fc9d00e9a63a056d00a7782c77323b0" ) diff --git a/uv.lock b/uv.lock index 6d288d699..ad78d8b0d 100644 --- a/uv.lock +++ b/uv.lock @@ -1543,8 +1543,8 @@ wheels = [ [[package]] name = "reflex" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, @@ -1575,8 +1575,8 @@ dependencies = [ [[package]] name = "reflex-base" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-base&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "packaging" }, { name = "platformdirs" }, @@ -1586,8 +1586,8 @@ dependencies = [ [[package]] name = "reflex-components-code" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-code&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1597,8 +1597,8 @@ dependencies = [ [[package]] name = "reflex-components-core" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-core&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "python-multipart" }, { name = "reflex-base" }, @@ -1610,8 +1610,8 @@ dependencies = [ [[package]] name = "reflex-components-dataeditor" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-dataeditor&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1619,24 +1619,24 @@ dependencies = [ [[package]] name = "reflex-components-gridjs" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-gridjs&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-lucide" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-lucide&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-markdown" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-markdown&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-code" }, @@ -1646,16 +1646,16 @@ dependencies = [ [[package]] name = "reflex-components-moment" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-moment&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-plotly" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-plotly&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1663,8 +1663,8 @@ dependencies = [ [[package]] name = "reflex-components-radix" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-radix&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1673,8 +1673,8 @@ dependencies = [ [[package]] name = "reflex-components-react-player" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-react-player&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1682,16 +1682,16 @@ dependencies = [ [[package]] name = "reflex-components-recharts" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-recharts&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-sonner" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-components-sonner&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-lucide" }, @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "reflex-hosting-cli" -version = "0.0.0.post3501.dev0+46cecf59" -source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=46cecf595bb48a6200bb0047b60acc19306133b0#46cecf595bb48a6200bb0047b60acc19306133b0" } +version = "0.0.0.post3502.dev0+609b2f51" +source = { git = "https://github.com/benedikt-bartscher/reflex.git?subdirectory=packages%2Freflex-hosting-cli&rev=609b2f515fc9d00e9a63a056d00a7782c77323b0#609b2f515fc9d00e9a63a056d00a7782c77323b0" } dependencies = [ { name = "click" }, { name = "httpx" }, @@ -2228,7 +2228,7 @@ dev = [ requires-dist = [ { name = "anywidget", specifier = ">=0.9" }, { name = "numpy", specifier = ">=1.24" }, - { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=46cecf595bb48a6200bb0047b60acc19306133b0" }, + { name = "reflex", marker = "extra == 'reflex'", git = "https://github.com/benedikt-bartscher/reflex.git?rev=609b2f515fc9d00e9a63a056d00a7782c77323b0" }, ] provides-extras = ["reflex"]