pulse-framework 0.1.52__tar.gz → 0.1.54__tar.gz

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_framework-0.1.54/PKG-INFO +196 -0
  2. pulse_framework-0.1.54/README.md +178 -0
  3. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/pyproject.toml +1 -1
  4. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/__init__.py +3 -3
  5. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/app.py +34 -20
  6. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/components/for_.py +17 -2
  7. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/cookies.py +38 -2
  8. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/env.py +4 -4
  9. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/hooks/init.py +174 -14
  10. pulse_framework-0.1.54/src/pulse/hooks/state.py +105 -0
  11. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/__init__.py +12 -9
  12. pulse_framework-0.1.54/src/pulse/js/obj.py +79 -0
  13. pulse_framework-0.1.54/src/pulse/js/pulse.py +112 -0
  14. pulse_framework-0.1.54/src/pulse/js/react.py +350 -0
  15. pulse_framework-0.1.54/src/pulse/js/react_dom.py +30 -0
  16. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/messages.py +13 -13
  17. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/proxy.py +18 -5
  18. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/render_session.py +282 -266
  19. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/renderer.py +36 -73
  20. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/serializer.py +5 -2
  21. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/builtins.py +0 -20
  22. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/errors.py +29 -11
  23. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/function.py +30 -3
  24. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/js_module.py +9 -12
  25. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/modules/pulse/tags.py +35 -15
  26. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/nodes.py +121 -36
  27. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/py_module.py +1 -1
  28. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/transpiler.py +28 -26
  29. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/user_session.py +10 -0
  30. pulse_framework-0.1.52/PKG-INFO +0 -18
  31. pulse_framework-0.1.52/README.md +0 -0
  32. pulse_framework-0.1.52/src/pulse/hooks/states.py +0 -285
  33. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/_examples.py +0 -0
  34. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/channel.py +0 -0
  35. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/cli/__init__.py +0 -0
  36. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/cli/cmd.py +0 -0
  37. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/cli/dependencies.py +0 -0
  38. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/cli/folder_lock.py +0 -0
  39. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/cli/helpers.py +0 -0
  40. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/cli/logging.py +0 -0
  41. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/cli/models.py +0 -0
  42. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/cli/packages.py +0 -0
  43. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/cli/processes.py +0 -0
  44. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/cli/secrets.py +0 -0
  45. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/cli/uvicorn_log_config.py +0 -0
  46. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/codegen/__init__.py +0 -0
  47. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/codegen/codegen.py +0 -0
  48. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/codegen/js.py +0 -0
  49. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/codegen/templates/__init__.py +0 -0
  50. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/codegen/templates/layout.py +0 -0
  51. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/codegen/templates/route.py +0 -0
  52. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/codegen/templates/routes_ts.py +0 -0
  53. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/codegen/utils.py +0 -0
  54. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/component.py +0 -0
  55. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/components/__init__.py +0 -0
  56. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/components/if_.py +0 -0
  57. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/components/react_router.py +0 -0
  58. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/context.py +0 -0
  59. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/decorators.py +0 -0
  60. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/dom/__init__.py +0 -0
  61. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/dom/elements.py +0 -0
  62. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/dom/events.py +0 -0
  63. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/dom/props.py +0 -0
  64. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/dom/svg.py +0 -0
  65. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/dom/tags.py +0 -0
  66. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/dom/tags.pyi +0 -0
  67. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/form.py +0 -0
  68. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/helpers.py +0 -0
  69. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/hooks/__init__.py +0 -0
  70. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/hooks/core.py +0 -0
  71. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/hooks/effects.py +0 -0
  72. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/hooks/runtime.py +0 -0
  73. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/hooks/setup.py +0 -0
  74. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/hooks/stable.py +0 -0
  75. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/__init__.pyi +0 -0
  76. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/_types.py +0 -0
  77. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/array.py +0 -0
  78. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/console.py +0 -0
  79. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/date.py +0 -0
  80. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/document.py +0 -0
  81. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/error.py +0 -0
  82. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/json.py +0 -0
  83. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/map.py +0 -0
  84. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/math.py +0 -0
  85. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/navigator.py +0 -0
  86. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/number.py +0 -0
  87. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/object.py +0 -0
  88. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/promise.py +0 -0
  89. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/regexp.py +0 -0
  90. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/set.py +0 -0
  91. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/string.py +0 -0
  92. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/weakmap.py +0 -0
  93. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/weakset.py +0 -0
  94. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/js/window.py +0 -0
  95. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/middleware.py +0 -0
  96. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/plugin.py +0 -0
  97. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/py.typed +0 -0
  98. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/queries/__init__.py +0 -0
  99. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/queries/client.py +0 -0
  100. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/queries/common.py +0 -0
  101. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/queries/effect.py +0 -0
  102. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/queries/infinite_query.py +0 -0
  103. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/queries/mutation.py +0 -0
  104. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/queries/protocol.py +0 -0
  105. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/queries/query.py +0 -0
  106. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/queries/store.py +0 -0
  107. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/react_component.py +0 -0
  108. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/reactive.py +0 -0
  109. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/reactive_extensions.py +0 -0
  110. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/request.py +0 -0
  111. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/routing.py +0 -0
  112. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/state.py +0 -0
  113. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/__init__.py +0 -0
  114. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/id.py +0 -0
  115. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/imports.py +0 -0
  116. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/modules/__init__.py +0 -0
  117. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/modules/asyncio.py +0 -0
  118. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/modules/json.py +0 -0
  119. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/modules/math.py +0 -0
  120. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/modules/pulse/__init__.py +0 -0
  121. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/modules/typing.py +0 -0
  122. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/react_component.py +0 -0
  123. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/transpiler/vdom.py +0 -0
  124. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/types/__init__.py +0 -0
  125. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/types/event_handler.py +0 -0
  126. {pulse_framework-0.1.52 → pulse_framework-0.1.54}/src/pulse/version.py +0 -0
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.3
2
+ Name: pulse-framework
3
+ Version: 0.1.54
4
+ Summary: Pulse - Full-stack framework for building real-time React applications in Python
5
+ Requires-Dist: websockets>=12.0
6
+ Requires-Dist: fastapi>=0.104.0
7
+ Requires-Dist: uvicorn>=0.24.0
8
+ Requires-Dist: mako>=1.3.10
9
+ Requires-Dist: typer>=0.16.0
10
+ Requires-Dist: python-socketio>=5.13.0
11
+ Requires-Dist: rich>=13.7.1
12
+ Requires-Dist: python-multipart>=0.0.20
13
+ Requires-Dist: python-dateutil>=2.9.0.post0
14
+ Requires-Dist: watchfiles>=1.1.0
15
+ Requires-Dist: httpx>=0.28.1
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Pulse Python
20
+
21
+ Core Python framework for building full-stack reactive web apps with React frontends.
22
+
23
+ ## Architecture
24
+
25
+ Server-driven UI model: Python components render to VDOM, synced to React via WebSocket. State changes trigger re-renders; diffs are sent to client.
26
+
27
+ ```
28
+ ┌─────────────────────────────────────────────────────────────────┐
29
+ │ Python Server │
30
+ │ ┌──────────┐ ┌───────────────┐ ┌──────────────────────────┐ │
31
+ │ │ App │──│ RenderSession │──│ VDOM Renderer │ │
32
+ │ │ (FastAPI)│ │ (per browser) │ │ (diff & serialize) │ │
33
+ │ └──────────┘ └───────────────┘ └──────────────────────────┘ │
34
+ │ │ │ │ │
35
+ │ │ ┌──────┴───────┐ │ │
36
+ │ │ │ Hooks │ │ │
37
+ │ │ │ (state/setup)│ │ │
38
+ │ │ └──────────────┘ │ │
39
+ └───────┼───────────────────────────────────────┼─────────────────┘
40
+ │ Socket.IO │ VDOM updates
41
+ ▼ ▼
42
+ ┌─────────────────────────────────────────────────────────────────┐
43
+ │ Browser (React) │
44
+ └─────────────────────────────────────────────────────────────────┘
45
+ ```
46
+
47
+ ## Folder Structure
48
+
49
+ ```
50
+ src/pulse/
51
+ ├── app.py # Main App class, FastAPI + Socket.IO setup
52
+ ├── channel.py # Bidirectional real-time channels
53
+ ├── routing.py # Route/Layout definitions, URL matching
54
+ ├── vdom.py # VDOM node types (Element, Component, Node)
55
+ ├── renderer.py # VDOM rendering and diffing
56
+ ├── render_session.py # Per-browser session, manages mounted routes
57
+ ├── reactive.py # Signal/Computed/Effect primitives
58
+ ├── reactive_extensions.py # ReactiveList, ReactiveDict, ReactiveSet
59
+ ├── state.py # State management
60
+ ├── serializer.py # Python<->JSON serialization
61
+ ├── middleware.py # Request middleware (prerender, connect, message)
62
+ ├── plugin.py # Plugin interface for extensions
63
+ ├── form.py # Form handling
64
+ ├── context.py # PulseContext (request/session context)
65
+ ├── cookies.py # Cookie management
66
+ ├── request.py # PulseRequest abstraction
67
+ ├── user_session.py # User session storage
68
+ ├── helpers.py # Utilities (CSSProperties, later, repeat)
69
+ ├── decorators.py # @computed, @effect decorators
70
+ ├── messages.py # Client<->server message types
71
+ ├── react_component.py # ReactComponent wrapper for JS libraries
72
+
73
+ ├── hooks/ # Server-side hooks (like React hooks)
74
+ │ ├── core.py # Hook registry, HooksAPI
75
+ │ ├── runtime.py # session(), route(), navigate(), redirect()
76
+ │ ├── states.py # Reactive state hook
77
+ │ ├── effects.py # Side effects hook
78
+ │ ├── setup.py # Initialization hook
79
+ │ ├── init.py # One-time setup hook
80
+ │ └── stable.py # Memoization hook
81
+
82
+ ├── queries/ # Data fetching (like TanStack Query)
83
+ │ ├── query.py # @query decorator
84
+ │ ├── mutation.py # @mutation decorator
85
+ │ ├── infinite_query.py # Pagination support
86
+ │ ├── client.py # QueryClient for cache management
87
+ │ └── store.py # Query state store
88
+
89
+ ├── components/ # Built-in components
90
+ │ ├── for_.py # <For> loop component
91
+ │ ├── if_.py # <If> conditional component
92
+ │ └── react_router.py # Link, Outlet for routing
93
+
94
+ ├── html/ # HTML element bindings
95
+ │ ├── tags.py # div, span, button, etc.
96
+ │ ├── props.py # Typed props for HTML elements
97
+ │ ├── events.py # Event types (MouseEvent, etc.)
98
+ │ └── elements.py # Element type definitions
99
+
100
+ ├── transpiler/ # Python->JS transpilation
101
+ │ ├── function.py # JsFunction, @javascript decorator
102
+ │ └── imports.py # Import/CssImport for client-side JS
103
+
104
+ ├── codegen/ # Code generation for React Router
105
+ │ ├── codegen.py # Generates routes.ts, loaders
106
+ │ └── templates/ # Mako templates for generated code
107
+
108
+ ├── cli/ # Command-line interface
109
+ │ ├── cmd.py # pulse run, pulse build
110
+ │ └── processes.py # Dev server process management
111
+
112
+ └── js/ # JS API stubs for transpilation
113
+ ├── window.py, document.py, navigator.py
114
+ ├── array.py, object.py, string.py
115
+ └── ...
116
+ ```
117
+
118
+ ## Key Concepts
119
+
120
+ ### App
121
+
122
+ Entry point defining routes, middleware, plugins.
123
+
124
+ ```python
125
+ import pulse as ps
126
+
127
+ app = ps.App(routes=[
128
+ ps.Route("/", home),
129
+ ps.Layout("/dashboard", layout, children=[
130
+ ps.Route("/", dashboard),
131
+ ]),
132
+ ])
133
+ ```
134
+
135
+ ### Components
136
+
137
+ Functions returning VDOM. Use `@ps.component` for stateful components.
138
+
139
+ ```python
140
+ def greeting(name: str):
141
+ return ps.div(f"Hello, {name}!")
142
+
143
+ @ps.component
144
+ def counter():
145
+ count = ps.states.use(0)
146
+ return ps.button(f"Count: {count()}", onClick=lambda _: count.set(count() + 1))
147
+ ```
148
+
149
+ ### Reactivity
150
+
151
+ - `Signal[T]` - reactive value
152
+ - `Computed[T]` - derived value
153
+ - `Effect` - side effect on change
154
+
155
+ ### Hooks
156
+
157
+ Server-side hooks via `ps.states`, `ps.effects`, `ps.setup`:
158
+ - `states.use(initial)` - reactive state
159
+ - `effects.use(fn, deps)` - side effects
160
+ - `setup.use(fn)` - one-time initialization
161
+
162
+ ### Queries
163
+
164
+ Data fetching with caching:
165
+
166
+ ```python
167
+ @ps.query
168
+ async def fetch_user(id: str):
169
+ return await db.get_user(id)
170
+ ```
171
+
172
+ ### Channels
173
+
174
+ Bidirectional real-time messaging:
175
+
176
+ ```python
177
+ ch = ps.channel("chat")
178
+
179
+ @ch.on("message")
180
+ def handle_message(data):
181
+ ch.broadcast("new_message", data)
182
+ ```
183
+
184
+ ## Main Exports
185
+
186
+ - `App`, `Route`, `Layout` - app/routing
187
+ - `component` - server-side component decorator
188
+ - `states`, `effects`, `setup`, `init` - hooks
189
+ - `query`, `mutation`, `infinite_query` - data fetching
190
+ - `channel` - real-time channels
191
+ - `State`, `@computed`, `@effect` - reactivity
192
+ - `ReactiveList`, `ReactiveDict`, `ReactiveSet` - reactive containers
193
+ - `div`, `span`, `button`, ... - HTML elements
194
+ - `For`, `If`, `Link`, `Outlet` - built-in components
195
+ - `@react_component` - wrap JS components
196
+ - `@javascript` - transpile Python to JS
@@ -0,0 +1,178 @@
1
+ # Pulse Python
2
+
3
+ Core Python framework for building full-stack reactive web apps with React frontends.
4
+
5
+ ## Architecture
6
+
7
+ Server-driven UI model: Python components render to VDOM, synced to React via WebSocket. State changes trigger re-renders; diffs are sent to client.
8
+
9
+ ```
10
+ ┌─────────────────────────────────────────────────────────────────┐
11
+ │ Python Server │
12
+ │ ┌──────────┐ ┌───────────────┐ ┌──────────────────────────┐ │
13
+ │ │ App │──│ RenderSession │──│ VDOM Renderer │ │
14
+ │ │ (FastAPI)│ │ (per browser) │ │ (diff & serialize) │ │
15
+ │ └──────────┘ └───────────────┘ └──────────────────────────┘ │
16
+ │ │ │ │ │
17
+ │ │ ┌──────┴───────┐ │ │
18
+ │ │ │ Hooks │ │ │
19
+ │ │ │ (state/setup)│ │ │
20
+ │ │ └──────────────┘ │ │
21
+ └───────┼───────────────────────────────────────┼─────────────────┘
22
+ │ Socket.IO │ VDOM updates
23
+ ▼ ▼
24
+ ┌─────────────────────────────────────────────────────────────────┐
25
+ │ Browser (React) │
26
+ └─────────────────────────────────────────────────────────────────┘
27
+ ```
28
+
29
+ ## Folder Structure
30
+
31
+ ```
32
+ src/pulse/
33
+ ├── app.py # Main App class, FastAPI + Socket.IO setup
34
+ ├── channel.py # Bidirectional real-time channels
35
+ ├── routing.py # Route/Layout definitions, URL matching
36
+ ├── vdom.py # VDOM node types (Element, Component, Node)
37
+ ├── renderer.py # VDOM rendering and diffing
38
+ ├── render_session.py # Per-browser session, manages mounted routes
39
+ ├── reactive.py # Signal/Computed/Effect primitives
40
+ ├── reactive_extensions.py # ReactiveList, ReactiveDict, ReactiveSet
41
+ ├── state.py # State management
42
+ ├── serializer.py # Python<->JSON serialization
43
+ ├── middleware.py # Request middleware (prerender, connect, message)
44
+ ├── plugin.py # Plugin interface for extensions
45
+ ├── form.py # Form handling
46
+ ├── context.py # PulseContext (request/session context)
47
+ ├── cookies.py # Cookie management
48
+ ├── request.py # PulseRequest abstraction
49
+ ├── user_session.py # User session storage
50
+ ├── helpers.py # Utilities (CSSProperties, later, repeat)
51
+ ├── decorators.py # @computed, @effect decorators
52
+ ├── messages.py # Client<->server message types
53
+ ├── react_component.py # ReactComponent wrapper for JS libraries
54
+
55
+ ├── hooks/ # Server-side hooks (like React hooks)
56
+ │ ├── core.py # Hook registry, HooksAPI
57
+ │ ├── runtime.py # session(), route(), navigate(), redirect()
58
+ │ ├── states.py # Reactive state hook
59
+ │ ├── effects.py # Side effects hook
60
+ │ ├── setup.py # Initialization hook
61
+ │ ├── init.py # One-time setup hook
62
+ │ └── stable.py # Memoization hook
63
+
64
+ ├── queries/ # Data fetching (like TanStack Query)
65
+ │ ├── query.py # @query decorator
66
+ │ ├── mutation.py # @mutation decorator
67
+ │ ├── infinite_query.py # Pagination support
68
+ │ ├── client.py # QueryClient for cache management
69
+ │ └── store.py # Query state store
70
+
71
+ ├── components/ # Built-in components
72
+ │ ├── for_.py # <For> loop component
73
+ │ ├── if_.py # <If> conditional component
74
+ │ └── react_router.py # Link, Outlet for routing
75
+
76
+ ├── html/ # HTML element bindings
77
+ │ ├── tags.py # div, span, button, etc.
78
+ │ ├── props.py # Typed props for HTML elements
79
+ │ ├── events.py # Event types (MouseEvent, etc.)
80
+ │ └── elements.py # Element type definitions
81
+
82
+ ├── transpiler/ # Python->JS transpilation
83
+ │ ├── function.py # JsFunction, @javascript decorator
84
+ │ └── imports.py # Import/CssImport for client-side JS
85
+
86
+ ├── codegen/ # Code generation for React Router
87
+ │ ├── codegen.py # Generates routes.ts, loaders
88
+ │ └── templates/ # Mako templates for generated code
89
+
90
+ ├── cli/ # Command-line interface
91
+ │ ├── cmd.py # pulse run, pulse build
92
+ │ └── processes.py # Dev server process management
93
+
94
+ └── js/ # JS API stubs for transpilation
95
+ ├── window.py, document.py, navigator.py
96
+ ├── array.py, object.py, string.py
97
+ └── ...
98
+ ```
99
+
100
+ ## Key Concepts
101
+
102
+ ### App
103
+
104
+ Entry point defining routes, middleware, plugins.
105
+
106
+ ```python
107
+ import pulse as ps
108
+
109
+ app = ps.App(routes=[
110
+ ps.Route("/", home),
111
+ ps.Layout("/dashboard", layout, children=[
112
+ ps.Route("/", dashboard),
113
+ ]),
114
+ ])
115
+ ```
116
+
117
+ ### Components
118
+
119
+ Functions returning VDOM. Use `@ps.component` for stateful components.
120
+
121
+ ```python
122
+ def greeting(name: str):
123
+ return ps.div(f"Hello, {name}!")
124
+
125
+ @ps.component
126
+ def counter():
127
+ count = ps.states.use(0)
128
+ return ps.button(f"Count: {count()}", onClick=lambda _: count.set(count() + 1))
129
+ ```
130
+
131
+ ### Reactivity
132
+
133
+ - `Signal[T]` - reactive value
134
+ - `Computed[T]` - derived value
135
+ - `Effect` - side effect on change
136
+
137
+ ### Hooks
138
+
139
+ Server-side hooks via `ps.states`, `ps.effects`, `ps.setup`:
140
+ - `states.use(initial)` - reactive state
141
+ - `effects.use(fn, deps)` - side effects
142
+ - `setup.use(fn)` - one-time initialization
143
+
144
+ ### Queries
145
+
146
+ Data fetching with caching:
147
+
148
+ ```python
149
+ @ps.query
150
+ async def fetch_user(id: str):
151
+ return await db.get_user(id)
152
+ ```
153
+
154
+ ### Channels
155
+
156
+ Bidirectional real-time messaging:
157
+
158
+ ```python
159
+ ch = ps.channel("chat")
160
+
161
+ @ch.on("message")
162
+ def handle_message(data):
163
+ ch.broadcast("new_message", data)
164
+ ```
165
+
166
+ ## Main Exports
167
+
168
+ - `App`, `Route`, `Layout` - app/routing
169
+ - `component` - server-side component decorator
170
+ - `states`, `effects`, `setup`, `init` - hooks
171
+ - `query`, `mutation`, `infinite_query` - data fetching
172
+ - `channel` - real-time channels
173
+ - `State`, `@computed`, `@effect` - reactivity
174
+ - `ReactiveList`, `ReactiveDict`, `ReactiveSet` - reactive containers
175
+ - `div`, `span`, `button`, ... - HTML elements
176
+ - `For`, `If`, `Link`, `Outlet` - built-in components
177
+ - `@react_component` - wrap JS components
178
+ - `@javascript` - transpile Python to JS
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pulse-framework"
3
- version = "0.1.52"
3
+ version = "0.1.54"
4
4
  description = "Pulse - Full-stack framework for building real-time React applications in Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1286,9 +1286,9 @@ from pulse.hooks.stable import (
