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.
- pywire/__init__.py +2 -0
- pywire/cli/__init__.py +1 -0
- pywire/cli/generators.py +48 -0
- pywire/cli/main.py +309 -0
- pywire/cli/tui.py +563 -0
- pywire/cli/validate.py +26 -0
- pywire/client/.prettierignore +8 -0
- pywire/client/.prettierrc +7 -0
- pywire/client/build.mjs +73 -0
- pywire/client/eslint.config.js +46 -0
- pywire/client/package.json +39 -0
- pywire/client/pnpm-lock.yaml +2971 -0
- pywire/client/src/core/app.ts +263 -0
- pywire/client/src/core/dom-updater.test.ts +78 -0
- pywire/client/src/core/dom-updater.ts +321 -0
- pywire/client/src/core/index.ts +5 -0
- pywire/client/src/core/transport-manager.test.ts +179 -0
- pywire/client/src/core/transport-manager.ts +159 -0
- pywire/client/src/core/transports/base.ts +122 -0
- pywire/client/src/core/transports/http.ts +142 -0
- pywire/client/src/core/transports/index.ts +13 -0
- pywire/client/src/core/transports/websocket.ts +97 -0
- pywire/client/src/core/transports/webtransport.ts +149 -0
- pywire/client/src/dev/dev-app.ts +93 -0
- pywire/client/src/dev/error-trace.test.ts +97 -0
- pywire/client/src/dev/error-trace.ts +76 -0
- pywire/client/src/dev/index.ts +4 -0
- pywire/client/src/dev/status-overlay.ts +63 -0
- pywire/client/src/events/handler.test.ts +318 -0
- pywire/client/src/events/handler.ts +454 -0
- pywire/client/src/pywire.core.ts +22 -0
- pywire/client/src/pywire.dev.ts +27 -0
- pywire/client/tsconfig.json +17 -0
- pywire/client/vitest.config.ts +15 -0
- pywire/compiler/__init__.py +6 -0
- pywire/compiler/ast_nodes.py +304 -0
- pywire/compiler/attributes/__init__.py +6 -0
- pywire/compiler/attributes/base.py +24 -0
- pywire/compiler/attributes/conditional.py +37 -0
- pywire/compiler/attributes/events.py +55 -0
- pywire/compiler/attributes/form.py +37 -0
- pywire/compiler/attributes/loop.py +75 -0
- pywire/compiler/attributes/reactive.py +34 -0
- pywire/compiler/build.py +28 -0
- pywire/compiler/build_artifacts.py +342 -0
- pywire/compiler/codegen/__init__.py +5 -0
- pywire/compiler/codegen/attributes/__init__.py +6 -0
- pywire/compiler/codegen/attributes/base.py +19 -0
- pywire/compiler/codegen/attributes/events.py +35 -0
- pywire/compiler/codegen/directives/__init__.py +6 -0
- pywire/compiler/codegen/directives/base.py +16 -0
- pywire/compiler/codegen/directives/path.py +53 -0
- pywire/compiler/codegen/generator.py +2341 -0
- pywire/compiler/codegen/template.py +2178 -0
- pywire/compiler/directives/__init__.py +7 -0
- pywire/compiler/directives/base.py +20 -0
- pywire/compiler/directives/component.py +33 -0
- pywire/compiler/directives/context.py +93 -0
- pywire/compiler/directives/layout.py +49 -0
- pywire/compiler/directives/no_spa.py +24 -0
- pywire/compiler/directives/path.py +71 -0
- pywire/compiler/directives/props.py +88 -0
- pywire/compiler/exceptions.py +19 -0
- pywire/compiler/interpolation/__init__.py +6 -0
- pywire/compiler/interpolation/base.py +28 -0
- pywire/compiler/interpolation/jinja.py +272 -0
- pywire/compiler/parser.py +750 -0
- pywire/compiler/paths.py +29 -0
- pywire/compiler/preprocessor.py +43 -0
- pywire/core/wire.py +119 -0
- pywire/py.typed +0 -0
- pywire/runtime/__init__.py +7 -0
- pywire/runtime/aioquic_server.py +194 -0
- pywire/runtime/app.py +889 -0
- pywire/runtime/compile_error_page.py +195 -0
- pywire/runtime/debug.py +203 -0
- pywire/runtime/dev_server.py +434 -0
- pywire/runtime/dev_server.py.broken +268 -0
- pywire/runtime/error_page.py +64 -0
- pywire/runtime/error_renderer.py +23 -0
- pywire/runtime/escape.py +23 -0
- pywire/runtime/files.py +40 -0
- pywire/runtime/helpers.py +97 -0
- pywire/runtime/http_transport.py +253 -0
- pywire/runtime/loader.py +272 -0
- pywire/runtime/logging.py +72 -0
- pywire/runtime/page.py +384 -0
- pywire/runtime/pydantic_integration.py +52 -0
- pywire/runtime/router.py +229 -0
- pywire/runtime/server.py +25 -0
- pywire/runtime/style_collector.py +31 -0
- pywire/runtime/upload_manager.py +76 -0
- pywire/runtime/validation.py +449 -0
- pywire/runtime/websocket.py +665 -0
- pywire/runtime/webtransport_handler.py +195 -0
- pywire/templates/error/404.html +11 -0
- pywire/templates/error/500.html +38 -0
- pywire/templates/error/base.html +207 -0
- pywire/templates/error/compile_error.html +31 -0
- pywire-0.1.0.dist-info/METADATA +50 -0
- pywire-0.1.0.dist-info/RECORD +104 -0
- pywire-0.1.0.dist-info/WHEEL +4 -0
- pywire-0.1.0.dist-info/entry_points.txt +2 -0
- pywire-0.1.0.dist-info/licenses/LICENSE +201 -0
pywire/runtime/loader.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Page loader - compiles and executes .pywire files."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import os
|
|
5
|
+
import hashlib
|
|
6
|
+
import importlib.util
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from types import ModuleType
|
|
11
|
+
from typing import Any, Dict, Optional, Set, Type, cast
|
|
12
|
+
|
|
13
|
+
from pywire.compiler.codegen.generator import CodeGenerator
|
|
14
|
+
from pywire.compiler.parser import PyWireParser
|
|
15
|
+
from pywire.runtime.page import BasePage
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PageLoader:
|
|
19
|
+
"""Loads and compiles .pywire files into page classes."""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self.parser = PyWireParser()
|
|
23
|
+
self.codegen = CodeGenerator()
|
|
24
|
+
self._cache: Dict[str, Type[BasePage]] = {} # path -> compiled class
|
|
25
|
+
self._reverse_deps: Dict[str, set[str]] = {} # dependency -> set of dependents
|
|
26
|
+
self._manifest_cache: Dict[str, tuple[float, dict]] = {}
|
|
27
|
+
|
|
28
|
+
def load(
|
|
29
|
+
self,
|
|
30
|
+
pywire_file: Path,
|
|
31
|
+
use_cache: bool = True,
|
|
32
|
+
implicit_layout: Optional[str] = None,
|
|
33
|
+
) -> Type[BasePage]:
|
|
34
|
+
"""Load and compile a .pywire file into a page class."""
|
|
35
|
+
# Normalize path
|
|
36
|
+
pywire_file = pywire_file.resolve()
|
|
37
|
+
path_key = str(pywire_file)
|
|
38
|
+
|
|
39
|
+
# Check cache first (incorporate layout into key if needed? No,
|
|
40
|
+
# file content + layout dep determines it)
|
|
41
|
+
# Actually if implicit layout changes, we might need to recompile,
|
|
42
|
+
# but for now assume strict mapping
|
|
43
|
+
if use_cache and path_key in self._cache:
|
|
44
|
+
return self._cache[path_key]
|
|
45
|
+
|
|
46
|
+
# Try precompiled artifact
|
|
47
|
+
precompiled = self._load_precompiled(pywire_file)
|
|
48
|
+
if precompiled:
|
|
49
|
+
self._cache[path_key] = precompiled
|
|
50
|
+
precompiled.__file_path__ = str(pywire_file)
|
|
51
|
+
return precompiled
|
|
52
|
+
|
|
53
|
+
# Parse
|
|
54
|
+
parsed = self.parser.parse_file(pywire_file)
|
|
55
|
+
|
|
56
|
+
# Inject implicit layout if no explicit layout present
|
|
57
|
+
if implicit_layout:
|
|
58
|
+
from pywire.compiler.ast_nodes import LayoutDirective
|
|
59
|
+
|
|
60
|
+
if not parsed.get_directive_by_type(LayoutDirective):
|
|
61
|
+
# Create directive
|
|
62
|
+
# We need to ensure implicit_layout is relative or absolute?
|
|
63
|
+
# content relies on load_layout taking a path.
|
|
64
|
+
parsed.directives.append(
|
|
65
|
+
LayoutDirective(
|
|
66
|
+
name="layout", line=0, column=0, layout_path=implicit_layout
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Generate code
|
|
71
|
+
module_ast = self.codegen.generate(parsed)
|
|
72
|
+
ast.fix_missing_locations(module_ast)
|
|
73
|
+
|
|
74
|
+
# Compile and load
|
|
75
|
+
code = compile(module_ast, str(pywire_file), "exec")
|
|
76
|
+
module = type(sys)("pywire_page")
|
|
77
|
+
|
|
78
|
+
# Inject global load_layout
|
|
79
|
+
module_any = cast(Any, module)
|
|
80
|
+
module_any.load_layout = self.load_layout
|
|
81
|
+
module_any.load_component = self.load_component
|
|
82
|
+
|
|
83
|
+
exec(code, module.__dict__)
|
|
84
|
+
|
|
85
|
+
page_class = self._find_page_class(module, pywire_file)
|
|
86
|
+
self._cache[path_key] = page_class
|
|
87
|
+
page_class.__file_path__ = str(pywire_file)
|
|
88
|
+
return page_class
|
|
89
|
+
raise ValueError(f"No page class found in {pywire_file}")
|
|
90
|
+
|
|
91
|
+
def _find_page_class(self, module: ModuleType, pywire_file: Path) -> Type[BasePage]:
|
|
92
|
+
if hasattr(module, "__page_class__"):
|
|
93
|
+
return cast(Type[BasePage], module.__page_class__)
|
|
94
|
+
|
|
95
|
+
import pywire.runtime.page as page_mod
|
|
96
|
+
|
|
97
|
+
current_base_page = page_mod.BasePage
|
|
98
|
+
for name, obj in module.__dict__.items():
|
|
99
|
+
if name.startswith("__"):
|
|
100
|
+
continue
|
|
101
|
+
if isinstance(obj, type):
|
|
102
|
+
if (
|
|
103
|
+
issubclass(obj, current_base_page)
|
|
104
|
+
and obj is not current_base_page
|
|
105
|
+
and name != "_LayoutBase"
|
|
106
|
+
):
|
|
107
|
+
return cast(Type[BasePage], obj)
|
|
108
|
+
|
|
109
|
+
raise ValueError(f"No page class found in {pywire_file}")
|
|
110
|
+
|
|
111
|
+
def _load_precompiled(self, pywire_file: Path) -> Optional[Type[BasePage]]:
|
|
112
|
+
manifest_path = self._find_manifest(pywire_file)
|
|
113
|
+
if not manifest_path:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
manifest = self._load_manifest(manifest_path)
|
|
117
|
+
if not manifest:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
entries = manifest.get("entries", {})
|
|
121
|
+
entry = entries.get(str(pywire_file))
|
|
122
|
+
if not entry:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
if not self._is_entry_fresh(pywire_file, entry):
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
artifact_path = (manifest_path.parent / entry.get("artifact", "")).resolve()
|
|
129
|
+
if not artifact_path.exists():
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
module_name = (
|
|
133
|
+
"pywire_build_"
|
|
134
|
+
+ hashlib.md5(str(artifact_path).encode("utf-8")).hexdigest()
|
|
135
|
+
)
|
|
136
|
+
spec = importlib.util.spec_from_file_location(module_name, artifact_path)
|
|
137
|
+
if not spec or not spec.loader:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
module = importlib.util.module_from_spec(spec)
|
|
141
|
+
spec.loader.exec_module(module)
|
|
142
|
+
return self._find_page_class(module, pywire_file)
|
|
143
|
+
|
|
144
|
+
def _find_manifest(self, pywire_file: Path) -> Optional[Path]:
|
|
145
|
+
build_dir_override = os.environ.get("PYWIRE_BUILD_DIR")
|
|
146
|
+
if build_dir_override:
|
|
147
|
+
build_dir = Path(build_dir_override)
|
|
148
|
+
if not build_dir.is_absolute():
|
|
149
|
+
build_dir = Path.cwd() / build_dir
|
|
150
|
+
manifest_path = build_dir / "manifest.json"
|
|
151
|
+
if manifest_path.exists():
|
|
152
|
+
return manifest_path
|
|
153
|
+
|
|
154
|
+
current_dir = pywire_file.parent.resolve()
|
|
155
|
+
while True:
|
|
156
|
+
manifest_path = current_dir / ".pywire" / "build" / "manifest.json"
|
|
157
|
+
if manifest_path.exists():
|
|
158
|
+
return manifest_path
|
|
159
|
+
|
|
160
|
+
if current_dir == current_dir.parent:
|
|
161
|
+
break
|
|
162
|
+
current_dir = current_dir.parent
|
|
163
|
+
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
def _load_manifest(self, manifest_path: Path) -> Optional[dict]:
|
|
167
|
+
try:
|
|
168
|
+
mtime = manifest_path.stat().st_mtime
|
|
169
|
+
cache_key = str(manifest_path)
|
|
170
|
+
cached = self._manifest_cache.get(cache_key)
|
|
171
|
+
if cached and cached[0] == mtime:
|
|
172
|
+
return cached[1]
|
|
173
|
+
|
|
174
|
+
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
175
|
+
self._manifest_cache[cache_key] = (mtime, data)
|
|
176
|
+
return data
|
|
177
|
+
except Exception:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
def _is_entry_fresh(self, pywire_file: Path, entry: dict) -> bool:
|
|
181
|
+
if entry.get("hash") != self._hash_file(pywire_file):
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
for dep in entry.get("deps", []):
|
|
185
|
+
dep_path = Path(dep.get("path", ""))
|
|
186
|
+
if not dep_path.exists():
|
|
187
|
+
return False
|
|
188
|
+
if dep.get("hash") != self._hash_file(dep_path):
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
def _hash_file(self, path: Path) -> str:
|
|
194
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
195
|
+
|
|
196
|
+
def invalidate_cache(self, path: Optional[Path] = None) -> Set[str]:
|
|
197
|
+
"""Clear cached classes. If path given, only clear that entry and its dependents.
|
|
198
|
+
Returns set of invalidated paths (strings).
|
|
199
|
+
"""
|
|
200
|
+
invalidated = set()
|
|
201
|
+
if path:
|
|
202
|
+
key = str(path.resolve())
|
|
203
|
+
if key in self._cache:
|
|
204
|
+
self._cache.pop(key, None)
|
|
205
|
+
invalidated.add(key)
|
|
206
|
+
|
|
207
|
+
# Recursively invalidate dependents
|
|
208
|
+
dependents = self._reverse_deps.get(key, set())
|
|
209
|
+
for dependent in list(dependents):
|
|
210
|
+
# We construct a Path object to recurse properly (though internal key is string)
|
|
211
|
+
print(
|
|
212
|
+
f"PyWire: Invalidating dependent {dependent} because {key} changed."
|
|
213
|
+
)
|
|
214
|
+
invalidated.update(self.invalidate_cache(Path(dependent)))
|
|
215
|
+
|
|
216
|
+
return invalidated
|
|
217
|
+
else:
|
|
218
|
+
self._cache.clear()
|
|
219
|
+
self._reverse_deps.clear()
|
|
220
|
+
return set() # All cleared
|
|
221
|
+
|
|
222
|
+
def load_layout(
|
|
223
|
+
self, layout_path: str, base_path: Optional[str] = None
|
|
224
|
+
) -> Type[BasePage]:
|
|
225
|
+
"""Load a layout file and return its class."""
|
|
226
|
+
path = Path(layout_path)
|
|
227
|
+
if not path.is_absolute():
|
|
228
|
+
# Resolve relative to base file's directory
|
|
229
|
+
if base_path:
|
|
230
|
+
base_dir = Path(base_path).parent
|
|
231
|
+
path = base_dir / layout_path
|
|
232
|
+
else:
|
|
233
|
+
# Fallback to CWD
|
|
234
|
+
path = Path.cwd() / layout_path
|
|
235
|
+
|
|
236
|
+
# Resolve symlinks for consistent path comparison
|
|
237
|
+
path = path.resolve()
|
|
238
|
+
|
|
239
|
+
# Record dependency
|
|
240
|
+
if base_path:
|
|
241
|
+
dep_key = str(path)
|
|
242
|
+
dependent_key = str(Path(base_path).resolve())
|
|
243
|
+
if dep_key not in self._reverse_deps:
|
|
244
|
+
self._reverse_deps[dep_key] = set()
|
|
245
|
+
self._reverse_deps[dep_key].add(dependent_key)
|
|
246
|
+
|
|
247
|
+
return self.load(path)
|
|
248
|
+
|
|
249
|
+
def load_component(
|
|
250
|
+
self, component_path: str, base_path: Optional[str] = None
|
|
251
|
+
) -> Type[BasePage]:
|
|
252
|
+
"""Load a component file and return its class (same logic as layout)."""
|
|
253
|
+
return self.load_layout(component_path, base_path)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# Global instance for generated code to use
|
|
257
|
+
_loader_instance = PageLoader()
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def get_loader() -> PageLoader:
|
|
261
|
+
"""Get global loader instance."""
|
|
262
|
+
return _loader_instance
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def load_layout(path: str, base_path: Optional[str] = None) -> Type[BasePage]:
|
|
266
|
+
"""Helper for generated code to load layouts."""
|
|
267
|
+
return _loader_instance.load_layout(path, base_path)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def load_component(path: str, base_path: Optional[str] = None) -> Type[BasePage]:
|
|
271
|
+
"""Helper for generated code to load components."""
|
|
272
|
+
return _loader_instance.load_component(path, base_path)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextvars
|
|
3
|
+
import io
|
|
4
|
+
import sys
|
|
5
|
+
from typing import IO, Any, Callable, Coroutine
|
|
6
|
+
|
|
7
|
+
# Context variable to hold the log callback for the current request/session
|
|
8
|
+
# Callback signature: async def callback(message: str)
|
|
9
|
+
log_callback_ctx: contextvars.ContextVar[
|
|
10
|
+
Callable[[str], Coroutine[Any, Any, None]] | None
|
|
11
|
+
] = contextvars.ContextVar("log_callback_ctx", default=None)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ContextAwareStdout:
|
|
15
|
+
"""
|
|
16
|
+
Simulates stdout but intercepts writes to send to specific clients
|
|
17
|
+
based on the current context.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, original_stdout: IO[str], level: str = "info") -> None:
|
|
21
|
+
self.original_stdout = original_stdout
|
|
22
|
+
self.level = level
|
|
23
|
+
self.buffer = io.StringIO()
|
|
24
|
+
|
|
25
|
+
def write(self, message: str) -> None:
|
|
26
|
+
# Always write to original stdout
|
|
27
|
+
self.original_stdout.write(message)
|
|
28
|
+
|
|
29
|
+
# Check context for callback
|
|
30
|
+
callback = log_callback_ctx.get()
|
|
31
|
+
if callback:
|
|
32
|
+
# Schedule the callback
|
|
33
|
+
# Since write is sync, we must schedule async task
|
|
34
|
+
try:
|
|
35
|
+
loop = asyncio.get_running_loop()
|
|
36
|
+
if loop.is_running():
|
|
37
|
+
loop.create_task(self._safe_callback(callback, message))
|
|
38
|
+
except RuntimeError:
|
|
39
|
+
# No running loop, can't stream
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def flush(self) -> None:
|
|
43
|
+
self.original_stdout.flush()
|
|
44
|
+
|
|
45
|
+
async def _safe_callback(self, callback: Callable[..., Any], message: str) -> None:
|
|
46
|
+
try:
|
|
47
|
+
# Check if callback accepts level argument
|
|
48
|
+
import inspect
|
|
49
|
+
|
|
50
|
+
sig = inspect.signature(callback)
|
|
51
|
+
if "level" in sig.parameters:
|
|
52
|
+
await callback(message, level=self.level)
|
|
53
|
+
else:
|
|
54
|
+
await callback(message)
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
def __getattr__(self, name: str) -> Any:
|
|
59
|
+
return getattr(self.original_stdout, name)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Global installation
|
|
63
|
+
_installed = False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def install_logging_interceptor() -> None:
|
|
67
|
+
global _installed
|
|
68
|
+
if not _installed:
|
|
69
|
+
sys.stdout = ContextAwareStdout(sys.stdout, level="info")
|
|
70
|
+
# Handle stderr too? Usually yes for errors.
|
|
71
|
+
sys.stderr = ContextAwareStdout(sys.stderr, level="error")
|
|
72
|
+
_installed = True
|