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.
- pulse/__init__.py +1493 -0
- pulse/_examples.py +29 -0
- pulse/app.py +1086 -0
- pulse/channel.py +607 -0
- pulse/cli/__init__.py +0 -0
- pulse/cli/cmd.py +575 -0
- pulse/cli/dependencies.py +181 -0
- pulse/cli/folder_lock.py +134 -0
- pulse/cli/helpers.py +271 -0
- pulse/cli/logging.py +102 -0
- pulse/cli/models.py +35 -0
- pulse/cli/packages.py +262 -0
- pulse/cli/processes.py +292 -0
- pulse/cli/secrets.py +39 -0
- pulse/cli/uvicorn_log_config.py +87 -0
- pulse/code_analysis.py +38 -0
- pulse/codegen/__init__.py +0 -0
- pulse/codegen/codegen.py +359 -0
- pulse/codegen/templates/__init__.py +0 -0
- pulse/codegen/templates/layout.py +106 -0
- pulse/codegen/templates/route.py +345 -0
- pulse/codegen/templates/routes_ts.py +42 -0
- pulse/codegen/utils.py +20 -0
- pulse/component.py +237 -0
- pulse/components/__init__.py +0 -0
- pulse/components/for_.py +83 -0
- pulse/components/if_.py +86 -0
- pulse/components/react_router.py +94 -0
- pulse/context.py +108 -0
- pulse/cookies.py +322 -0
- pulse/decorators.py +344 -0
- pulse/dom/__init__.py +0 -0
- pulse/dom/elements.py +1024 -0
- pulse/dom/events.py +445 -0
- pulse/dom/props.py +1250 -0
- pulse/dom/svg.py +0 -0
- pulse/dom/tags.py +328 -0
- pulse/dom/tags.pyi +480 -0
- pulse/env.py +178 -0
- pulse/form.py +538 -0
- pulse/helpers.py +541 -0
- pulse/hooks/__init__.py +0 -0
- pulse/hooks/core.py +452 -0
- pulse/hooks/effects.py +88 -0
- pulse/hooks/init.py +668 -0
- pulse/hooks/runtime.py +464 -0
- pulse/hooks/setup.py +254 -0
- pulse/hooks/stable.py +138 -0
- pulse/hooks/state.py +192 -0
- pulse/js/__init__.py +125 -0
- pulse/js/__init__.pyi +115 -0
- pulse/js/_types.py +299 -0
- pulse/js/array.py +339 -0
- pulse/js/console.py +50 -0
- pulse/js/date.py +119 -0
- pulse/js/document.py +145 -0
- pulse/js/error.py +140 -0
- pulse/js/json.py +66 -0
- pulse/js/map.py +97 -0
- pulse/js/math.py +69 -0
- pulse/js/navigator.py +79 -0
- pulse/js/number.py +57 -0
- pulse/js/obj.py +81 -0
- pulse/js/object.py +172 -0
- pulse/js/promise.py +172 -0
- pulse/js/pulse.py +115 -0
- pulse/js/react.py +495 -0
- pulse/js/regexp.py +57 -0
- pulse/js/set.py +124 -0
- pulse/js/string.py +38 -0
- pulse/js/weakmap.py +53 -0
- pulse/js/weakset.py +48 -0
- pulse/js/window.py +205 -0
- pulse/messages.py +202 -0
- pulse/middleware.py +471 -0
- pulse/plugin.py +96 -0
- pulse/proxy.py +242 -0
- pulse/py.typed +0 -0
- pulse/queries/__init__.py +0 -0
- pulse/queries/client.py +609 -0
- pulse/queries/common.py +101 -0
- pulse/queries/effect.py +55 -0
- pulse/queries/infinite_query.py +1418 -0
- pulse/queries/mutation.py +295 -0
- pulse/queries/protocol.py +136 -0
- pulse/queries/query.py +1314 -0
- pulse/queries/store.py +120 -0
- pulse/react_component.py +88 -0
- pulse/reactive.py +1208 -0
- pulse/reactive_extensions.py +1172 -0
- pulse/render_session.py +768 -0
- pulse/renderer.py +584 -0
- pulse/request.py +205 -0
- pulse/routing.py +598 -0
- pulse/serializer.py +279 -0
- pulse/state.py +556 -0
- pulse/test_helpers.py +15 -0
- pulse/transpiler/__init__.py +111 -0
- pulse/transpiler/assets.py +81 -0
- pulse/transpiler/builtins.py +1029 -0
- pulse/transpiler/dynamic_import.py +130 -0
- pulse/transpiler/emit_context.py +49 -0
- pulse/transpiler/errors.py +96 -0
- pulse/transpiler/function.py +611 -0
- pulse/transpiler/id.py +18 -0
- pulse/transpiler/imports.py +341 -0
- pulse/transpiler/js_module.py +336 -0
- pulse/transpiler/modules/__init__.py +33 -0
- pulse/transpiler/modules/asyncio.py +57 -0
- pulse/transpiler/modules/json.py +24 -0
- pulse/transpiler/modules/math.py +265 -0
- pulse/transpiler/modules/pulse/__init__.py +5 -0
- pulse/transpiler/modules/pulse/tags.py +250 -0
- pulse/transpiler/modules/typing.py +63 -0
- pulse/transpiler/nodes.py +1987 -0
- pulse/transpiler/py_module.py +135 -0
- pulse/transpiler/transpiler.py +1100 -0
- pulse/transpiler/vdom.py +256 -0
- pulse/types/__init__.py +0 -0
- pulse/types/event_handler.py +50 -0
- pulse/user_session.py +386 -0
- pulse/version.py +69 -0
- pulse_framework-0.1.62.dist-info/METADATA +198 -0
- pulse_framework-0.1.62.dist-info/RECORD +126 -0
- pulse_framework-0.1.62.dist-info/WHEEL +4 -0
- 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
|
+
)
|