1286
1286
  stable as stable,
1287
1287
  )
1288
1288
 
1289
- # Hooks - States
1290
- from pulse.hooks.states import StatesHookState as StatesHookState
1291
- from pulse.hooks.states import states as states
1289
+ # Hooks - State
1290
+ from pulse.hooks.state import StateHookState as StateHookState
1291
+ from pulse.hooks.state import state as state
1292
1292
  from pulse.messages import ClientMessage as ClientMessage
1293
1293
  from pulse.messages import Directives as Directives
1294
1294
  from pulse.messages import Prerender as Prerender
@@ -21,6 +21,7 @@ from fastapi import FastAPI, HTTPException, Request, Response
21
21
  from fastapi.middleware.cors import CORSMiddleware
22
22
  from fastapi.responses import JSONResponse
23
23
  from starlette.types import ASGIApp
24
+ from starlette.websockets import WebSocket
24
25
 
25
26
  from pulse.codegen.codegen import Codegen, CodegenConfig
26
27
  from pulse.context import PULSE_CONTEXT, PulseContext
@@ -28,6 +29,7 @@ from pulse.cookies import (
28
29
  Cookie,
29
30
  CORSOptions,
30
31
  compute_cookie_domain,
32
+ compute_cookie_secure,
31
33
  cors_options,
32
34
  session_cookie,
33
35
  )
