tw-framework 0.3.7__tar.gz → 0.3.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tw_framework-0.3.7 → tw_framework-0.3.8}/PKG-INFO +1 -1
- {tw_framework-0.3.7 → tw_framework-0.3.8}/pyproject.toml +1 -1
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/framework.py +113 -0
- tw_framework-0.3.8/tw_framework/websocket.py +189 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework.egg-info/PKG-INFO +1 -1
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework.egg-info/SOURCES.txt +1 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/LICENSE +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/README.md +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/setup.cfg +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tests/test_deployment.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/__init__.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/adapters/__init__.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/adapters/cloudflare.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/adapters/netlify.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/adapters/vercel.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/adapters/vercel_functions.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/advanced_diagnostics.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/asset_optimizer.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/ast_nodes.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/build.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/build_performance.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/build_report.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/cli.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/code_splitting.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/common.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/compiler.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/compiler_stats.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/dead_code.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/dependency_graph.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/deploy.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/dev.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/diagnostics.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/dynamic_imports.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/error_formatter.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/hydration.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/incremental_cache.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/interpreter.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/ir.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/lexer.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/lowering.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/parser.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/partial_rebuild.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/performance_analyzer.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/plugin_runtime.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/production_optimizer.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/reactivity.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/render_css.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/render_html.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/route_optimizer.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/router.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/runtime_model.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/runtime_values.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/semantic.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/server.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/signature.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/static_dynamic_auto.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/streaming.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/test_build_cli_pytest.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/tree_shaking.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/twm_api_runner.js +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/twm_parser.py +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework.egg-info/dependency_links.txt +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework.egg-info/entry_points.txt +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework.egg-info/requires.txt +0 -0
- {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework.egg-info/top_level.txt +0 -0
|
@@ -23,6 +23,7 @@ import time
|
|
|
23
23
|
import ctypes
|
|
24
24
|
import select
|
|
25
25
|
import struct
|
|
26
|
+
import importlib.util
|
|
26
27
|
import urllib.parse
|
|
27
28
|
from email.utils import formatdate
|
|
28
29
|
from dataclasses import dataclass
|
|
@@ -32,6 +33,7 @@ from . import compiler
|
|
|
32
33
|
from .common import content_hash, log
|
|
33
34
|
from .plugin_runtime import ExtensionManager
|
|
34
35
|
from .twm_parser import compile_twm_module_to_cjs, parse_twm_functions
|
|
36
|
+
from . import websocket as tw_websocket
|
|
35
37
|
|
|
36
38
|
# Deployment adapter imports
|
|
37
39
|
from .adapters.vercel import build_vercel_config, generate_vercel_output
|
|
@@ -614,6 +616,73 @@ def load_project_env(project_root: str, mode: str) -> Dict[str, str]:
|
|
|
614
616
|
return env
|
|
615
617
|
|
|
616
618
|
|
|
619
|
+
def validate_env_schema(config: dict, env: Dict[str, str]) -> List[str]:
|
|
620
|
+
raw = compiler.get_config_value(config, "env", "required", default="")
|
|
621
|
+
if not raw:
|
|
622
|
+
return []
|
|
623
|
+
if isinstance(raw, str):
|
|
624
|
+
names = [part.strip() for part in raw.split(",") if part.strip()]
|
|
625
|
+
elif isinstance(raw, (list, tuple)):
|
|
626
|
+
names = [str(part).strip() for part in raw if str(part).strip()]
|
|
627
|
+
else:
|
|
628
|
+
return []
|
|
629
|
+
missing = []
|
|
630
|
+
for name in names:
|
|
631
|
+
value = env.get(name) if env.get(name) is not None else os.environ.get(name)
|
|
632
|
+
if value is None or str(value).strip() == "":
|
|
633
|
+
missing.append(name)
|
|
634
|
+
return missing
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def warn_missing_env(missing: List[str]) -> None:
|
|
638
|
+
if not missing:
|
|
639
|
+
return
|
|
640
|
+
log("", level="warning")
|
|
641
|
+
log("\u26a0 Missing required environment variable(s):", level="warning")
|
|
642
|
+
for name in missing:
|
|
643
|
+
log(f" - {name}", level="warning")
|
|
644
|
+
log(" Set these in `.env`, `.env.development`, or `.env.local` before relying on them.", level="warning")
|
|
645
|
+
log(" Declared in `tw.config` under `env: required:`.", level="warning")
|
|
646
|
+
log("", level="warning")
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def discover_ws_routes(project_root: str) -> Dict[str, str]:
|
|
650
|
+
routes = {}
|
|
651
|
+
ws_dir = os.path.join(project_root, "[home]", "ws")
|
|
652
|
+
if not os.path.isdir(ws_dir):
|
|
653
|
+
return routes
|
|
654
|
+
for dirpath, _, filenames in os.walk(ws_dir):
|
|
655
|
+
for filename in sorted(filenames):
|
|
656
|
+
if not filename.endswith(".py") or filename.startswith("_"):
|
|
657
|
+
continue
|
|
658
|
+
full_path = os.path.join(dirpath, filename)
|
|
659
|
+
rel = os.path.relpath(full_path, ws_dir)
|
|
660
|
+
rel = rel[:-3] if rel.endswith(".py") else rel
|
|
661
|
+
rel = rel.replace(os.sep, "/")
|
|
662
|
+
if rel.endswith("/index"):
|
|
663
|
+
rel = rel[: -len("/index")]
|
|
664
|
+
elif rel == "index":
|
|
665
|
+
rel = ""
|
|
666
|
+
route = "/ws/" + rel if rel else "/ws"
|
|
667
|
+
routes[route] = full_path
|
|
668
|
+
return routes
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def load_ws_handler(path: str):
|
|
672
|
+
module_name = f"tw_ws_{os.path.splitext(os.path.basename(path))[0]}_{content_hash(path, length=10)}"
|
|
673
|
+
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
674
|
+
if spec is None or spec.loader is None:
|
|
675
|
+
return None
|
|
676
|
+
module = importlib.util.module_from_spec(spec)
|
|
677
|
+
try:
|
|
678
|
+
spec.loader.exec_module(module)
|
|
679
|
+
except Exception:
|
|
680
|
+
logger.exception("Failed to load WebSocket route: %s", path)
|
|
681
|
+
return None
|
|
682
|
+
handler = getattr(module, "on_connect", None)
|
|
683
|
+
return handler if callable(handler) else None
|
|
684
|
+
|
|
685
|
+
|
|
617
686
|
def parse_cookie_header(raw_value: str) -> Dict[str, str]:
|
|
618
687
|
cookies = {}
|
|
619
688
|
if not raw_value:
|
|
@@ -1269,6 +1338,7 @@ class TWProject:
|
|
|
1269
1338
|
configure_compiler_paths(self.project_root)
|
|
1270
1339
|
self.env = load_project_env(self.project_root, "development")
|
|
1271
1340
|
self.config = compiler.load_config()
|
|
1341
|
+
warn_missing_env(validate_env_schema(self.config, self.env))
|
|
1272
1342
|
self.modular_pipeline = use_modular_pipeline(self.config)
|
|
1273
1343
|
self.extensions = ExtensionManager(self.project_root, self.config, self.env).refresh()
|
|
1274
1344
|
self._rate_limiters: Dict[str, TokenBucketRateLimiter] = {}
|
|
@@ -2029,6 +2099,11 @@ def make_dev_handler(state: TWDevState):
|
|
|
2029
2099
|
|
|
2030
2100
|
def handle_request(self, method: str):
|
|
2031
2101
|
path = normalize_url_path(self.path)
|
|
2102
|
+
|
|
2103
|
+
if tw_websocket.is_websocket_upgrade(self.headers):
|
|
2104
|
+
self.handle_websocket(path)
|
|
2105
|
+
return
|
|
2106
|
+
|
|
2032
2107
|
if path == "/__tw/events":
|
|
2033
2108
|
self.handle_events()
|
|
2034
2109
|
return
|
|
@@ -2155,6 +2230,44 @@ def make_dev_handler(state: TWDevState):
|
|
|
2155
2230
|
body = render_error_html("Compile error", message, 500, ip=self.client_address[0] if self.client_address else "")
|
|
2156
2231
|
self.respond_bytes(500, body, "text/html; charset=utf-8", headers=middleware.get("headers", []), cookies=middleware.get("cookies", []))
|
|
2157
2232
|
|
|
2233
|
+
def handle_websocket(self, path: str):
|
|
2234
|
+
routes = discover_ws_routes(state.project.project_root)
|
|
2235
|
+
handler_path = routes.get(path)
|
|
2236
|
+
if handler_path is None:
|
|
2237
|
+
self.send_response(404)
|
|
2238
|
+
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
|
2239
|
+
self.end_headers()
|
|
2240
|
+
try:
|
|
2241
|
+
self.wfile.write(b"No WebSocket route for this path")
|
|
2242
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
2243
|
+
pass
|
|
2244
|
+
return
|
|
2245
|
+
|
|
2246
|
+
on_connect = load_ws_handler(handler_path)
|
|
2247
|
+
if on_connect is None:
|
|
2248
|
+
self.send_response(500)
|
|
2249
|
+
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
|
2250
|
+
self.end_headers()
|
|
2251
|
+
try:
|
|
2252
|
+
self.wfile.write(b"WebSocket route failed to load (see server logs)")
|
|
2253
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
2254
|
+
pass
|
|
2255
|
+
return
|
|
2256
|
+
|
|
2257
|
+
if not tw_websocket.perform_handshake(self):
|
|
2258
|
+
return
|
|
2259
|
+
|
|
2260
|
+
conn = tw_websocket.WebSocketConnection(
|
|
2261
|
+
self.connection, path,
|
|
2262
|
+
headers={key: value for key, value in self.headers.items()},
|
|
2263
|
+
)
|
|
2264
|
+
try:
|
|
2265
|
+
on_connect(conn)
|
|
2266
|
+
except Exception:
|
|
2267
|
+
logger.exception("WebSocket handler raised an exception: %s", handler_path)
|
|
2268
|
+
finally:
|
|
2269
|
+
conn.close()
|
|
2270
|
+
|
|
2158
2271
|
def handle_events(self):
|
|
2159
2272
|
self.send_response(200)
|
|
2160
2273
|
self.send_header("Content-Type", "text/event-stream")
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Minimal RFC 6455 WebSocket implementation, stdlib-only (no external deps).
|
|
2
|
+
|
|
3
|
+
Handles the HTTP Upgrade handshake and basic text/binary frame
|
|
4
|
+
send/receive over the raw socket owned by an http.server request
|
|
5
|
+
handler. Designed for TW's threaded dev/prod server: each connection
|
|
6
|
+
runs in its own thread (ThreadingMixIn), so a blocking recv loop here
|
|
7
|
+
is fine and does not stall other requests.
|
|
8
|
+
"""
|
|
9
|
+
import base64
|
|
10
|
+
import hashlib
|
|
11
|
+
import socket
|
|
12
|
+
import struct
|
|
13
|
+
|
|
14
|
+
WS_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
15
|
+
|
|
16
|
+
OPCODE_CONTINUATION = 0x0
|
|
17
|
+
OPCODE_TEXT = 0x1
|
|
18
|
+
OPCODE_BINARY = 0x2
|
|
19
|
+
OPCODE_CLOSE = 0x8
|
|
20
|
+
OPCODE_PING = 0x9
|
|
21
|
+
OPCODE_PONG = 0xA
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WebSocketClosed(Exception):
|
|
25
|
+
"""Raised internally when the peer closes the connection or the socket errors out."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_websocket_upgrade(headers) -> bool:
|
|
29
|
+
connection = str(headers.get("Connection", "")).lower()
|
|
30
|
+
upgrade = str(headers.get("Upgrade", "")).lower()
|
|
31
|
+
return "upgrade" in connection and upgrade == "websocket"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _compute_accept_key(client_key: str) -> str:
|
|
35
|
+
sha1 = hashlib.sha1((client_key + WS_MAGIC).encode("utf-8")).digest()
|
|
36
|
+
return base64.b64encode(sha1).decode("ascii")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def perform_handshake(handler) -> bool:
|
|
40
|
+
"""Sends the 101 Switching Protocols response directly over the raw socket.
|
|
41
|
+
Returns True on success. `handler` is a BaseHTTPRequestHandler instance."""
|
|
42
|
+
client_key = handler.headers.get("Sec-WebSocket-Key")
|
|
43
|
+
if not client_key:
|
|
44
|
+
return False
|
|
45
|
+
accept_key = _compute_accept_key(client_key)
|
|
46
|
+
response = (
|
|
47
|
+
"HTTP/1.1 101 Switching Protocols\r\n"
|
|
48
|
+
"Upgrade: websocket\r\n"
|
|
49
|
+
"Connection: Upgrade\r\n"
|
|
50
|
+
f"Sec-WebSocket-Accept: {accept_key}\r\n"
|
|
51
|
+
"\r\n"
|
|
52
|
+
)
|
|
53
|
+
try:
|
|
54
|
+
handler.wfile.write(response.encode("utf-8"))
|
|
55
|
+
handler.wfile.flush()
|
|
56
|
+
except (BrokenPipeError, ConnectionResetError, OSError):
|
|
57
|
+
return False
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _recv_exact(sock: socket.socket, n: int) -> bytes:
|
|
62
|
+
chunks = []
|
|
63
|
+
remaining = n
|
|
64
|
+
while remaining > 0:
|
|
65
|
+
chunk = sock.recv(remaining)
|
|
66
|
+
if not chunk:
|
|
67
|
+
raise WebSocketClosed("connection closed while reading")
|
|
68
|
+
chunks.append(chunk)
|
|
69
|
+
remaining -= len(chunk)
|
|
70
|
+
return b"".join(chunks)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _read_frame(sock: socket.socket):
|
|
74
|
+
"""Reads one WebSocket frame from the client. Returns (opcode, payload_bytes)."""
|
|
75
|
+
header = _recv_exact(sock, 2)
|
|
76
|
+
b0, b1 = header[0], header[1]
|
|
77
|
+
fin = (b0 & 0x80) != 0
|
|
78
|
+
opcode = b0 & 0x0F
|
|
79
|
+
masked = (b1 & 0x80) != 0
|
|
80
|
+
length = b1 & 0x7F
|
|
81
|
+
|
|
82
|
+
if length == 126:
|
|
83
|
+
length = struct.unpack(">H", _recv_exact(sock, 2))[0]
|
|
84
|
+
elif length == 127:
|
|
85
|
+
length = struct.unpack(">Q", _recv_exact(sock, 8))[0]
|
|
86
|
+
|
|
87
|
+
mask_key = _recv_exact(sock, 4) if masked else b""
|
|
88
|
+
payload = _recv_exact(sock, length) if length else b""
|
|
89
|
+
if masked:
|
|
90
|
+
payload = bytes(b ^ mask_key[i % 4] for i, b in enumerate(payload))
|
|
91
|
+
|
|
92
|
+
return opcode, payload, fin
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _encode_frame(opcode: int, payload: bytes) -> bytes:
|
|
96
|
+
header = bytearray()
|
|
97
|
+
header.append(0x80 | opcode)
|
|
98
|
+
length = len(payload)
|
|
99
|
+
if length < 126:
|
|
100
|
+
header.append(length)
|
|
101
|
+
elif length < 65536:
|
|
102
|
+
header.append(126)
|
|
103
|
+
header += struct.pack(">H", length)
|
|
104
|
+
else:
|
|
105
|
+
header.append(127)
|
|
106
|
+
header += struct.pack(">Q", length)
|
|
107
|
+
return bytes(header) + payload
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class WebSocketConnection:
|
|
111
|
+
"""A single upgraded connection. One instance per client; runs on its
|
|
112
|
+
own request-handler thread."""
|
|
113
|
+
|
|
114
|
+
def __init__(self, sock: socket.socket, path: str, headers=None):
|
|
115
|
+
self.sock = sock
|
|
116
|
+
self.path = path
|
|
117
|
+
self.headers = headers or {}
|
|
118
|
+
self.closed = False
|
|
119
|
+
|
|
120
|
+
def send_text(self, message: str) -> None:
|
|
121
|
+
if self.closed:
|
|
122
|
+
return
|
|
123
|
+
try:
|
|
124
|
+
self.sock.sendall(_encode_frame(OPCODE_TEXT, message.encode("utf-8")))
|
|
125
|
+
except (BrokenPipeError, ConnectionResetError, OSError):
|
|
126
|
+
self.closed = True
|
|
127
|
+
|
|
128
|
+
def send_bytes(self, data: bytes) -> None:
|
|
129
|
+
if self.closed:
|
|
130
|
+
return
|
|
131
|
+
try:
|
|
132
|
+
self.sock.sendall(_encode_frame(OPCODE_BINARY, data))
|
|
133
|
+
except (BrokenPipeError, ConnectionResetError, OSError):
|
|
134
|
+
self.closed = True
|
|
135
|
+
|
|
136
|
+
def close(self, code: int = 1000) -> None:
|
|
137
|
+
if self.closed:
|
|
138
|
+
return
|
|
139
|
+
self.closed = True
|
|
140
|
+
try:
|
|
141
|
+
self.sock.sendall(_encode_frame(OPCODE_CLOSE, struct.pack(">H", code)))
|
|
142
|
+
except (BrokenPipeError, ConnectionResetError, OSError):
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
def _pong(self, payload: bytes) -> None:
|
|
146
|
+
try:
|
|
147
|
+
self.sock.sendall(_encode_frame(OPCODE_PONG, payload))
|
|
148
|
+
except (BrokenPipeError, ConnectionResetError, OSError):
|
|
149
|
+
self.closed = True
|
|
150
|
+
|
|
151
|
+
def __iter__(self):
|
|
152
|
+
"""Yields text (str) or binary (bytes) messages until the client
|
|
153
|
+
disconnects. Ping/pong and close frames are handled transparently."""
|
|
154
|
+
buffer = bytearray()
|
|
155
|
+
buffer_opcode = None
|
|
156
|
+
while not self.closed:
|
|
157
|
+
try:
|
|
158
|
+
opcode, payload, fin = _read_frame(self.sock)
|
|
159
|
+
except (WebSocketClosed, ConnectionResetError, OSError, struct.error):
|
|
160
|
+
self.closed = True
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
if opcode == OPCODE_CLOSE:
|
|
164
|
+
self.closed = True
|
|
165
|
+
return
|
|
166
|
+
if opcode == OPCODE_PING:
|
|
167
|
+
self._pong(payload)
|
|
168
|
+
continue
|
|
169
|
+
if opcode == OPCODE_PONG:
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
if opcode in (OPCODE_TEXT, OPCODE_BINARY):
|
|
173
|
+
buffer_opcode = opcode
|
|
174
|
+
buffer = bytearray(payload)
|
|
175
|
+
elif opcode == OPCODE_CONTINUATION:
|
|
176
|
+
buffer.extend(payload)
|
|
177
|
+
else:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
if fin and buffer_opcode is not None:
|
|
181
|
+
if buffer_opcode == OPCODE_TEXT:
|
|
182
|
+
try:
|
|
183
|
+
yield buffer.decode("utf-8")
|
|
184
|
+
except UnicodeDecodeError:
|
|
185
|
+
continue
|
|
186
|
+
else:
|
|
187
|
+
yield bytes(buffer)
|
|
188
|
+
buffer = bytearray()
|
|
189
|
+
buffer_opcode = None
|
|
@@ -49,6 +49,7 @@ tw_framework/test_build_cli_pytest.py
|
|
|
49
49
|
tw_framework/tree_shaking.py
|
|
50
50
|
tw_framework/twm_api_runner.js
|
|
51
51
|
tw_framework/twm_parser.py
|
|
52
|
+
tw_framework/websocket.py
|
|
52
53
|
tw_framework.egg-info/PKG-INFO
|
|
53
54
|
tw_framework.egg-info/SOURCES.txt
|
|
54
55
|
tw_framework.egg-info/dependency_links.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|