Skip to content

Commit 4a48fec

Browse files
authored
Add basic websockets support (#1027)
1 parent 0278d91 commit 4a48fec

File tree

20 files changed

+661
-414
lines changed

20 files changed

+661
-414
lines changed

.github/workflows/ci.yml

+8
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ jobs:
7272
name: playwright-report-with-concurrent-updates-enabled
7373
path: playwright-report-with-concurrent-updates-enabled/
7474
retention-days: 30
75+
- name: Run playwright test with websockets enabled
76+
run: MESOP_WEBSOCKETS_ENABLED=true PLAYWRIGHT_HTML_OUTPUT_DIR=playwright-report-with-websockets-enabled yarn playwright test
77+
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
78+
if: always()
79+
with:
80+
name: playwright-report-with-websockets-enabled
81+
path: playwright-report-with-websockets-enabled/
82+
retention-days: 30
7583
- name: Run playwright test with memory state session
7684
run: MESOP_STATE_SESSION_BACKEND=memory PLAYWRIGHT_HTML_OUTPUT_DIR=playwright-report-with-memory-state-session yarn playwright test
7785
- uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3

.vscode/settings.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@
2323
"bazel-out/**": true,
2424
"bazel-mesop/**": true
2525
},
26-
"python.analysis.extraPaths": ["./bazel-bin"]
26+
"python.analysis.extraPaths": ["./bazel-bin"],
27+
"typescript.tsdk": "node_modules/typescript/lib"
2728
}

build_defs/defaults.bzl

+8
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ THIRD_PARTY_JS_HIGHLIGHTJS = [
7777
"@npm//highlight.js",
7878
]
7979

80+
THIRD_PARTY_JS_SOCKETIO_CLIENT = [
81+
"@npm//socket.io-client",
82+
]
83+
8084
THIRD_PARTY_PY_ABSL_PY = [
8185
requirement("absl-py"),
8286
]
@@ -89,6 +93,10 @@ THIRD_PARTY_PY_FLASK = [
8993
requirement("flask"),
9094
]
9195

96+
THIRD_PARTY_PY_FLASK_SOCKETIO = [
97+
requirement("flask-socketio"),
98+
]
99+
92100
THIRD_PARTY_PY_MATPLOTLIB = [
93101
requirement("matplotlib"),
94102
]

build_defs/requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ python-dotenv
88

99
# Optional (lazily-loaded) deps:
1010
sqlalchemy
11+
flask-socketio
12+
1113
# greenlet is needed for SQL Alchemy depending on the architecture, but because of how
1214
# Bazel works using requirements_lock.txt, it does seem to able to install the
1315
# architecture specific requirements (in this case caught on Github CI).

build_defs/requirements_lock.txt

