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.
Files changed (65) hide show
  1. {tw_framework-0.3.7 → tw_framework-0.3.8}/PKG-INFO +1 -1
  2. {tw_framework-0.3.7 → tw_framework-0.3.8}/pyproject.toml +1 -1
  3. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/framework.py +113 -0
  4. tw_framework-0.3.8/tw_framework/websocket.py +189 -0
  5. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework.egg-info/PKG-INFO +1 -1
  6. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework.egg-info/SOURCES.txt +1 -0
  7. {tw_framework-0.3.7 → tw_framework-0.3.8}/LICENSE +0 -0
  8. {tw_framework-0.3.7 → tw_framework-0.3.8}/README.md +0 -0
  9. {tw_framework-0.3.7 → tw_framework-0.3.8}/setup.cfg +0 -0
  10. {tw_framework-0.3.7 → tw_framework-0.3.8}/tests/test_deployment.py +0 -0
  11. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/__init__.py +0 -0
  12. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/adapters/__init__.py +0 -0
  13. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/adapters/cloudflare.py +0 -0
  14. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/adapters/netlify.py +0 -0
  15. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/adapters/vercel.py +0 -0
  16. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/adapters/vercel_functions.py +0 -0
  17. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/advanced_diagnostics.py +0 -0
  18. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/asset_optimizer.py +0 -0
  19. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/ast_nodes.py +0 -0
  20. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/build.py +0 -0
  21. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/build_performance.py +0 -0
  22. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/build_report.py +0 -0
  23. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/cli.py +0 -0
  24. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/code_splitting.py +0 -0
  25. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/common.py +0 -0
  26. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/compiler.py +0 -0
  27. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/compiler_stats.py +0 -0
  28. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/dead_code.py +0 -0
  29. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/dependency_graph.py +0 -0
  30. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/deploy.py +0 -0
  31. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/dev.py +0 -0
  32. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/diagnostics.py +0 -0
  33. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/dynamic_imports.py +0 -0
  34. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/error_formatter.py +0 -0
  35. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/hydration.py +0 -0
  36. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/incremental_cache.py +0 -0
  37. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/interpreter.py +0 -0
  38. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/ir.py +0 -0
  39. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/lexer.py +0 -0
  40. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/lowering.py +0 -0
  41. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/parser.py +0 -0
  42. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/partial_rebuild.py +0 -0
  43. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/performance_analyzer.py +0 -0
  44. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/plugin_runtime.py +0 -0
  45. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/production_optimizer.py +0 -0
  46. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/reactivity.py +0 -0
  47. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/render_css.py +0 -0
  48. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/render_html.py +0 -0
  49. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/route_optimizer.py +0 -0
  50. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/router.py +0 -0
  51. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/runtime_model.py +0 -0
  52. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/runtime_values.py +0 -0
  53. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/semantic.py +0 -0
  54. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/server.py +0 -0
  55. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/signature.py +0 -0
  56. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/static_dynamic_auto.py +0 -0
  57. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/streaming.py +0 -0
  58. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/test_build_cli_pytest.py +0 -0
  59. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/tree_shaking.py +0 -0
  60. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/twm_api_runner.js +0 -0
  61. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework/twm_parser.py +0 -0
  62. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework.egg-info/dependency_links.txt +0 -0
  63. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework.egg-info/entry_points.txt +0 -0
  64. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework.egg-info/requires.txt +0 -0
  65. {tw_framework-0.3.7 → tw_framework-0.3.8}/tw_framework.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tw-framework
3
- Version: 0.3.7
3
+ Version: 0.3.8
4
4
  Summary: TW framework CLI with create, dev, build, and deploy commands
5
5
  Author: TW Framework
6
6
  Requires-Python: >=3.9
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tw-framework"
7
- version = "0.3.7"
7
+ version = "0.3.8"
8
8
  description = "TW framework CLI with create, dev, build, and deploy commands"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tw-framework
3
- Version: 0.3.7
3
+ Version: 0.3.8
4
4
  Summary: TW framework CLI with create, dev, build, and deploy commands
5
5
  Author: TW Framework
6
6
  Requires-Python: >=3.9
@@ -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