pulse-framework 0.1.62__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 (126) hide show
  1. pulse/__init__.py +1493 -0
  2. pulse/_examples.py +29 -0
  3. pulse/app.py +1086 -0
  4. pulse/channel.py +607 -0
  5. pulse/cli/__init__.py +0 -0
  6. pulse/cli/cmd.py +575 -0
  7. pulse/cli/dependencies.py +181 -0
  8. pulse/cli/folder_lock.py +134 -0
  9. pulse/cli/helpers.py +271 -0
  10. pulse/cli/logging.py +102 -0
  11. pulse/cli/models.py +35 -0
  12. pulse/cli/packages.py +262 -0
  13. pulse/cli/processes.py +292 -0
  14. pulse/cli/secrets.py +39 -0
  15. pulse/cli/uvicorn_log_config.py +87 -0
  16. pulse/code_analysis.py +38 -0
  17. pulse/codegen/__init__.py +0 -0
  18. pulse/codegen/codegen.py +359 -0
  19. pulse/codegen/templates/__init__.py +0 -0
  20. pulse/codegen/templates/layout.py +106 -0
  21. pulse/codegen/templates/route.py +345 -0
  22. pulse/codegen/templates/routes_ts.py +42 -0
  23. pulse/codegen/utils.py +20 -0
  24. pulse/component.py +237 -0
  25. pulse/components/__init__.py +0 -0
  26. pulse/components/for_.py +83 -0
  27. pulse/components/if_.py +86 -0
  28. pulse/components/react_router.py +94 -0
  29. pulse/context.py +108 -0
  30. pulse/cookies.py +322 -0
  31. pulse/decorators.py +344 -0
  32. pulse/dom/__init__.py +0 -0
  33. pulse/dom/elements.py +1024 -0
  34. pulse/dom/events.py +445 -0
  35. pulse/dom/props.py +1250 -0
  36. pulse/dom/svg.py +0 -0
  37. pulse/dom/tags.py +328 -0
  38. pulse/dom/tags.pyi +480 -0
  39. pulse/env.py +178 -0
  40. pulse/form.py +538 -0
  41. pulse/helpers.py +541 -0
  42. pulse/hooks/__init__.py +0 -0
  43. pulse/hooks/core.py +452 -0
  44. pulse/hooks/effects.py +88 -0
  45. pulse/hooks/init.py +668 -0
  46. pulse/hooks/runtime.py +464 -0
  47. pulse/hooks/setup.py +254 -0
  48. pulse/hooks/stable.py +138 -0
  49. pulse/hooks/state.py +192 -0
  50. pulse/js/__init__.py +125 -0
  51. pulse/js/__init__.pyi +115 -0
  52. pulse/js/_types.py +299 -0
  53. pulse/js/array.py +339 -0
  54. pulse/js/console.py +50 -0
  55. pulse/js/date.py +119 -0
  56. pulse/js/document.py +145 -0
  57. pulse/js/error.py +140 -0
  58. pulse/js/json.py +66 -0
  59. pulse/js/map.py +97 -0
  60. pulse/js/math.py +69 -0
  61. pulse/js/navigator.py +79 -0
  62. pulse/js/number.py +57 -0
  63. pulse/js/obj.py +81 -0
  64. pulse/js/object.py +172 -0
  65. pulse/js/promise.py +172 -0
  66. pulse/js/pulse.py +115 -0
  67. pulse/js/react.py +495 -0
  68. pulse/js/regexp.py +57 -0
  69. pulse/js/set.py +124 -0
  70. pulse/js/string.py +38 -0
  71. pulse/js/weakmap.py +53 -0
  72. pulse/js/weakset.py +48 -0
  73. pulse/js/window.py +205 -0
  74. pulse/messages.py +202 -0
  75. pulse/middleware.py +471 -0
  76. pulse/plugin.py +96 -0
  77. pulse/proxy.py +242 -0
  78. pulse/py.typed +0 -0
  79. pulse/queries/__init__.py +0 -0
  80. pulse/queries/client.py +609 -0
  81. pulse/queries/common.py +101 -0
  82. pulse/queries/effect.py +55 -0
  83. pulse/queries/infinite_query.py +1418 -0
  84. pulse/queries/mutation.py +295 -0
  85. pulse/queries/protocol.py +136 -0
  86. pulse/queries/query.py +1314 -0
  87. pulse/queries/store.py +120 -0
  88. pulse/react_component.py +88 -0
  89. pulse/reactive.py +1208 -0
  90. pulse/reactive_extensions.py +1172 -0
  91. pulse/render_session.py +768 -0
  92. pulse/renderer.py +584 -0
  93. pulse/request.py +205 -0
  94. pulse/routing.py +598 -0
  95. pulse/serializer.py +279 -0
  96. pulse/state.py +556 -0
  97. pulse/test_helpers.py +15 -0
  98. pulse/transpiler/__init__.py +111 -0
  99. pulse/transpiler/assets.py +81 -0
  100. pulse/transpiler/builtins.py +1029 -0
  101. pulse/transpiler/dynamic_import.py +130 -0
  102. pulse/transpiler/emit_context.py +49 -0
  103. pulse/transpiler/errors.py +96 -0
  104. pulse/transpiler/function.py +611 -0
  105. pulse/transpiler/id.py +18 -0
  106. pulse/transpiler/imports.py +341 -0
  107. pulse/transpiler/js_module.py +336 -0
  108. pulse/transpiler/modules/__init__.py +33 -0
  109. pulse/transpiler/modules/asyncio.py +57 -0
  110. pulse/transpiler/modules/json.py +24 -0
  111. pulse/transpiler/modules/math.py +265 -0
  112. pulse/transpiler/modules/pulse/__init__.py +5 -0
  113. pulse/transpiler/modules/pulse/tags.py +250 -0
  114. pulse/transpiler/modules/typing.py +63 -0
  115. pulse/transpiler/nodes.py +1987 -0
  116. pulse/transpiler/py_module.py +135 -0
  117. pulse/transpiler/transpiler.py +1100 -0
  118. pulse/transpiler/vdom.py +256 -0
  119. pulse/types/__init__.py +0 -0
  120. pulse/types/event_handler.py +50 -0
  121. pulse/user_session.py +386 -0
  122. pulse/version.py +69 -0
  123. pulse_framework-0.1.62.dist-info/METADATA +198 -0
  124. pulse_framework-0.1.62.dist-info/RECORD +126 -0
  125. pulse_framework-0.1.62.dist-info/WHEEL +4 -0
  126. pulse_framework-0.1.62.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,130 @@
