pywire 0.1.1__py3-none-any.whl → 0.1.2__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 (101) 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 +901 -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-0.1.1.dist-info → pywire-0.1.2.dist-info}/METADATA +1 -1
  97. pywire-0.1.2.dist-info/RECORD +104 -0
  98. pywire-0.1.1.dist-info/RECORD +0 -9
  99. {pywire-0.1.1.dist-info → pywire-0.1.2.dist-info}/WHEEL +0 -0
  100. {pywire-0.1.1.dist-info → pywire-0.1.2.dist-info}/entry_points.txt +0 -0
  101. {pywire-0.1.1.dist-info → pywire-0.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,342 @@
1
+ """Build system for precompiled PyWire artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import hashlib
7
+ import json
8
+ import re
9
+ import shutil
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Dict, List, Optional, Set, Tuple
13
+
14
+ from pywire.compiler.ast_nodes import (
15
+ ComponentDirective,
16
+ LayoutDirective,
17
+ ParsedPyWire,
18
+ PathDirective,
19
+ )
20
+ from pywire.compiler.codegen.generator import CodeGenerator
21
+ from pywire.compiler.parser import PyWireParser
22
+
23
+
24
+ @dataclass
25
+ class BuildSummary:
26
+ pages: int
27
+ layouts: int
28
+ components: int
29
+ out_dir: Path
30
+
31
+
32
+ class ArtifactBuilder:
33
+ def __init__(self, pages_dir: Path, out_dir: Path) -> None:
34
+ self.pages_dir = pages_dir.resolve()
35
+ self.out_dir = out_dir.resolve()
36
+ self.parser = PyWireParser()
37
+ self.codegen = CodeGenerator()
38
+ self.entries: Dict[str, dict] = {}
39
+ self._compiled: Set[str] = set()
40
+ self._page_count = 0
41
+ self._layout_count = 0
42
+ self._component_count = 0
43
+
44
+ def build(self, optimize: bool = False) -> BuildSummary:
45
+ if self.out_dir.exists():
46
+ shutil.rmtree(self.out_dir)
47
+
48
+ (self.out_dir / "pages").mkdir(parents=True, exist_ok=True)
49
+ (self.out_dir / "components").mkdir(parents=True, exist_ok=True)
50
+
51
+ self._scan_directory(self.pages_dir, layout_path=None, url_prefix="")
52
+ self._build_error_page()
53
+
54
+ manifest = {
55
+ "version": 1,
56
+ "pages_dir": str(self.pages_dir),
57
+ "entries": self.entries,
58
+ }
59
+ manifest_path = self.out_dir / "manifest.json"
60
+ manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
61
+
62
+ if optimize:
63
+ import compileall
64
+
65
+ compileall.compile_dir(self.out_dir, quiet=1, optimize=2)
66
+
67
+ return BuildSummary(
68
+ pages=self._page_count,
69
+ layouts=self._layout_count,
70
+ components=self._component_count,
71
+ out_dir=self.out_dir,
72
+ )
73
+
74
+ def _build_error_page(self) -> None:
75
+ error_page_path = self.pages_dir / "__error__.wire"
76
+ if not error_page_path.exists():
77
+ return
78
+
79
+ implicit_layout = None
80
+ root_layout = self.pages_dir / "__layout__.wire"
81
+ if root_layout.exists():
82
+ implicit_layout = str(root_layout.resolve())
83
+
84
+ self._compile_file(
85
+ error_page_path, kind="page", implicit_layout=implicit_layout, is_error=True
86
+ )
87
+
88
+ def _scan_directory(
89
+ self, dir_path: Path, layout_path: Optional[str], url_prefix: str
90
+ ) -> None:
91
+ current_layout = layout_path
92
+ potential_layout = dir_path / "__layout__.wire"
93
+ if potential_layout.exists():
94
+ self._compile_file(
95
+ potential_layout, kind="layout", implicit_layout=current_layout
96
+ )
97
+ current_layout = str(potential_layout.resolve())
98
+
99
+ try:
100
+ entries = sorted(list(dir_path.iterdir()))
101
+ except FileNotFoundError:
102
+ return
103
+
104
+ for entry in entries:
105
+ if entry.name.startswith("_") or entry.name.startswith("."):
106
+ continue
107
+
108
+ if entry.is_dir():
109
+ name = entry.name
110
+ new_segment = name
111
+ param_match = re.match(r"^\[(.*?)\]$", name)
112
+ if param_match:
113
+ param_name = param_match.group(1)
114
+ new_segment = f"{{{param_name}}}"
115
+
116
+ new_prefix = (url_prefix + "/" + new_segment).replace("//", "/")
117
+ self._scan_directory(entry, current_layout, new_prefix)
118
+ continue
119
+
120
+ if not entry.is_file() or entry.suffix != ".wire":
121
+ continue
122
+
123
+ if entry.name == "layout.wire":
124
+ continue
125
+
126
+ self._compile_file(
127
+ entry, kind="page", implicit_layout=current_layout, is_error=False
128
+ )
129
+
130
+ def _compile_file(
131
+ self,
132
+ file_path: Path,
133
+ kind: str,
134
+ implicit_layout: Optional[str],
135
+ is_error: bool = False,
136
+ ) -> None:
137
+ resolved_path = file_path.resolve()
138
+ key = str(resolved_path)
139
+
140
+ if key in self._compiled:
141
+ if kind == "page":
142
+ entry = self.entries.get(key)
143
+ if entry and entry.get("kind") != "page":
144
+ entry["kind"] = "page"
145
+ parsed = self.parser.parse_file(resolved_path)
146
+ entry["routes"] = self._get_routes(parsed, resolved_path, is_error)
147
+ return
148
+
149
+ parsed = self.parser.parse_file(resolved_path)
150
+ if implicit_layout:
151
+ if not parsed.get_directive_by_type(LayoutDirective):
152
+ parsed.directives.append(
153
+ LayoutDirective(
154
+ name="layout",
155
+ line=0,
156
+ column=0,
157
+ layout_path=implicit_layout,
158
+ )
159
+ )
160
+
161
+ module_ast = self.codegen.generate(parsed)
162
+ ast.fix_missing_locations(module_ast)
163
+ source = ast.unparse(module_ast)
164
+
165
+ artifact_rel = self._artifact_path_for(resolved_path)
166
+ artifact_path = self.out_dir / artifact_rel
167
+ artifact_path.parent.mkdir(parents=True, exist_ok=True)
168
+ artifact_path.write_text(source, encoding="utf-8")
169
+
170
+ deps = self._collect_deps(parsed, implicit_layout, resolved_path)
171
+ entry_deps = []
172
+ for dep_path, dep_kind in deps:
173
+ if not dep_path.exists():
174
+ continue
175
+ entry_deps.append(
176
+ {"path": str(dep_path), "hash": self._hash_file(dep_path)}
177
+ )
178
+
179
+ entry = {
180
+ "artifact": str(artifact_rel),
181
+ "hash": self._hash_file(resolved_path),
182
+ "deps": entry_deps,
183
+ "kind": kind,
184
+ "routes": self._get_routes(parsed, resolved_path, is_error)
185
+ if kind == "page"
186
+ else [],
187
+ "implicit_layout": implicit_layout,
188
+ }
189
+ self.entries[key] = entry
190
+ self._compiled.add(key)
191
+
192
+ if kind == "page":
193
+ self._page_count += 1
194
+ elif kind == "layout":
195
+ self._layout_count += 1
196
+ elif kind == "component":
197
+ self._component_count += 1
198
+
199
+ for dep_path, dep_kind in deps:
200
+ if not dep_path.exists():
201
+ continue
202
+ dep_implicit_layout = None
203
+ if self._is_in_pages(dep_path):
204
+ dep_implicit_layout = self._resolve_implicit_layout(dep_path)
205
+ self._compile_file(
206
+ dep_path, kind=dep_kind, implicit_layout=dep_implicit_layout
207
+ )
208
+
209
+ def _collect_deps(
210
+ self, parsed: ParsedPyWire, implicit_layout: Optional[str], base_path: Path
211
+ ) -> List[Tuple[Path, str]]:
212
+ deps: Dict[str, str] = {}
213
+
214
+ if implicit_layout:
215
+ deps[str(Path(implicit_layout).resolve())] = "layout"
216
+
217
+ for directive in parsed.directives:
218
+ if isinstance(directive, LayoutDirective):
219
+ path = self._resolve_path(directive.layout_path, base_path)
220
+ deps[str(path)] = "layout"
221
+ elif isinstance(directive, ComponentDirective):
222
+ path = self._resolve_path(directive.path, base_path)
223
+ deps[str(path)] = "component"
224
+
225
+ return [(Path(path), kind) for path, kind in deps.items()]
226
+
227
+ def _resolve_path(self, path_str: str, base_path: Path) -> Path:
228
+ path = Path(path_str)
229
+ if not path.is_absolute():
230
+ path = base_path.parent / path
231
+ return path.resolve()
232
+
233
+ def _artifact_path_for(self, file_path: Path) -> Path:
234
+ if self._is_in_pages(file_path):
235
+ rel = file_path.relative_to(self.pages_dir)
236
+ return Path("pages") / rel.with_suffix(".py")
237
+
238
+ file_hash = hashlib.md5(str(file_path).encode("utf-8")).hexdigest()[:10]
239
+ safe_name = f"{file_path.stem}_{file_hash}.py"
240
+ return Path("components") / safe_name
241
+
242
+ def _get_routes(
243
+ self, parsed: ParsedPyWire, file_path: Path, is_error: bool
244
+ ) -> List[str]:
245
+ if is_error:
246
+ return ["/__error__"]
247
+
248
+ path_directive = parsed.get_directive_by_type(PathDirective)
249
+ if isinstance(path_directive, PathDirective):
250
+ return list(path_directive.routes.values())
251
+
252
+ implicit = self._get_implicit_route(file_path)
253
+ if implicit:
254
+ return [implicit]
255
+ return []
256
+
257
+ def _get_implicit_route(self, file_path: Path) -> Optional[str]:
258
+ try:
259
+ rel_path = file_path.relative_to(self.pages_dir)
260
+ except ValueError:
261
+ return None
262
+
263
+ segments = []
264
+ for i, part in enumerate(rel_path.parts):
265
+ if part.startswith("_") or part.startswith("."):
266
+ return None
267
+
268
+ name = part
269
+ is_file = i == len(rel_path.parts) - 1
270
+ if is_file:
271
+ if not name.endswith(".wire"):
272
+ return None
273
+ if name == "layout.wire":
274
+ return None
275
+ name = Path(name).stem
276
+
277
+ segment = name
278
+ if name == "index":
279
+ segment = ""
280
+
281
+ param_match = re.match(r"^\[(.*?)\]$", name)
282
+ if param_match:
283
+ param_name = param_match.group(1)
284
+ segment = f"{{{param_name}}}"
285
+
286
+ segments.append(segment)
287
+
288
+ route_path = "/" + "/".join(segments)
289
+ while "//" in route_path:
290
+ route_path = route_path.replace("//", "/")
291
+
292
+ if route_path != "/" and route_path.endswith("/"):
293
+ route_path = route_path.rstrip("/")
294
+
295
+ if not route_path:
296
+ route_path = "/"
297
+
298
+ return route_path
299
+
300
+ def _resolve_implicit_layout(self, page_path: Path) -> Optional[str]:
301
+ current_dir = page_path.parent
302
+ try:
303
+ current_dir.relative_to(self.pages_dir)
304
+ except ValueError:
305
+ return None
306
+
307
+ while True:
308
+ layout = current_dir / "__layout__.wire"
309
+ if layout.exists():
310
+ if layout.resolve() != page_path.resolve():
311
+ return str(layout.resolve())
312
+
313
+ if current_dir == self.pages_dir:
314
+ break
315
+
316
+ current_dir = current_dir.parent
317
+ if current_dir == current_dir.parent:
318
+ break
319
+
320
+ return None
321
+
322
+ def _hash_file(self, path: Path) -> str:
323
+ return hashlib.sha256(path.read_bytes()).hexdigest()
324
+
325
+ def _is_in_pages(self, path: Path) -> bool:
326
+ try:
327
+ path.resolve().relative_to(self.pages_dir)
328
+ return True
329
+ except ValueError:
330
+ return False
331
+
332
+
333
+ def build_artifacts(
334
+ pages_dir: Path, out_dir: Optional[Path] = None, optimize: bool = False
335
+ ) -> BuildSummary:
336
+ if out_dir is None:
337
+ from pywire.compiler.paths import get_build_path
338
+
339
+ out_dir = get_build_path()
340
+
341
+ builder = ArtifactBuilder(pages_dir=pages_dir, out_dir=out_dir)
342
+ return builder.build(optimize=optimize)
@@ -0,0 +1,5 @@
1
+ """Code generation modules."""
2
+
3
+ from pywire.compiler.codegen.generator import CodeGenerator
4
+
5
+ __all__ = ["CodeGenerator"]
@@ -0,0 +1,6 @@
1
+ """Attribute code generators."""
2
+
3
+ from pywire.compiler.codegen.attributes.base import AttributeCodegen
4
+ from pywire.compiler.codegen.attributes.events import EventAttributeCodegen
5
+
6
+ __all__ = ["AttributeCodegen", "EventAttributeCodegen"]
@@ -0,0 +1,19 @@
1
+ """Base attribute code generator."""
2
+
3
+ import ast
4
+ from abc import ABC, abstractmethod
5
+ from typing import Optional
6
+
7
+ from pywire.compiler.ast_nodes import SpecialAttribute
8
+
9
+
10
+ class AttributeCodegen(ABC):
11
+ """Base class for attribute code generation."""
12
+
13
+ @abstractmethod
14
+ def generate_html(self, attr: SpecialAttribute) -> str:
15
+ """Generate HTML attributes for client."""
16
+
17
+ @abstractmethod
18
+ def generate_handler(self, attr: SpecialAttribute) -> Optional[ast.FunctionDef]:
19
+ """Generate server-side handler if needed. Returns None if user defines it."""
@@ -0,0 +1,35 @@
1
+ """Event attribute code generator."""
2
+
3
+ import ast
4
+ from typing import Optional
5
+
6
+ from pywire.compiler.ast_nodes import EventAttribute, SpecialAttribute
7
+ from pywire.compiler.codegen.attributes.base import AttributeCodegen
8
+
9
+
10
+ class EventAttributeCodegen(AttributeCodegen):
11
+ """Generates event handler hookup for @click."""
12
+
13
+ def generate_html(self, attr: SpecialAttribute) -> str:
14
+ """Generate HTML data attribute for event."""
15
+ assert isinstance(attr, EventAttribute)
16
+ # @click.prevent={handler} → data-on-click="handler" data-modifiers-click="prevent"
17
+ attrs = [f'data-on-{attr.event_type}="{attr.handler_name}"']
18
+ if attr.modifiers:
19
+ attrs.append(
20
+ f'data-modifiers-{attr.event_type}="{" ".join(attr.modifiers)}"'
21
+ )
22
+
23
+ # Lifted arguments support
24
+ if hasattr(attr, "args") and attr.args:
25
+ for i, arg in enumerate(attr.args):
26
+ # We need to escape quotes in the argument value for HTML
27
+ escaped_arg = str(arg).replace('"', """)
28
+ attrs.append(f'data-arg-{i}="{escaped_arg}"')
29
+
30
+ return " ".join(attrs)
31
+
32
+ def generate_handler(self, attr: SpecialAttribute) -> Optional[ast.FunctionDef]:
33
+ """Generate handler method AST."""
34
+ assert isinstance(attr, EventAttribute)
35
+ return None
@@ -0,0 +1,6 @@
1
+ """Directive code generators."""
2
+
3
+ from pywire.compiler.codegen.directives.base import DirectiveCodegen
4
+ from pywire.compiler.codegen.directives.path import PathDirectiveCodegen
5
+
6
+ __all__ = ["DirectiveCodegen", "PathDirectiveCodegen"]
@@ -0,0 +1,16 @@
1
+ """Base directive code generator."""
2
+
3
+ import ast
4
+ from abc import ABC, abstractmethod
5
+ from typing import List
6
+
7
+ from pywire.compiler.ast_nodes import Directive
8
+
9
+
10
+ class DirectiveCodegen(ABC):
11
+ """Base class for directive code generation."""
12
+
13
+ @abstractmethod
14
+ def generate(self, directive: Directive) -> List[ast.stmt]:
15
+ """Generate AST statements for directive."""
16
+ pass
@@ -0,0 +1,53 @@
1
+ """Path directive code generator."""
2
+
3
+ import ast
4
+ from typing import List
5
+
6
+ from pywire.compiler.ast_nodes import Directive, PathDirective
7
+ from pywire.compiler.codegen.directives.base import DirectiveCodegen
8
+
9
+
10
+ class PathDirectiveCodegen(DirectiveCodegen):
11
+ """Generates routing metadata from !path."""
12
+
13
+ def generate(self, directive: Directive) -> List[ast.stmt]:
14
+ """Generate route metadata assignments."""
15
+ assert isinstance(directive, PathDirective)
16
+ statements: List[ast.stmt] = []
17
+
18
+ # Generate __routes__ dict with all route names
19
+ routes_dict = {}
20
+ for name, pattern in directive.routes.items():
21
+ routes_dict[name] = pattern
22
+
23
+ routes_ast = ast.Dict(
24
+ keys=[ast.Constant(value=k) for k in routes_dict.keys()],
25
+ values=[ast.Constant(value=v) for v in routes_dict.values()],
26
+ )
27
+
28
+ statements.append(
29
+ ast.Assign(
30
+ targets=[ast.Name(id="__routes__", ctx=ast.Store())], value=routes_ast
31
+ )
32
+ )
33
+
34
+ # Generate __path_mode__
35
+ mode = "string" if directive.is_simple_string else "dict"
36
+ statements.append(
37
+ ast.Assign(
38
+ targets=[ast.Name(id="__path_mode__", ctx=ast.Store())],
39
+ value=ast.Constant(value=mode),
40
+ )
41
+ )
42
+
43
+ # Generate __route__ with first route pattern (for backward compatibility)
44
+ if routes_dict:
45
+ first_pattern = list(routes_dict.values())[0]
46
+ statements.append(
47
+ ast.Assign(
48
+ targets=[ast.Name(id="__route__", ctx=ast.Store())],
49
+ value=ast.Constant(value=first_pattern),
50
+ )
51
+ )
52
+
53
+ return statements