+31-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ babel==2.15.0 \
1616
--hash=sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb \
1717
--hash=sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413
1818
# via mkdocs-material
19+
bidict==0.23.1 \
20+
--hash=sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71 \
21+
--hash=sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5
22+
# via python-socketio
1923
blinker==1.8.2 \
2024
--hash=sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01 \
2125
--hash=sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83
@@ -290,6 +294,12 @@ firebase-admin==6.5.0 \
290294
flask==3.0.3 \
291295
--hash=sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3 \
292296
--hash=sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842
297+
# via
298+
# -r build_defs/requirements.txt
299+
# flask-socketio
300+
flask-socketio==5.4.1 \
301+
--hash=sha256:2e9b8864a5be37ca54f6c76a4d06b1ac5e0df61fde12d03afc81ab4057e1eb86 \
302+
--hash=sha256:895da879d162781b9193cbb8fe8f3cf25b263ff242980d5c5e6c16d3c03930d2
293303
# via -r build_defs/requirements.txt
294304
fonttools==4.53.0 \
295305
--hash=sha256:099634631b9dd271d4a835d2b2a9e042ccc94ecdf7e2dd9f7f34f7daf333358d \
@@ -580,6 +590,10 @@ grpcio-status==1.62.2 \
580590
--hash=sha256:206ddf0eb36bc99b033f03b2c8e95d319f0044defae9b41ae21408e7e0cda48f \
581591
--hash=sha256:62e1bfcb02025a1cd73732a2d33672d3e9d0df4d21c12c51e0bbcaf09bab742a
582592
# via google-api-core
593+
h11==0.14.0 \
594+
--hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
595+
--hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
596+
# via wsproto
583597
httplib2==0.22.0 \
584598
--hash=sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc \
585599
--hash=sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81
@@ -716,7 +730,6 @@ markdown==3.6 \
716730
--hash=sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f \
717731
--hash=sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224
718732
# via
719-
# -r build_defs/requirements.txt
720733
# mkdocs
721734
# mkdocs-autorefs
722735
# mkdocs-material
@@ -1231,9 +1244,7 @@ pydantic-core==2.18.4 \
12311244
pygments==2.18.0 \
12321245
--hash=sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199 \
12331246
--hash=sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a
1234-
# via
1235-
# -r build_defs/requirements.txt
1236-
# mkdocs-material
1247+
# via mkdocs-material
12371248
pyjwt[crypto]==2.8.0 \
12381249
--hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \
12391250
--hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320
@@ -1265,6 +1276,14 @@ python-dotenv==1.0.1 \
12651276
--hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \
12661277
--hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a
12671278
# via -r build_defs/requirements.txt
1279+
python-engineio==4.9.1 \
1280+
--hash=sha256:7631cf5563086076611e494c643b3fa93dd3a854634b5488be0bba0ef9b99709 \
1281+
--hash=sha256:f995e702b21f6b9ebde4e2000cd2ad0112ba0e5116ec8d22fe3515e76ba9dddd
1282+
# via python-socketio
1283+
python-socketio==5.11.4 \
1284+
--hash=sha256:42efaa3e3e0b166fc72a527488a13caaac2cefc76174252486503bd496284945 \
1285+
--hash=sha256:8b0b8ff2964b2957c865835e936310190639c00310a47d77321a594d1665355e
1286+
# via flask-socketio
12681287
pytz==2024.1 \
12691288
--hash=sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812 \
12701289
--hash=sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319
@@ -1423,6 +1442,10 @@ rsa==4.9 \
14231442
--hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \
14241443
--hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21
14251444
# via google-auth
1445+
simple-websocket==1.1.0 \
1446+
--hash=sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c \
1447+
--hash=sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4
1448+
# via python-engineio
14261449
six==1.16.0 \
14271450
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
14281451
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
@@ -1543,3 +1566,7 @@ werkzeug==3.0.3 \
15431566
--hash=sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18 \
15441567
--hash=sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8
15451568
# via flask
1569+
wsproto==1.2.0 \
1570+
--hash=sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065 \
1571+
--hash=sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736
1572+
# via simple-websocket

mesop/cli/cli.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from mesop.server.flags import port
2323
from mesop.server.logging import log_startup
2424
from mesop.server.server import configure_flask_app
25+
from mesop.server.server_utils import MESOP_WEBSOCKETS_ENABLED
2526
from mesop.server.static_file_serving import configure_static_file_serving
2627
from mesop.utils.host_util import get_public_host
2728
from mesop.utils.runfiles import get_runfile_location
@@ -153,7 +154,17 @@ def main(argv: Sequence[str]):
153154
log_startup(port=port())
154155
logging.getLogger("werkzeug").setLevel(logging.WARN)
155156

156-
flask_app.run(host=get_public_host(), port=port(), use_reloader=False)
157+
if MESOP_WEBSOCKETS_ENABLED:
158+
socketio = flask_app.socketio # type: ignore
159+
socketio.run(
160+
flask_app,
161+
host=get_public_host(),
162+
port=port(),
163+
use_reloader=False,
164+
allow_unsafe_werkzeug=True,
165+
)
166+
else:
167+
flask_app.run(host=get_public_host(), port=port(), use_reloader=False)
157168

