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,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