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,7 @@
1
+ """Directive parsers."""
2
+
3
+ from pywire.compiler.directives.base import DirectiveParser
4
+ from pywire.compiler.directives.no_spa import NoSpaDirectiveParser
5
+ from pywire.compiler.directives.path import PathDirectiveParser
6
+
7
+ __all__ = ["DirectiveParser", "PathDirectiveParser", "NoSpaDirectiveParser"]
@@ -0,0 +1,20 @@
1
+ """Base directive parser."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Optional
5
+
6
+ from pywire.compiler.ast_nodes import Directive
7
+
8
+
9
+ class DirectiveParser(ABC):
10
+ """Base class for parsing directives - extensible for new directives."""
11
+
12
+ @abstractmethod
13
+ def can_parse(self, line: str) -> bool:
14
+ """Check if this parser can handle the given line."""
15
+ pass
16
+
17
+ @abstractmethod
18
+ def parse(self, line: str, line_num: int, col_num: int) -> Optional[Directive]:
19
+ """Parse directive from line. Returns None if not applicable."""
20
+ pass
@@ -0,0 +1,33 @@
1
+ import re
2
+ from typing import Optional
3
+
4
+ from pywire.compiler.ast_nodes import ComponentDirective, Directive
5
+ from pywire.compiler.directives.base import DirectiveParser
6
+
7
+
8
+ class ComponentDirectiveParser(DirectiveParser):
9
+ """Parses !component 'path' as Name"""
10
+
11
+ def can_parse(self, line: str) -> bool:
12
+ return line.startswith("!component")
13
+
14
+ def parse(self, line: str, line_num: int, col_num: int) -> Optional[Directive]:
15
+ # Format: !component 'path/to/file' as ComponentName
16
+ # or !component "path/to/file" as ComponentName
17
+
18
+ # Regex to match: !component\s+['"](.+?)['"]\s+as\s+(\w+)
19
+ match = re.search(r"^!component\s+['\"](.+?)['\"]\s+as\s+(\w+)", line)
20
+ if not match:
21
+ # Maybe invalid format
22
+ return None
23
+
24
+ path = match.group(1)
25
+ name = match.group(2)
26
+
27
+ return ComponentDirective(
28
+ line=line_num,
29
+ column=col_num,
30
+ name="!component",
31
+ path=path,
32
+ component_name=name,
33
+ )
@@ -0,0 +1,93 @@
1
+ import ast
2
+ from typing import Optional
3
+
4
+ from pywire.compiler.ast_nodes import Directive, InjectDirective, ProvideDirective
5
+ from pywire.compiler.directives.base import DirectiveParser
6
+
7
+
8
+ class ContextDirectiveParser(DirectiveParser):
9
+ """Parses !inject and !provide"""
10
+
11
+ def can_parse(self, line: str) -> bool:
12
+ return line.startswith("!inject") or line.startswith("!provide")
13
+
14
+ def parse(self, line: str, line_num: int, col_num: int) -> Optional[Directive]:
15
+ is_inject = line.startswith("!inject")
16
+ directive_name = "!inject" if is_inject else "!provide"
17
+
18
+ content = line[len(directive_name) :].strip()
19
+
20
+ # Wrapped in dict braces?
21
+ if not content.startswith("{") or not content.endswith("}"):
22
+ # Maybe they omitted braces? Let's assume strict syntax for now as per design doc
23
+ # !inject { ... }
24
+ return None
25
+
26
+ try:
27
+ # Parse as a dictionary expression
28
+ # code: _ = { ... }
29
+ dummy_code = f"_ = {content}"
30
+ mod = ast.parse(dummy_code)
31
+ assign = mod.body[0]
32
+ if not isinstance(assign, ast.Assign) or not isinstance(
33
+ assign.value, ast.Dict
34
+ ):
35
+ return None
36
+
37
+ dict_node = assign.value
38
+
39
+ mapping = {}
40
+
41
+ for key_node, value_node in zip(dict_node.keys, dict_node.values):
42
+ if is_inject:
43
+ # !inject { local_var: 'GLOBAL_KEY' }
44
+ # key should be the local variable name (Name node or Constant string)
45
+ # value should be the context key (Constant string)
46
+
47
+ local_var = None
48
+ if isinstance(key_node, ast.Name):
49
+ local_var = key_node.id
50
+ elif isinstance(key_node, ast.Constant) and isinstance(
51
+ key_node.value, str
52
+ ):
53
+ local_var = key_node.value
54
+
55
+ global_key = None
56
+ if isinstance(value_node, ast.Constant) and isinstance(
57
+ value_node.value, str
58
+ ):
59
+ global_key = value_node.value
60
+
61
+ if local_var and global_key:
62
+ mapping[local_var] = global_key
63
+
64
+ else:
65
+ # !provide { 'GLOBAL_KEY': local_expr }
66
+ # key must be string
67
+ # value is expression to be evaluated at runtime
68
+
69
+ global_key = None
70
+ if isinstance(key_node, ast.Constant) and isinstance(
71
+ key_node.value, str
72
+ ):
73
+ global_key = key_node.value
74
+
75
+ # Convert value node back to source string to be put in generated code
76
+ val_expr = ""
77
+ if hasattr(ast, "unparse"):
78
+ val_expr = ast.unparse(value_node)
79
+
80
+ if global_key:
81
+ mapping[global_key] = val_expr
82
+
83
+ if is_inject:
84
+ return InjectDirective(
85
+ line=line_num, column=col_num, name="!inject", mapping=mapping
86
+ )
87
+ else:
88
+ return ProvideDirective(
89
+ line=line_num, column=col_num, name="!provide", mapping=mapping
90
+ )
91
+
92
+ except Exception:
93
+ return None
@@ -0,0 +1,49 @@
1
+ """Layout directive parser."""
2
+
3
+ import ast
4
+ import re
5
+ from typing import Optional
6
+
7
+ from pywire.compiler.ast_nodes import LayoutDirective
8
+ from pywire.compiler.directives.base import DirectiveParser
9
+
10
+
11
+ class LayoutDirectiveParser(DirectiveParser):
12
+ """Parses !layout directives."""
13
+
14
+ PATTERN = re.compile(r"^!layout\s+(.+)$", re.DOTALL)
15
+
16
+ def can_parse(self, line: str) -> bool:
17
+ """Check if line starts with !layout."""
18
+ return line.strip().startswith("!layout")
19
+
20
+ def parse(
21
+ self, line: str, line_num: int, col_num: int
22
+ ) -> Optional[LayoutDirective]:
23
+ """Parse !layout "path/to/layout" directive."""
24
+ match = self.PATTERN.match(line.strip())
25
+ if not match:
26
+ return None
27
+
28
+ path_str = match.group(1).strip()
29
+ if not path_str:
30
+ return None
31
+
32
+ try:
33
+ # Parse python string
34
+ # We expect a simple string literal
35
+ expr_ast = ast.parse(path_str, mode="eval")
36
+
37
+ if isinstance(expr_ast.body, ast.Constant) and isinstance(
38
+ expr_ast.body.value, str
39
+ ):
40
+ return LayoutDirective(
41
+ name="layout",
42
+ layout_path=expr_ast.body.value,
43
+ line=line_num,
44
+ column=col_num,
45
+ )
46
+
47
+ return None
48
+ except (SyntaxError, ValueError, AttributeError):
49
+ return None
@@ -0,0 +1,24 @@
1
+ """No-SPA directive parser."""
2
+
3
+ import re
4
+ from typing import Optional
5
+
6
+ from pywire.compiler.ast_nodes import NoSpaDirective
7
+ from pywire.compiler.directives.base import DirectiveParser
8
+
9
+
10
+ class NoSpaDirectiveParser(DirectiveParser):
11
+ """Parses !no_spa directive to disable client-side navigation."""
12
+
13
+ PATTERN = re.compile(r"^!no_spa\s*$")
14
+
15
+ def can_parse(self, line: str) -> bool:
16
+ """Check if line is !no_spa."""
17
+ return line.strip() == "!no_spa"
18
+
19
+ def parse(self, line: str, line_num: int, col_num: int) -> Optional[NoSpaDirective]:
20
+ """Parse !no_spa directive."""
21
+ if not self.PATTERN.match(line.strip()):
22
+ return None
23
+
24
+ return NoSpaDirective(name="no_spa", line=line_num, column=col_num)
@@ -0,0 +1,71 @@
1
+ """Path directive parser."""
2
+
3
+ import ast
4
+ import re
5
+ from typing import Dict, Optional
6
+
7
+ from pywire.compiler.ast_nodes import PathDirective
8
+ from pywire.compiler.directives.base import DirectiveParser
9
+
10
+
11
+ class PathDirectiveParser(DirectiveParser):
12
+ """Parses !path directives."""
13
+
14
+ PATTERN = re.compile(r"^!path\s+(.+)$", re.DOTALL)
15
+
16
+ def can_parse(self, line: str) -> bool:
17
+ """Check if line starts with !path."""
18
+ return line.strip().startswith("!path")
19
+
20
+ def parse(self, line: str, line_num: int, col_num: int) -> Optional[PathDirective]:
21
+ """Parse !path { 'name': '/route' } directive."""
22
+ match = self.PATTERN.match(line.strip())
23
+ if not match:
24
+ return None
25
+
26
+ routes_str = match.group(1).strip()
27
+ if not routes_str:
28
+ return None
29
+
30
+ try:
31
+ # Parse python expression
32
+ expr_ast = ast.parse(routes_str, mode="eval")
33
+
34
+ # Case 1: Dictionary !path {'main': '/'}
35
+ if isinstance(expr_ast.body, ast.Dict):
36
+ routes: Dict[str, str] = {}
37
+ for key_node, value_node in zip(
38
+ expr_ast.body.keys, expr_ast.body.values
39
+ ):
40
+ if (
41
+ not isinstance(key_node, ast.Constant)
42
+ or not isinstance(key_node.value, str)
43
+ or not isinstance(value_node, ast.Constant)
44
+ or not isinstance(value_node.value, str)
45
+ ):
46
+ return None
47
+ routes[key_node.value] = value_node.value
48
+
49
+ return PathDirective(
50
+ name="path",
51
+ routes=routes,
52
+ is_simple_string=False,
53
+ line=line_num,
54
+ column=col_num,
55
+ )
56
+
57
+ # Case 2: String !path '/test'
58
+ elif isinstance(expr_ast.body, ast.Constant) and isinstance(
59
+ expr_ast.body.value, str
60
+ ):
61
+ return PathDirective(
62
+ name="path",
63
+ routes={"main": expr_ast.body.value},
64
+ is_simple_string=True,
65
+ line=line_num,
66
+ column=col_num,
67
+ )
68
+
69
+ return None
70
+ except (SyntaxError, ValueError, AttributeError):
71
+ return None
@@ -0,0 +1,88 @@
1
+ import ast
2
+ from typing import List, Optional, Tuple
3
+
4
+ from pywire.compiler.ast_nodes import Directive, PropsDirective
5
+ from pywire.compiler.directives.base import DirectiveParser
6
+
7
+
8
+ class PropsDirectiveParser(DirectiveParser):
9
+ """Parses !props(name: type, arg=default)"""
10
+
11
+ def can_parse(self, line: str) -> bool:
12
+ return line.startswith("!props")
13
+
14
+ def parse(self, line: str, line_num: int, col_num: int) -> Optional[Directive]:
15
+ # Format: !props(arg: type, arg2=default)
16
+
17
+ # 1. Extract content inside parentheses
18
+ # Note: 'line' might be multiline string passed from parser.
19
+ # We assume the parser handles accumulation until matching parens if needed.
20
+ # Or we rely on the fact that directives are usually single line or we need to handle it.
21
+ # The main parser accumulates if braces/brackets match, but what about parens?
22
+ # The main parser logic: "Count open braces/brackets". It does NOT count parens currently.
23
+ # We might need to update the main parser to also count parens for !props?
24
+ # For now, let's assume it's passed correctly or single line.
25
+
26
+ # Strip '!props'
27
+ content = line[len("!props") :].strip()
28
+ if not content.startswith("(") or not content.endswith(")"):
29
+ return None
30
+
31
+ # content is "(...)"
32
+
33
+ # 2. Use AST parsing by wrapping in a function def
34
+ # This handles complex types, strings with commas, etc.
35
+ dummy_code = f"def _p{content}: pass"
36
+
37
+ try:
38
+ mod = ast.parse(dummy_code)
39
+ func_def = mod.body[0]
40
+ if not isinstance(func_def, ast.FunctionDef):
41
+ return None
42
+
43
+ args = func_def.args
44
+
45
+ parsed_args: List[Tuple[str, str, Optional[str]]] = []
46
+
47
+ # Helper to get source segment if possible, or unparse
48
+ def unparse_node(node: ast.AST) -> str:
49
+ if hasattr(ast, "unparse"):
50
+ source = ast.unparse(node)
51
+ # ast.unparse might return something slightly different formatted,
52
+ # which is fine for type strings
53
+ return source
54
+ return "" # Fallback for older python if needed, but we likely preserve 3.9+
55
+
56
+ # Process args (normal arguments)
57
+ # defaults are at the end of the list.
58
+ # e.g. args.args = [a, b], args.defaults = [def_b] -> a has no default, b has def_b
59
+
60
+ num_args = len(args.args)
61
+ num_defaults = len(args.defaults)
62
+ offset = num_args - num_defaults
63
+
64
+ for i, arg in enumerate(args.args):
65
+ name = arg.arg
66
+ type_hint = "Any"
67
+ if arg.annotation:
68
+ type_hint = unparse_node(arg.annotation)
69
+
70
+ default_val = None
71
+ if i >= offset:
72
+ default_idx = i - offset
73
+ default_val = unparse_node(args.defaults[default_idx])
74
+
75
+ parsed_args.append((name, type_hint, default_val))
76
+
77
+ # TODO: Handle kwonlyargs if we want to enforce keyword only props?
78
+ # For now simple args.
79
+
80
+ return PropsDirective(
81
+ line=line_num, column=col_num, name="!props", args=parsed_args
82
+ )
83
+
84
+ except SyntaxError:
85
+ # Invalid python syntax in props
86
+ return None
87
+ except Exception:
88
+ return None
@@ -0,0 +1,19 @@
1
+ """Compiler exceptions."""
2
+
3
+
4
+ class PyWireSyntaxError(Exception):
5
+ """Raised when PyWire syntax is invalid."""
6
+
7
+ def __init__(
8
+ self, message: str, file_path: str = "", line: int = 0, column: int = 0
9
+ ):
10
+ self.message = message
11
+ self.file_path = file_path
12
+ self.line = line
13
+ self.column = column
14
+ super().__init__(message)
15
+
16
+ def __str__(self) -> str:
17
+ if self.file_path and self.line:
18
+ return f"{self.file_path}:{self.line}: {self.message}"
19
+ return self.message
@@ -0,0 +1,6 @@
1
+ """Interpolation parsers."""
2
+
3
+ from pywire.compiler.interpolation.base import InterpolationParser
4
+ from pywire.compiler.interpolation.jinja import JinjaInterpolationParser
5
+
6
+ __all__ = ["InterpolationParser", "JinjaInterpolationParser"]
@@ -0,0 +1,28 @@
1
+ """Base interpolation parser."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import List, Union
5
+
6
+ from pywire.compiler.ast_nodes import InterpolationNode
7
+
8
+
9
+ class InterpolationParser(ABC):
10
+ """Base class for parsing interpolations - can swap Jinja for custom later."""
11
+
12
+ @abstractmethod
13
+ def parse(
14
+ self, text: str, line: int, col: int
15
+ ) -> List[Union[str, InterpolationNode]]:
16
+ """
17
+ Parse text with interpolations into mix of strings and InterpolationNodes.
18
+ Returns: ['Hello, ', InterpolationNode(expr='name'), '!']
19
+ """
20
+ pass
21
+
22
+ @abstractmethod
23
+ def compile(self, text: str) -> str:
24
+ """
25
+ Compile to Python code for runtime.
26
+ 'Hello {name}!' → f'Hello {self.name}!'
27
+ """
28
+ pass