pywire 0.1.0__py3-none-any.whl

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 (104) hide show
  1. pywire/__init__.py +2 -0
  2. pywire/cli/__init__.py +1 -0
  3. pywire/cli/generators.py +48 -0
  4. pywire/cli/main.py +309 -0
  5. pywire/cli/tui.py +563 -0
  6. pywire/cli/validate.py +26 -0
  7. pywire/client/.prettierignore +8 -0
  8. pywire/client/.prettierrc +7 -0
  9. pywire/client/build.mjs +73 -0
  10. pywire/client/eslint.config.js +46 -0
  11. pywire/client/package.json +39 -0
  12. pywire/client/pnpm-lock.yaml +2971 -0
  13. pywire/client/src/core/app.ts +263 -0
  14. pywire/client/src/core/dom-updater.test.ts +78 -0
  15. pywire/client/src/core/dom-updater.ts +321 -0
  16. pywire/client/src/core/index.ts +5 -0
  17. pywire/client/src/core/transport-manager.test.ts +179 -0
  18. pywire/client/src/core/transport-manager.ts +159 -0
  19. pywire/client/src/core/transports/base.ts +122 -0
  20. pywire/client/src/core/transports/http.ts +142 -0
  21. pywire/client/src/core/transports/index.ts +13 -0
  22. pywire/client/src/core/transports/websocket.ts +97 -0
  23. pywire/client/src/core/transports/webtransport.ts +149 -0
  24. pywire/client/src/dev/dev-app.ts +93 -0
  25. pywire/client/src/dev/error-trace.test.ts +97 -0
  26. pywire/client/src/dev/error-trace.ts +76 -0
  27. pywire/client/src/dev/index.ts +4 -0
  28. pywire/client/src/dev/status-overlay.ts +63 -0
  29. pywire/client/src/events/handler.test.ts +318 -0
  30. pywire/client/src/events/handler.ts +454 -0
  31. pywire/client/src/pywire.core.ts +22 -0
  32. pywire/client/src/pywire.dev.ts +27 -0
  33. pywire/client/tsconfig.json +17 -0
  34. pywire/client/vitest.config.ts +15 -0
  35. pywire/compiler/__init__.py +6 -0
  36. pywire/compiler/ast_nodes.py +304 -0
  37. pywire/compiler/attributes/__init__.py +6 -0
  38. pywire/compiler/attributes/base.py +24 -0
  39. pywire/compiler/attributes/conditional.py +37 -0
  40. pywire/compiler/attributes/events.py +55 -0
  41. pywire/compiler/attributes/form.py +37 -0
  42. pywire/compiler/attributes/loop.py +75 -0
  43. pywire/compiler/attributes/reactive.py +34 -0
  44. pywire/compiler/build.py +28 -0
  45. pywire/compiler/build_artifacts.py +342 -0
  46. pywire/compiler/codegen/__init__.py +5 -0
  47. pywire/compiler/codegen/attributes/__init__.py +6 -0
  48. pywire/compiler/codegen/attributes/base.py +19 -0
  49. pywire/compiler/codegen/attributes/events.py +35 -0
  50. pywire/compiler/codegen/directives/__init__.py +6 -0
  51. pywire/compiler/codegen/directives/base.py +16 -0
  52. pywire/compiler/codegen/directives/path.py +53 -0
  53. pywire/compiler/codegen/generator.py +2341 -0
  54. pywire/compiler/codegen/template.py +2178 -0
  55. pywire/compiler/directives/__init__.py +7 -0
  56. pywire/compiler/directives/base.py +20 -0
  57. pywire/compiler/directives/component.py +33 -0
  58. pywire/compiler/directives/context.py +93 -0
  59. pywire/compiler/directives/layout.py +49 -0
  60. pywire/compiler/directives/no_spa.py +24 -0
  61. pywire/compiler/directives/path.py +71 -0
  62. pywire/compiler/directives/props.py +88 -0
  63. pywire/compiler/exceptions.py +19 -0
  64. pywire/compiler/interpolation/__init__.py +6 -0
  65. pywire/compiler/interpolation/base.py +28 -0
  66. pywire/compiler/interpolation/jinja.py +272 -0
  67. pywire/compiler/parser.py +750 -0
  68. pywire/compiler/paths.py +29 -0
  69. pywire/compiler/preprocessor.py +43 -0
  70. pywire/core/wire.py +119 -0
  71. pywire/py.typed +0 -0
  72. pywire/runtime/__init__.py +7 -0
  73. pywire/runtime/aioquic_server.py +194 -0
  74. pywire/runtime/app.py +889 -0
  75. pywire/runtime/compile_error_page.py +195 -0
  76. pywire/runtime/debug.py +203 -0
  77. pywire/runtime/dev_server.py +434 -0
  78. pywire/runtime/dev_server.py.broken +268 -0
  79. pywire/runtime/error_page.py +64 -0
  80. pywire/runtime/error_renderer.py +23 -0
  81. pywire/runtime/escape.py +23 -0
  82. pywire/runtime/files.py +40 -0
  83. pywire/runtime/helpers.py +97 -0
  84. pywire/runtime/http_transport.py +253 -0
  85. pywire/runtime/loader.py +272 -0
  86. pywire/runtime/logging.py +72 -0
  87. pywire/runtime/page.py +384 -0
  88. pywire/runtime/pydantic_integration.py +52 -0
  89. pywire/runtime/router.py +229 -0
  90. pywire/runtime/server.py +25 -0
  91. pywire/runtime/style_collector.py +31 -0
  92. pywire/runtime/upload_manager.py +76 -0
  93. pywire/runtime/validation.py +449 -0
  94. pywire/runtime/websocket.py +665 -0
  95. pywire/runtime/webtransport_handler.py +195 -0
  96. pywire/templates/error/404.html +11 -0
  97. pywire/templates/error/500.html +38 -0
  98. pywire/templates/error/base.html +207 -0
  99. pywire/templates/error/compile_error.html +31 -0
  100. pywire-0.1.0.dist-info/METADATA +50 -0
  101. pywire-0.1.0.dist-info/RECORD +104 -0
  102. pywire-0.1.0.dist-info/WHEEL +4 -0
  103. pywire-0.1.0.dist-info/entry_points.txt +2 -0
  104. pywire-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,29 @@