@@ -364,6 +366,8 @@ class App:
364
366
  # Compute cookie domain from deployment/server address if not explicitly provided
365
367
  if self.cookie.domain is None:
366
368
  self.cookie.domain = compute_cookie_domain(self.mode, self.server_address)
369
+ if self.cookie.secure is None:
370
+ self.cookie.secure = compute_cookie_secure(self.env, self.server_address)
367
371
 
368
372
  # Add CORS middleware (configurable/overridable)
369
373
  if self.cors is not None:
@@ -450,7 +454,7 @@ class App:
450
454
  self._schedule_render_cleanup(render_id)
451
455
 
452
456
  async def _prerender_one(path: str):
453
- captured = render.prerender_mount_capture(path, route_info)
457
+ captured = render.prerender(path, route_info)
454
458
  if captured["type"] == "vdom_init":
455
459
  return Ok(captured)
456
460
  if captured["type"] == "navigate_to":
@@ -578,20 +582,20 @@ class App:
578
582
  server_address=server_address,
579
583
  )
580
584
 
585
+ # In dev mode, proxy WebSocket connections to React Router (e.g. Vite HMR)
586
+ # Socket.IO handles /socket.io/ at ASGI level before reaching FastAPI
587
+ if self.env == "dev":
588
+
589
+ @self.fastapi.websocket("/{path:path}")
590
+ async def websocket_proxy(websocket: WebSocket, path: str): # pyright: ignore[reportUnusedFunction]
591
+ await proxy_handler.proxy_websocket(websocket)
592
+
581
593
  @self.fastapi.api_route(
582
594
  "/{path:path}",
583
595
  methods=["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
584
596
  include_in_schema=False,
585
597
  )
586
598
  async def proxy_catch_all(request: Request, path: str): # pyright: ignore[reportUnusedFunction]
587
- # Skip WebSocket upgrades outside the Vite dev server (handled by Socket.IO)
588
- is_websocket_upgrade = (
589
- request.headers.get("upgrade", "").lower() == "websocket"
590
- )
591
- is_vite_dev_server = self.env == "dev" and request.url.path == "/"
592
- if is_websocket_upgrade and not is_vite_dev_server:
593
- raise HTTPException(status_code=404, detail="Not found")
594
-
595
599
  # Proxy all unmatched HTTP requests to React Router
596
600
  return await proxy_handler(request)
597
601
 
@@ -605,12 +609,14 @@ class App:
605
609
  # Parse cookies from environ and ensure a session exists
606
610
  cookie = self.cookie.get_from_socketio(environ)
607
611
  if cookie is None:
608
- raise ConnectionRefusedError()
612
+ raise ConnectionRefusedError("Socket connect missing cookie")
609
613
  session = await self.get_or_create_session(cookie)
610
614
 
611
615
  if not rid:
612
616
  # Still refuse connections without a renderId
613
- raise ConnectionRefusedError()
617
+ raise ConnectionRefusedError(
618
+ f"Socket connect missing render_id session={session.sid}"
619
+ )
614
620
 
615
621
  # Allow reconnects where the provided renderId no longer exists by creating a new RenderSession
616
622
  render = self.render_sessions.get(rid)
@@ -621,7 +627,10 @@ class App:
621
627
  else:
622
628
  owner = self._render_to_user.get(render.id)
623
629
  if owner != session.sid:
624
- raise ConnectionRefusedError()
630
+ raise ConnectionRefusedError(
631
+ f"Socket connect session mismatch render={render.id} "
632
+ + f"owner={owner} session={session.sid}"
633
+ )
625
634
 
626
635
  def on_message(message: ServerMessage):
627
636
  payload = serialize(message)
@@ -733,14 +742,14 @@ class App:
733
742
  self, render: RenderSession, session: UserSession, msg: ClientPulseMessage
734
743
  ) -> None:
735
744
  async def _next() -> Ok[None]:
736
- if msg["type"] == "mount":
737
- render.mount(msg["path"], msg["routeInfo"])
738
- elif msg["type"] == "navigate":
739
- render.navigate(msg["path"], msg["routeInfo"])
745
+ if msg["type"] == "attach":
746
+ render.attach(msg["path"], msg["routeInfo"])
747
+ elif msg["type"] == "update":
748
+ render.update_route(msg["path"], msg["routeInfo"])
740
749
  elif msg["type"] == "callback":
741
750
  render.execute_callback(msg["path"], msg["callback"], msg["args"])
742
- elif msg["type"] == "unmount":
743
- render.unmount(msg["path"])
751
+ elif msg["type"] == "detach":
752
+ render.detach(msg["path"])
744
753
  render.channels.remove_route(msg["path"])
745
754
  elif msg["type"] == "api_result":
746
755
  render.handle_api_result(dict(msg))
@@ -845,6 +854,11 @@ class App:
845
854
 
846
855
  # Server-backed store path
847
856
  assert isinstance(self.session_store, SessionStore)
857
+ cookie_secure = self.cookie.secure
858
+ if cookie_secure is None:
859
+ raise RuntimeError(
860
+ "Cookie.secure is not resolved. Ensure App.setup() ran before sessions."
861
+ )
848
862
  if raw_cookie is not None:
849
863
  sid = raw_cookie
850
864
  data = await self.session_store.get(sid) or await self.session_store.create(
@@ -855,7 +869,7 @@ class App:
855
869
  name=self.cookie.name,
856
870
  value=sid,
857
871
  domain=self.cookie.domain,
858
- secure=self.cookie.secure,
872
+ secure=cookie_secure,
859
873
  samesite=self.cookie.samesite,
860
874
  max_age_seconds=self.cookie.max_age_seconds,
861
875
  )
@@ -871,7 +885,7 @@ class App:
871
885
  name=self.cookie.name,
872
886
  value=sid,
873
887
  domain=self.cookie.domain,
874
- secure=self.cookie.secure,
888
+ secure=cookie_secure,
875
889
  samesite=self.cookie.samesite,
876
890
  max_age_seconds=self.cookie.max_age_seconds,
877
891
  )
@@ -1,12 +1,23 @@
1
1
  from collections.abc import Callable, Iterable
2
2
  from inspect import Parameter, signature
3
- from typing import TypeVar, overload
3
+ from typing import TYPE_CHECKING, Any, TypeVar, overload
4
4
 
5
- from pulse.transpiler.nodes import Element
5
+ from pulse.transpiler.nodes import Call, Element, Expr, Member, transformer
6
+
7
+ if TYPE_CHECKING:
8
+ from pulse.transpiler.transpiler import Transpiler
6
9
 
