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
pulse/serializer.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Pulse serializer v3 implementation (Python).
|
|
2
|
+
|
|
3
|
+
The format mirrors the TypeScript implementation in ``packages/pulse/js``.
|
|
4
|
+
|
|
5
|
+
Serialized payload structure::
|
|
6
|
+
|
|
7
|
+
(
|
|
8
|
+
("refs|dates|sets|maps", payload),
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
- The first element is a compact metadata string with four pipe-separated
|
|
12
|
+
comma-separated integer lists representing global node indices for:
|
|
13
|
+
``refs``, ``dates``, ``sets``, ``maps``.
|
|
14
|
+
- ``refs`` – indices where the payload entry is an integer pointing to a
|
|
15
|
+
previously visited node's index (shared refs/cycles).
|
|
16
|
+
- ``dates`` – indices that should be materialised as ``datetime`` objects; the
|
|
17
|
+
payload entry is the millisecond timestamp since the Unix epoch (UTC).
|
|
18
|
+
- ``sets`` – indices that are ``set`` instances; payload is an array of their
|
|
19
|
+
items.
|
|
20
|
+
- ``maps`` – indices that are ``Map`` instances; payload is an object mapping
|
|
21
|
+
string keys to child payloads. Python reconstructs these as ``dict``.
|
|
22
|
+
|
|
23
|
+
Nodes are assigned a single global index as they are visited (non-primitives
|
|
24
|
+
only). This preserves shared references and cycles across nested structures
|
|
25
|
+
containing primitives, lists/tuples, ``dict``/plain objects, ``set`` and
|
|
26
|
+
``datetime`` objects.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import datetime as dt
|
|
32
|
+
import math
|
|
33
|
+
import types
|
|
34
|
+
from dataclasses import fields, is_dataclass
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
Primitive = int | float | str | bool | None
|
|
38
|
+
PlainJSON = Primitive | list["PlainJSON"] | dict[str, "PlainJSON"]
|
|
39
|
+
Serialized = tuple[tuple[list[int], list[int], list[int], list[int]], PlainJSON]
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"serialize",
|
|
43
|
+
"deserialize",
|
|
44
|
+
"Serialized",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def serialize(data: Any) -> Serialized:
|
|
49
|
+
"""Serialize a Python value to wire format.
|
|
50
|
+
|
|
51
|
+
Converts Python values to a JSON-compatible format with metadata for
|
|
52
|
+
preserving types like datetime, set, and shared references.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
data: Value to serialize.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Serialized tuple containing metadata and JSON payload.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
TypeError: For unsupported types (functions, modules, classes).
|
|
62
|
+
ValueError: For Infinity float values.
|
|
63
|
+
|
|
64
|
+
Supported types:
|
|
65
|
+
- Primitives: None, bool, int, float, str
|
|
66
|
+
- Collections: list, tuple, dict, set
|
|
67
|
+
- datetime.datetime (converted to milliseconds since Unix epoch)
|
|
68
|
+
- Dataclasses (serialized as dict of fields)
|
|
69
|
+
- Objects with __dict__ (public attributes only)
|
|
70
|
+
|
|
71
|
+
Notes:
|
|
72
|
+
- NaN floats serialize as None
|
|
73
|
+
- Infinity raises ValueError
|
|
74
|
+
- Dict keys must be strings
|
|
75
|
+
- Private attributes (starting with _) are excluded
|
|
76
|
+
- Shared references and cycles are preserved
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
```python
|
|
80
|
+
from datetime import datetime
|
|
81
|
+
import pulse as ps
|
|
82
|
+
|
|
83
|
+
data = {
|
|
84
|
+
"name": "Alice",
|
|
85
|
+
"created": datetime.now(),
|
|
86
|
+
"tags": {"admin", "user"},
|
|
87
|
+
}
|
|
88
|
+
serialized = ps.serialize(data)
|
|
89
|
+
```
|
|
90
|
+
"""
|
|
91
|
+
# Map object id -> assigned global index
|
|
92
|
+
seen: dict[int, int] = {}
|
|
93
|
+
refs: list[int] = []
|
|
94
|
+
dates: list[int] = []
|
|
95
|
+
sets: list[int] = []
|
|
96
|
+
maps: list[int] = []
|
|
97
|
+
|
|
98
|
+
global_index = 0
|
|
99
|
+
|
|
100
|
+
def process(value: Any) -> PlainJSON:
|
|
101
|
+
nonlocal global_index
|
|
102
|
+
if value is None or isinstance(value, (bool, int, str)):
|
|
103
|
+
return value
|
|
104
|
+
if isinstance(value, float):
|
|
105
|
+
if math.isnan(value):
|
|
106
|
+
return None # NaN → None (matches pandas None ↔ NaN semantics)
|
|
107
|
+
if math.isinf(value):
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"Cannot serialize {value}: Infinity is not valid JSON. "
|
|
110
|
+
+ "Replace with None or a sentinel value."
|
|
111
|
+
)
|
|
112
|
+
return value
|
|
113
|
+
|
|
114
|
+
idx = global_index
|
|
115
|
+
global_index += 1
|
|
116
|
+
|
|
117
|
+
obj_id = id(value)
|
|
118
|
+
prev_ref = seen.get(obj_id)
|
|
119
|
+
if prev_ref is not None:
|
|
120
|
+
refs.append(idx)
|
|
121
|
+
return prev_ref
|
|
122
|
+
seen[obj_id] = idx
|
|
123
|
+
|
|
124
|
+
if isinstance(value, dt.datetime):
|
|
125
|
+
dates.append(idx)
|
|
126
|
+
return _datetime_to_millis(value)
|
|
127
|
+
|
|
128
|
+
if isinstance(value, dict):
|
|
129
|
+
result_dict: dict[str, PlainJSON] = {}
|
|
130
|
+
for key, entry in value.items():
|
|
131
|
+
if not isinstance(key, str):
|
|
132
|
+
raise TypeError(
|
|
133
|
+
f"Dict keys must be strings, got {type(key).__name__}: {key!r}" # pyright: ignore[reportUnknownArgumentType]
|
|
134
|
+
)
|
|
135
|
+
result_dict[key] = process(entry)
|
|
136
|
+
return result_dict
|
|
137
|
+
|
|
138
|
+
if isinstance(value, (list, tuple)):
|
|
139
|
+
result_list: list[PlainJSON] = []
|
|
140
|
+
for entry in value:
|
|
141
|
+
result_list.append(process(entry))
|
|
142
|
+
return result_list
|
|
143
|
+
|
|
144
|
+
if isinstance(value, set):
|
|
145
|
+
sets.append(idx)
|
|
146
|
+
items: list[PlainJSON] = []
|
|
147
|
+
for entry in value:
|
|
148
|
+
items.append(process(entry))
|
|
149
|
+
return items
|
|
150
|
+
|
|
151
|
+
if is_dataclass(value):
|
|
152
|
+
dc_obj: dict[str, PlainJSON] = {}
|
|
153
|
+
for f in fields(value):
|
|
154
|
+
dc_obj[f.name] = process(getattr(value, f.name))
|
|
155
|
+
return dc_obj
|
|
156
|
+
|
|
157
|
+
if callable(value) or isinstance(value, (type, types.ModuleType)):
|
|
158
|
+
raise TypeError(f"Unsupported value in serialization: {type(value)!r}")
|
|
159
|
+
|
|
160
|
+
if hasattr(value, "__dict__"):
|
|
161
|
+
inst_obj: dict[str, PlainJSON] = {}
|
|
162
|
+
for key, entry in vars(value).items():
|
|
163
|
+
if key.startswith("_"):
|
|
164
|
+
continue
|
|
165
|
+
inst_obj[key] = process(entry)
|
|
166
|
+
return inst_obj
|
|
167
|
+
|
|
168
|
+
raise TypeError(f"Unsupported value in serialization: {type(value)!r}")
|
|
169
|
+
|
|
170
|
+
payload = process(data)
|
|
171
|
+
|
|
172
|
+
return ((refs, dates, sets, maps), payload)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def deserialize(
|
|
176
|
+
payload: Serialized,
|
|
177
|
+
) -> Any:
|
|
178
|
+
"""Deserialize wire format back to Python values.
|
|
179
|
+
|
|
180
|
+
Reconstructs Python values from the serialized format, restoring
|
|
181
|
+
datetime objects, sets, and shared references.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
payload: Serialized tuple from serialize().
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Reconstructed Python value.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
TypeError: For malformed payloads.
|
|
191
|
+
|
|
192
|
+
Notes:
|
|
193
|
+
- datetime values are reconstructed as UTC-aware
|
|
194
|
+
- set values are reconstructed as Python sets
|
|
195
|
+
- Shared references and cycles are restored
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
```python
|
|
199
|
+
from datetime import datetime
|
|
200
|
+
import pulse as ps
|
|
201
|
+
|
|
202
|
+
original = {"items": [1, 2, 3], "timestamp": datetime.now()}
|
|
203
|
+
serialized = ps.serialize(original)
|
|
204
|
+
restored = ps.deserialize(serialized)
|
|
205
|
+
```
|
|
206
|
+
"""
|
|
207
|
+
(refs, dates, sets, _maps), data = payload
|
|
208
|
+
refs = set(refs)
|
|
209
|
+
dates = set(dates)
|
|
210
|
+
sets = set(sets)
|
|
211
|
+
# we don't care about maps
|
|
212
|
+
|
|
213
|
+
objects: list[Any] = []
|
|
214
|
+
|
|
215
|
+
def reconstruct(value: PlainJSON) -> Any:
|
|
216
|
+
idx = len(objects)
|
|
217
|
+
|
|
218
|
+
if idx in refs:
|
|
219
|
+
assert isinstance(value, (int, float)), (
|
|
220
|
+
"Reference payload must be numeric index"
|
|
221
|
+
)
|
|
222
|
+
# Placeholder to keep indices aligned
|
|
223
|
+
objects.append(None)
|
|
224
|
+
target_index = int(value)
|
|
225
|
+
assert 0 <= target_index < len(objects), (
|
|
226
|
+
f"Dangling reference to index {target_index}"
|
|
227
|
+
)
|
|
228
|
+
return objects[target_index]
|
|
229
|
+
|
|
230
|
+
if idx in dates:
|
|
231
|
+
assert isinstance(value, (int, float)), (
|
|
232
|
+
"Date payload must be a numeric timestamp"
|
|
233
|
+
)
|
|
234
|
+
dt_value = _datetime_from_millis(value)
|
|
235
|
+
objects.append(dt_value)
|
|
236
|
+
return dt_value
|
|
237
|
+
|
|
238
|
+
if value is None:
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
if isinstance(value, (bool, int, float, str)):
|
|
242
|
+
return value
|
|
243
|
+
|
|
244
|
+
if isinstance(value, list):
|
|
245
|
+
if idx in sets:
|
|
246
|
+
result_set: set[Any] = set()
|
|
247
|
+
objects.append(result_set)
|
|
248
|
+
for entry in value:
|
|
249
|
+
result_set.add(reconstruct(entry))
|
|
250
|
+
return result_set
|
|
251
|
+
result_list: list[Any] = []
|
|
252
|
+
objects.append(result_list)
|
|
253
|
+
for entry in value:
|
|
254
|
+
result_list.append(reconstruct(entry))
|
|
255
|
+
return result_list
|
|
256
|
+
|
|
257
|
+
if isinstance(value, dict):
|
|
258
|
+
# Both maps and records are reconstructed as dictionaries in Python
|
|
259
|
+
result_dict: dict[str, Any] = {}
|
|
260
|
+
objects.append(result_dict)
|
|
261
|
+
for key, entry in value.items():
|
|
262
|
+
result_dict[str(key)] = reconstruct(entry)
|
|
263
|
+
return result_dict
|
|
264
|
+
|
|
265
|
+
raise TypeError(f"Unsupported value in deserialization: {type(value)!r}")
|
|
266
|
+
|
|
267
|
+
return reconstruct(data)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _datetime_to_millis(value: dt.datetime) -> int:
|
|
271
|
+
if value.tzinfo is None:
|
|
272
|
+
ts = value.replace(tzinfo=dt.UTC).timestamp()
|
|
273
|
+
else:
|
|
274
|
+
ts = value.astimezone(dt.UTC).timestamp()
|
|
275
|
+
return int(round(ts * 1000))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _datetime_from_millis(value: int | float) -> dt.datetime:
|
|
279
|
+
return dt.datetime.fromtimestamp(value / 1000.0, tz=dt.UTC)
|