1
+ """Helpers for PyWire filesystem paths."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ def ensure_pywire_folder() -> Path:
9
+ """Ensure .pywire exists and has a local .gitignore."""
10
+ dot_pywire = Path(".pywire")
11
+ if not dot_pywire.exists():
12
+ dot_pywire.mkdir()
13
+
14
+ gitignore_path = dot_pywire / ".gitignore"
15
+ if not gitignore_path.exists():
16
+ gitignore_path.write_text("*")
17
+
18
+ return dot_pywire
19
+
20
+
21
+ def get_pywire_path(*parts: str) -> Path:
22
+ """Return a path inside .pywire/."""
23
+ dot_pywire = ensure_pywire_folder()
24
+ return dot_pywire.joinpath(*parts)
25
+
26
+
27
+ def get_build_path(*parts: str) -> Path:
28
+ """Return a path inside .pywire/build/."""
29
+ return get_pywire_path("build", *parts)
@@ -0,0 +1,43 @@
1
+ import re
2
+
3
+
4
+ def preprocess_python_code(code: str) -> str:
5
+ """
6
+ Pre-process Python code to handle custom PyWire syntax ($var -> var.value).
7
+
8
+ This function replaces usage of the '$' prefix on identifiers with the '.value' suffix access,
9
+ which is the standard way to access PyWire 'wire' primitives.
10
+
11
+ Example:
12
+ $count += 1 -> count.value += 1
13
+
14
+ It respects Python string boundaries (single and triple quoted) to avoid replacing
15
+ text inside strings.
16
+ """
17
+ # Pattern matches:
18
+ # Group 1: Strings (Triple double, Triple single, Double, Single)
19
+ # Group 2: The $ token
20
+ # Group 3: The identifier
21
+
22
+ # We use non-capturing groups (?:...) for internal parts of string patterns to verify content
23
+ # Triple quoted strings can contain newlines ([\s\S]*?)
24
+ # Single quoted strings cannot contain unescaped newlines (not matching \n)
25
+
26
+ pattern = (
27
+ r"(\"\"\"[\s\S]*?\"\"\"|'''[\s\S]*?'''|" # Triple quoted strings
28
+ r"\"(?:\\.|[^\\\"\n])*\"|'(?:\\.|[^\\'\n])*')|" # Single quoted strings
29
+ r"(\$)([a-zA-Z_]\w*)" # The syntax we want to replace
30
+ )
31
+
32
+ def replacer(match):
33
+ # If it matched a string (Group 1), return it unchanged
34
+ if match.group(1):
35
+ return match.group(1)
36
+
37
+ # If it matched our syntax ($ + Identifier)
38
+ if match.group(2) and match.group(3):
39
+ return f"{match.group(3)}.value"
40
+
41
+ return match.group(0)
42
+
43
+ return re.sub(pattern, replacer, code, flags=re.MULTILINE)
pywire/core/wire.py ADDED
@@ -0,0 +1,119 @@
1
+ from contextvars import ContextVar
2
+ from typing import TypeVar, Generic, Any, Optional, Tuple, cast
3
+ from weakref import WeakSet
4
+
5
+ T = TypeVar("T")
6
+
7
+ _render_context: ContextVar[Optional[Tuple[Any, str]]] = ContextVar(
8
+ "pywire_render_context", default=None
9
+ )
10
+
11
+
12
+ def set_render_context(page: Any, region_id: str) -> Any:
13
+ return _render_context.set((page, region_id))
14
+
15
+
16
+ def reset_render_context(token: Any) -> None:
17
+ _render_context.reset(token)
18
+
19
+
20
+ class wire(Generic[T]):
21
+ """
22
+ A reactive container for state.
23
+
24
+ Usage:
25
+ # Single value
26
+ count = wire(0)
27
+ count.value += 1
28
+
29
+ # Namespace
30
+ user = wire(name="Alice", age=30)
31
+ user.name = "Bob"
32
+ """
33
+
34
+ def __init__(self, value: Optional[T] = None, **kwargs):
35
+ # We use strict dict manipulation to avoid triggering __setattr__
36
+ self.__dict__["_value"] = value
37
+ self.__dict__["_namespace"] = kwargs
38
+ self.__dict__["_pages"] = WeakSet()
39
+
40
+ def _track_read(self, field: str) -> None:
41
+ ctx = _render_context.get()
42
+ if not ctx:
43
+ return
44
+ page, region_id = ctx
45
+ pages = cast(WeakSet[Any], self.__dict__.get("_pages"))
46
+ if pages is not None:
47
+ pages.add(page)
48
+ register = getattr(page, "_register_wire_read", None)
49
+ if register:
50
+ register(self, field, region_id)
51
+
52
+ def _notify_write(self, field: str) -> None:
53
+ pages = cast(WeakSet[Any], self.__dict__.get("_pages"))
54
+ if not pages:
55
+ return
56
+ for page in list(pages):
57
+ invalidate = getattr(page, "_invalidate_wire", None)
58
+ if invalidate:
59
+ invalidate(self, field)
60
+
61
+ @property
62
+ def value(self) -> T:
63
+ """Access the underlying value."""
64
+ # strict priority: if 'value' is in namespace, return that.
65
+ # otherwise return the positional value.
66
+ if "value" in self.__dict__["_namespace"]:
67
+ self._track_read("value")
68
+ return self.__dict__["_namespace"]["value"]
69
+ self._track_read("value")
70
+ return self.__dict__["_value"]
71
+
72
+ @value.setter
73
+ def value(self, new_val: T):
74
+ if "value" in self.__dict__["_namespace"]:
75
+ self.__dict__["_namespace"]["value"] = new_val
76
+ else:
77
+ self.__dict__["_value"] = new_val
78
+ self._notify_write("value")
79
+
80
+ # Alias for shorter typing, if desired.
81
+ @property
82
+ def val(self) -> T:
83
+ return self.value
84
+
85
+ @val.setter
86
+ def val(self, new_val: T):
87
+ self.value = new_val
88
+
89
+ def __getattr__(self, name: str) -> Any:
90
+ if name in self.__dict__["_namespace"]:
91
+ self._track_read(name)
92
+ return self.__dict__["_namespace"][name]
93
+ raise AttributeError(f"'wire' object has no attribute '{name}'")
94
+
95
+ def __setattr__(self, name: str, val: Any):
96
+ # If the attribute is 'value' or 'val', go through the property
97
+ if name in ("value", "val"):
98
+ super().__setattr__(name, val)
99
+ return
100
+
101
+ # If it's an internal attribute (shouldn't really happen from outside)
102
+ if name in self.__dict__:
103
+ super().__setattr__(name, val)
104
+ return
105
+
106
+ # Otherwise, treat it as setting a namespace key
107
+ # We allow adding new keys dynamically
108
+ self.__dict__["_namespace"][name] = val
109
+ self._notify_write(name)
110
+
111
+ def __repr__(self):
112
+ if self._namespace:
113
+ items = [f"{k}={v!r}" for k, v in self._namespace.items()]
114
+ # If we also have a positional value that isn't None (and not shadowed), show it?
115
+ # Typically one uses EITHER positional OR kwargs.
116
+ if self._value is not None and "value" not in self._namespace:
117
+ return f"wire({self._value!r}, {', '.join(items)})"
118
+ return f"wire({', '.join(items)})"
119
+ return f"wire({self._value!r})"
pywire/py.typed ADDED
File without changes
@@ -0,0 +1,7 @@
1
+ """Runtime components."""
2
+
3
+ from pywire.runtime.app import PyWire
4
+ from pywire.runtime.page import BasePage
5
+ from pywire.runtime.router import Route, Router
6
+
7
+ __all__ = ["BasePage", "Router", "Route", "PyWire"]
@@ -0,0 +1,194 @@
1
+ """
2
+ ASGI HTTP/3 + WebTransport server using aioquic directly.
3
+
4
+ This module bypasses Hypercorn to use aioquic's native WebTransport support,
5
+ which requires explicit enable_webtransport=True in H3Connection initialization.
6
+ """
7
+
8
+ import asyncio
9
+ from typing import Any, Callable, Optional
10
+
11
+ from aioquic.asyncio import QuicConnectionProtocol, serve
12
+ from aioquic.h3.connection import H3_ALPN, H3Connection
13
+ from aioquic.h3.events import (
14
+ H3Event,
15
+ HeadersReceived,
16
+ )
17
+ from aioquic.quic.configuration import QuicConfiguration
18
+ from aioquic.quic.events import ProtocolNegotiated, QuicEvent
19
+
20
+
21
+ class ASGIProtocol(QuicConnectionProtocol):
22
+ """
23
+ QUIC/HTTP3 protocol handler that routes to ASGI application.
24
+
25
+ Handles WebTransport by creating H3Connection with enable_webtransport=True.
26
+ """
27
+
28
+ def __init__(
29
+ self, quic: Any, *args: Any, app_factory: Callable, **kwargs: Any
30
+ ) -> None:
31
+ super().__init__(quic, *args, **kwargs)
32
+ self._http: Optional[H3Connection] = None
33
+ self._app_factory = app_factory
34
+ self._app: Optional[Callable] = None
35
+
36
+ def quic_event_received(self, event: QuicEvent) -> None:
37
+ """Handle QUIC events, including protocol negotiation."""
38
+ if isinstance(event, ProtocolNegotiated):
39
+ if event.alpn_protocol in H3_ALPN:
40
+ # CRITICAL: Enable WebTransport support
41
+ self._http = H3Connection(self._quic, enable_webtransport=True)
42
+ print(
43
+ "PyWire: HTTP/3 connection established with WebTransport enabled",
44
+ flush=True,
45
+ )
46
+
47
+ # Pass events to HTTP/3 layer
48
+ if self._http is not None:
49
+ for http_event in self._http.handle_event(event):
50
+ self.http_event_received(http_event)
51
+
52
+ def http_event_received(self, event: H3Event) -> None:
53
+ """Route HTTP/3 events to ASGI application."""
54
+ if isinstance(event, HeadersReceived):
55
+ # Parse ASGI scope from headers
56
+ scope = self._build_scope(event)
57
+ print(
58
+ f"PyWire: Received {scope['type']} request to {scope.get('path', '/')}",
59
+ flush=True,
60
+ )
61
+
62
+ # Create ASGI handler
63
+ if self._app is None:
64
+ self._app = self._app_factory()
65
+
66
+ # Dispatch to ASGI app
67
+ asyncio.ensure_future(self._handle_asgi(scope, event))
68
+
69
+ def _build_scope(self, event: HeadersReceived) -> dict:
70
+ """Build ASGI scope dictionary from HTTP/3 headers."""
71
+ headers = []
72
+ method = ""
73
+ path = "/"
74
+ protocol = None
75
+
76
+ for header, value in event.headers:
77
+ if header == b":method":
78
+ method = value.decode()
79
+ elif header == b":path":
80
+ path = value.decode()
81
+ elif header == b":protocol":
82
+ protocol = value.decode()
83
+ elif header and not header.startswith(b":"):
84
+ headers.append((header, value))
85
+
86
+ # Determine scope type
87
+ if method == "CONNECT" and protocol == "webtransport":
88
+ scope_type = "webtransport"
89
+ else:
90
+ scope_type = "http"
91
+
92
+ return {
93
+ "type": scope_type,
94
+ "asgi": {"version": "3.0"},
95
+ "http_version": "3",
96
+ "method": method,
97
+ "path": path,
98
+ "headers": headers,
99
+ "server": ("localhost", 3000),
100
+ }
101
+
102
+ async def _handle_asgi(self, scope: dict, event: HeadersReceived) -> None:
103
+ """Handle ASGI application invocation."""
104
+ stream_id = event.stream_id
105
+
106
+ # Create receive/send callables
107
+ async def receive() -> dict:
108
+ # For WebTransport: wait for connect message
109
+ if scope["type"] == "webtransport":
110
+ return {"type": "webtransport.connect"}
111
+ return {"type": "http.request"}
112
+
113
+ async def send(message: dict) -> None:
114
+ msg_type = message["type"]
115
+ print(f"PyWire: Sending {msg_type} on stream {stream_id}", flush=True)
116
+
117
+ if msg_type == "webtransport.accept":
118
+ # Send 200 OK for WebTransport
119
+ if self._http:
120
+ self._http.send_headers(
121
+ stream_id=stream_id,
122
+ headers=[
123
+ (b":status", b"200"),
124
+ (b"sec-webtransport-http3-draft", b"draft02"),
125
+ ],
126
+ )
127
+ print(
128
+ f"PyWire: WebTransport connection accepted on stream {stream_id}",
129
+ flush=True,
130
+ )
131
+ elif msg_type == "http.response.start":
132
+ status = message.get("status", 200)
133
+ response_headers = message.get("headers", [])
134
+ if self._http:
135
+ self._http.send_headers(
136
+ stream_id=stream_id,
137
+ headers=[(b":status", str(status).encode())] + response_headers,
138
+ )
139
+ elif msg_type == "http.response.body":
140
+ data = message.get("body", b"")
141
+ if self._http:
142
+ self._http.send_data(
143
+ stream_id=stream_id,
144
+ data=data,
145
+ end_stream=not message.get("more_body", False),
146
+ )
147
+
148
+ self.transmit()
149
+
150
+ # Dispatch to ASGI app
151
+ if self._app:
152
+ await self._app(scope, receive, send)
153
+
154
+
155
+ async def run_aioquic_server(
156
+ app_factory: Callable,
157
+ host: str,
158
+ port: int,
159
+ certfile: str,
160
+ keyfile: str,
161
+ ) -> None:
162
+ """
163
+ Run HTTP/3 + WebTransport server using aioquic directly.
164
+
165
+ Args:
166
+ app_factory: Callable that returns ASGI application
167
+ host: Host to bind to
168
+ port: Port to bind to
169
+ certfile: Path to SSL certificate
170
+ keyfile: Path to SSL private key
171
+ """
172
+ # Configure QUIC
173
+ configuration = QuicConfiguration(
174
+ alpn_protocols=H3_ALPN,
175
+ is_client=False,
176
+ max_datagram_frame_size=65536,
177
+ )
178
+ configuration.load_cert_chain(certfile, keyfile)
179
+
180
+ # Create protocol factory
181
+ # Create protocol factory
182
+ def create_protocol(*args: Any, **kwargs: Any) -> ASGIProtocol:
183
+ if "app_factory" in kwargs:
184
+ del kwargs["app_factory"]
185
+ return ASGIProtocol(*args, app_factory=app_factory, **kwargs)
186
+
187
+ # Start server
188
+ print(f"PyWire: Starting aioquic HTTP/3 server on {host}:{port}", flush=True)
189
+ await serve(
190
+ host,
191
+ port,
192
+ configuration=configuration,
193
+ create_protocol=create_protocol,
194
+ )