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
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)