7
10
  T = TypeVar("T")
8
11
 
9
12
 
13
+ @transformer("For")
14
+ def emit_for(items: Any, fn: Any, *, ctx: "Transpiler") -> Expr:
15
+ """For(items, fn) -> items.map(fn)"""
16
+ items_expr = ctx.emit_expr(items)
17
+ fn_expr = ctx.emit_expr(fn)
18
+ return Call(Member(items_expr, "map"), [fn_expr])
19
+
20
+
10
21
  @overload
11
22
  def For(items: Iterable[T], fn: Callable[[T], Element]) -> list[Element]: ...
12
23
 
@@ -40,3 +51,7 @@ def For(items: Iterable[T], fn: Callable[..., Element]) -> list[Element]:
40
51
  if accepts_two:
41
52
  return [fn(item, idx) for idx, item in enumerate(items)]
42
53
  return [fn(item) for item in items]
54
+
55
+
56
+ # Register For in EXPR_REGISTRY so it can be used in transpiled functions
57
+ Expr.register(For, emit_for)
@@ -5,6 +5,7 @@ from urllib.parse import urlparse
5
5
 
6
6
  from fastapi import Request, Response
7
7
 
8
+ from pulse.env import PulseEnv
8
9
  from pulse.hooks.runtime import set_cookie