158169

159170
if __name__ == "__main__":

mesop/components/input/e2e/input_test.ts

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ test('test input on_blur works', async ({page}) => {
1919
// Fill in input and then click button and make sure values match
2020
await page.getByLabel('Input').click();
2121
await page.getByLabel('Input').fill('abc');
22+
await page.getByLabel('Input').blur();
2223
await page.getByRole('button', {name: 'button'}).click();
2324
await expect(page.getByText('Input: abc')).toBeVisible();
2425
await expect(
@@ -28,6 +29,7 @@ test('test input on_blur works', async ({page}) => {
2829
// Same with textarea:
2930
await page.getByLabel('Regular textarea').click();
3031
await page.getByLabel('Regular textarea').fill('123');
32+
await page.getByLabel('Regular textarea').blur();
3133
await page.getByRole('button', {name: 'button'}).click();
3234
await expect(page.getByText('Input: 123')).toBeVisible();
3335
await expect(
@@ -37,6 +39,7 @@ test('test input on_blur works', async ({page}) => {
3739
// Same with native textarea:
3840
await page.getByRole('textbox').nth(2).click();
3941
await page.getByRole('textbox').nth(2).fill('second_textarea');
42+
await page.getByRole('textbox').nth(2).blur();
4043
await page.getByRole('button', {name: 'button'}).click();
4144
await expect(page.getByText('Input: second_textarea')).toBeVisible();
4245
await expect(

mesop/examples/e2e/chat_test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ test('Chat UI can send messages and display responses', async ({page}) => {
88

99
// Test that we can send a message.
1010
await page.locator('//input').fill('Lorem ipsum');
11+
await page.locator('//input').blur();
1112
// Need to wait for the input state to be saved before clicking.
1213
await page.waitForTimeout(2000);
1314
await page.getByRole('button').filter({hasText: 'send'}).click();

mesop/protos/ui.proto

+1
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ message RenderEvent {
179179
message ExperimentSettings {
180180
optional bool experimental_editor_toolbar_enabled = 1;
181181
optional bool concurrent_updates_enabled = 2;
182+
optional bool websockets_enabled = 3;
182183
}
183184

184185
// UI response event for updating state.

mesop/server/BUILD

+7-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ load(
44
"THIRD_PARTY_PY_DOTENV",
55
"THIRD_PARTY_PY_FIREBASE_ADMIN",
66
"THIRD_PARTY_PY_FLASK",
7+
"THIRD_PARTY_PY_FLASK_SOCKETIO",
78
"THIRD_PARTY_PY_GREENLET",
89
"THIRD_PARTY_PY_MSGPACK",
910
"THIRD_PARTY_PY_PYTEST",
@@ -37,16 +38,18 @@ py_library(
3738
"//mesop/utils",
3839
"//mesop/warn",
3940
] + THIRD_PARTY_PY_ABSL_PY +
40-
THIRD_PARTY_PY_FLASK,
41+
THIRD_PARTY_PY_FLASK +
42+
THIRD_PARTY_PY_FLASK_SOCKETIO,
4143
)
4244

4345
py_library(
4446
name = "state_sessions",
4547
srcs = STATE_SESSIONS_SRCS,
4648
deps = [
47-
"//mesop/dataclass_utils",
48-
"//mesop/exceptions",
49-
] + THIRD_PARTY_PY_MSGPACK + THIRD_PARTY_PY_DOTENV,
49+
"//mesop/dataclass_utils",
50+
"//mesop/exceptions",
51+
] + THIRD_PARTY_PY_MSGPACK +
52+
THIRD_PARTY_PY_DOTENV,
5053
)
5154

5255
py_test(

0 commit comments

Comments
 (0)