1
+ """Dynamic import primitive for code-splitting.
2
+
3
+ Provides `import_` for inline dynamic imports in @javascript functions:
4
+
5
+ @javascript
6
+ def load_chart():
7
+ return import_("./Chart").then(lambda m: m.default)
8
+
9
+ For lazy-loaded React components, use Import(lazy=True) with React.lazy:
10
+
11
+ from pulse.js.react import React, lazy
12
+
13
+ # Low-level: Import(lazy=True) creates a factory, wrap with React.lazy
14
+ factory = Import("./Chart", lazy=True)
15
+ LazyChart = Jsx(React.lazy(factory))
16
+
17
+ # High-level: lazy() helper combines both
18
+ LazyChart = lazy("./Chart")
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import ast
24
+ from dataclasses import dataclass
25
+ from typing import TYPE_CHECKING, override
26
+
27
+ from pulse.transpiler.assets import LocalAsset, register_local_asset
28
+ from pulse.transpiler.errors import TranspileError
29
+ from pulse.transpiler.imports import is_local_path, resolve_local_path
30
+ from pulse.transpiler.nodes import Expr, Member
31
+
32
+ if TYPE_CHECKING:
33
+ from pulse.transpiler.transpiler import Transpiler
34
+
35
+
36
+ @dataclass(slots=True)
37
+ class DynamicImport(Expr):
38
+ """Represents a dynamic import() expression.
39
+
40
+ Emits as: import("src")
41
+
42
+ Supports method chaining for .then():
43
+ import_("./foo").then(lambda m: m.bar)
44
+ -> import("./foo").then(m => m.bar)
45
+ """
46
+
47
+ src: str
48
+ asset: LocalAsset | None = None
49
+
50
+ @override
51
+ def emit(self, out: list[str]) -> None:
52
+ if self.asset:
53
+ out.append(f'import("{self.asset.import_path()}")')
54
+ else:
55
+ out.append(f'import("{self.src}")')
56
+
57
+ @override
58
+ def render(self):
59
+ raise TypeError("DynamicImport cannot be rendered to VDOM")
60
+
61
+ @override
62
+ def transpile_getattr(self, attr: str, ctx: Transpiler) -> Expr:
63
+ """Allow .then() and other method chaining."""
64
+ return Member(self, attr)
65
+
66
+
67
+ class DynamicImportFn(Expr):
68
+ """Sentinel expr that intercepts import_() calls.
69
+
70
+ When used in a @javascript function:
71
+ import_("./module")
72
+
73
+ Transpiles to:
74
+ import("./module")
75
+
76
+ For local paths, resolves the file and registers it for asset copying.
77
+ """
78
+
79
+ @override
80
+ def emit(self, out: list[str]) -> None:
81
+ raise TypeError(
82
+ "import_ cannot be emitted directly - call it with a source path"
83
+ )
84
+
85
+ @override
86
+ def render(self):
87
+ raise TypeError("import_ cannot be rendered to VDOM")
88
+
89
+ @override
90
+ def transpile_call(
91
+ self,
92
+ args: list[ast.expr],
93
+ keywords: list[ast.keyword],
94
+ ctx: Transpiler,
95
+ ) -> Expr:
96
+ """Handle import_("source") calls."""
97
+ if keywords:
98
+ raise TranspileError("import_() does not accept keyword arguments")
99
+ if len(args) != 1:
100
+ raise TranspileError("import_() takes exactly 1 argument")
101
+
102
+ # Extract string literal from AST
103
+ src_node = args[0]
104
+ if not isinstance(src_node, ast.Constant) or not isinstance(
105
+ src_node.value, str
106
+ ):
107
+ raise TranspileError("import_() argument must be a string literal")
108
+
109
+ src = src_node.value
110
+ asset: LocalAsset | None = None
111
+
112
+ # Resolve local paths and register asset
113
+ if is_local_path(src):
114
+ if ctx.source_file is None:
115
+ raise TranspileError(
116
+ "Cannot resolve relative import_() path: source file unknown"
117
+ )
118
+ source_path = resolve_local_path(src, ctx.source_file)
119
+ if source_path:
120
+ asset = register_local_asset(source_path)
121
+ else:
122
+ raise TranspileError(
123
+ f"import_({src!r}) references a local path that does not exist"
124
+ )
125
+
126
+ return DynamicImport(src, asset)
127
+
128
+
129
+ # Singleton for use in deps
130
+ import_ = DynamicImportFn()
@@ -0,0 +1,49 @@
1
+ """Emit context for code generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextvars import ContextVar, Token
6
+ from dataclasses import dataclass, field
7
+ from types import TracebackType
8
+ from typing import Literal
9
+
10
+
11
+ @dataclass
12
+ class EmitContext:
13
+ """Context for emit operations during route code generation.
14
+
15
+ Stores information about the current route file being generated,
16
+ allowing emit methods to compute correct relative paths.
17
+
18
+ Usage:
19
+ with EmitContext(route_file_path="routes/users/index.tsx"):
20
+ js_code = emit(fn.transpile())
21
+ """
22
+
23
+ route_file_path: str
24
+ """Path to route file from pulse folder root, e.g. 'routes/users/index.tsx'"""
25
+
26
+ _token: Token[EmitContext | None] | None = field(default=None, repr=False)
27
+
28
+ @classmethod
29
+ def get(cls) -> EmitContext | None:
30
+ """Get current emit context, or None if not set."""
31
+ return _EMIT_CONTEXT.get()
32
+
33
+ def __enter__(self) -> EmitContext:
34
+ self._token = _EMIT_CONTEXT.set(self)
35
+ return self
36
+
37
+ def __exit__(
38
+ self,
39
+ exc_type: type[BaseException] | None = None,
40
+ exc_val: BaseException | None = None,
41
+ exc_tb: TracebackType | None = None,
42
+ ) -> Literal[False]:
43
+ if self._token is not None:
44
+ _EMIT_CONTEXT.reset(self._token)
45
+ self._token = None
46
+ return False
47
+
48
+
49
+ _EMIT_CONTEXT: ContextVar[EmitContext | None] = ContextVar("emit_context", default=None)
@@ -0,0 +1,96 @@
1
+ """Transpiler-specific error classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+
7
+
8
+ class TranspileError(Exception):
9
+ """Error during transpilation with optional source location."""
10
+
11
+ message: str
12
+ node: ast.expr | ast.stmt | ast.excepthandler | None
13
+ source: str | None
14
+ filename: str | None
15
+ func_name: str | None
16
+ source_start_line: int | None
17
+
18
+ def __init__(
19
+ self,
20
+ message: str,
21
+ *,
22
+ node: ast.expr | ast.stmt | ast.excepthandler | None = None,
23
+ source: str | None = None,
24
+ filename: str | None = None,
25
+ func_name: str | None = None,
26
+ source_start_line: int | None = None,
27
+ ) -> None:
28
+ self.message = message
29
+ self.node = node
30
+ self.source = source
31
+ self.filename = filename
32
+ self.func_name = func_name
33
+ self.source_start_line = source_start_line
34
+ super().__init__(self._format_message())
35
+
36
+ def _format_message(self) -> str:
37
+ """Format the error message with source location if available."""
38
+ parts = [self.message]
39
+
40
+ if self.node is not None and hasattr(self.node, "lineno"):
41
+ loc_parts: list[str] = []
42
+ if self.func_name:
43
+ loc_parts.append(f"in {self.func_name}")
44
+ display_lineno = self.node.lineno
45
+ if self.source_start_line is not None:
46
+ display_lineno = self.source_start_line + self.node.lineno - 1
47
+ if self.filename:
48
+ loc_parts.append(f"at {self.filename}:{display_lineno}")
49
+ else:
50
+ loc_parts.append(f"at line {display_lineno}")
51
+
52
+ display_line = None
53
+ display_col = None
54
+ if self.source:
55
+ lines = self.source.splitlines()
56
+ if 0 < self.node.lineno <= len(lines):
57
+ source_line = lines[self.node.lineno - 1]
58
+ display_line = source_line.expandtabs(4)
59
+ if hasattr(self.node, "col_offset"):
60
+ prefix = source_line[: self.node.col_offset]
61
+ display_col = len(prefix.expandtabs(4))
62
+
63
+ if hasattr(self.node, "col_offset"):
64
+ col = display_col if display_col is not None else self.node.col_offset
65
+ loc_parts[-1] += f":{col}"
66
+
67
+ if loc_parts:
68
+ parts.append(" ".join(loc_parts))
69
+
70
+ # Show the source line if available
71
+ if display_line is not None:
72
+ parts.append(f"\n {display_line}")
73
+ # Add caret pointing to column
74
+ if display_col is not None:
75
+ parts.append(" " + " " * display_col + "^")
76
+
77
+ return "\n".join(parts) if len(parts) > 1 else parts[0]
78
+
79
+ def with_context(
80
+ self,
81
+ *,
82
+ node: ast.expr | ast.stmt | ast.excepthandler | None = None,
83
+ source: str | None = None,
84
+ filename: str | None = None,
85
+ func_name: str | None = None,
86
+ source_start_line: int | None = None,
87
+ ) -> TranspileError:
88
+ """Return a new TranspileError with additional context."""
89
+ return TranspileError(
90
+ self.message,
91
+ node=node or self.node,
92
+ source=source or self.source,
93
+ filename=filename or self.filename,
94
+ func_name=func_name or self.func_name,
95
+ source_start_line=source_start_line or self.source_start_line,
96
+ )