9
10
 
10
11
  if TYPE_CHECKING:
@@ -16,7 +17,7 @@ class Cookie:
16
17
  name: str
17
18
  _: KW_ONLY
18
19
  domain: str | None = None
19
- secure: bool = True
20
+ secure: bool | None = None
20
21
  samesite: Literal["lax", "strict", "none"] = "lax"
21
22
  max_age_seconds: int = 7 * 24 * 3600
22
23
 
@@ -33,6 +34,10 @@ class Cookie:
33
34
  return cookies.get(self.name)
34
35
 
35
36
  async def set_through_api(self, value: str):
37
+ if self.secure is None:
38
+ raise RuntimeError(
39
+ "Cookie.secure is not resolved. Ensure App.setup() ran or set Cookie(secure=True/False)."
40
+ )
36
41
  await set_cookie(
37
42
  name=self.name,
38
43
  value=value,
@@ -44,6 +49,10 @@ class Cookie:
44
49
 
45
50
  def set_on_fastapi(self, response: Response, value: str) -> None:
46
51
  """Set the session cookie on a FastAPI Response-like object."""
52
+ if self.secure is None:
53
+ raise RuntimeError(
54
+ "Cookie.secure is not resolved. Ensure App.setup() ran or set Cookie(secure=True/False)."
55
+ )
47
56
  response.set_cookie(
48
57
  key=self.name,
49
58
  value=value,
@@ -62,6 +71,10 @@ class SetCookie(Cookie):
62
71
 
63
72
  @classmethod
64
73
  def from_cookie(cls, cookie: Cookie, value: str) -> "SetCookie":
74
+ if cookie.secure is None:
75
+ raise RuntimeError(
76
+ "Cookie.secure is not resolved. Ensure App.setup() ran or set Cookie(secure=True/False)."
77
+ )
65
78
  return cls(
66
79
  name=cookie.name,
67
80
  value=value,
@@ -81,7 +94,7 @@ def session_cookie(
81
94
  return Cookie(
82
95
  name,
83
96
  domain=None,
84
- secure=False,
97
+ secure=None,
85
98
  samesite="lax",
86
99
  max_age_seconds=max_age_seconds,
87
100
  )
@@ -146,6 +159,29 @@ def compute_cookie_domain(mode: "PulseMode", server_address: str) -> str | None:
146
159
  return None
147
160
 
148
161
 
162
+ def compute_cookie_secure(env: PulseEnv, server_address: str | None) -> bool:
163
+ scheme = urlparse(server_address or "").scheme.lower()
164
+ if scheme in ("https", "wss"):
165
+ secure = True
166
+ elif scheme in ("http", "ws"):
167
+ secure = False
168
+ else:
169
+ secure = None
170
+ if secure is None:
171
+ if env in ("prod", "ci"):
172
+ raise RuntimeError(
173
+ "Could not determine cookie security from server_address. "
174
+ + "Use an explicit https:// server_address or set Cookie(secure=True/False)."
175
+ )
176
+ return False
177
+ if env in ("prod", "ci") and not secure:
178
+ raise RuntimeError(
179
+ "Refusing to use insecure cookies in prod/ci. "
180
+ + "Use an https server_address or set Cookie(secure=True) explicitly."
181
+ )
182
+ return secure
183
+
184
+
149
185
  def cors_options(mode: "PulseMode", server_address: str) -> CORSOptions:
150
186
  host = _parse_host(server_address) or "localhost"
151
187
  opts: CORSOptions = {