pulse-framework 0.1.53__tar.gz → 0.1.55__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.
- pulse_framework-0.1.55/PKG-INFO +196 -0
- pulse_framework-0.1.55/README.md +178 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/pyproject.toml +1 -1
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/__init__.py +3 -3
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/app.py +34 -20
- pulse_framework-0.1.55/src/pulse/code_analysis.py +38 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/codegen/codegen.py +18 -50
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/codegen/templates/route.py +100 -56
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/component.py +24 -6
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/components/for_.py +17 -2
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/cookies.py +38 -2
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/env.py +4 -4
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/hooks/init.py +174 -14
- pulse_framework-0.1.55/src/pulse/hooks/state.py +105 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/__init__.py +12 -9
- pulse_framework-0.1.55/src/pulse/js/obj.py +79 -0
- pulse_framework-0.1.55/src/pulse/js/pulse.py +112 -0
- pulse_framework-0.1.55/src/pulse/js/react.py +457 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/messages.py +13 -13
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/proxy.py +18 -5
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/render_session.py +282 -266
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/renderer.py +36 -73
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/serializer.py +5 -2
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/__init__.py +13 -0
- pulse_framework-0.1.55/src/pulse/transpiler/assets.py +66 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/builtins.py +0 -20
- pulse_framework-0.1.55/src/pulse/transpiler/dynamic_import.py +131 -0
- pulse_framework-0.1.55/src/pulse/transpiler/emit_context.py +49 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/errors.py +29 -11
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/function.py +36 -5
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/imports.py +33 -27
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/js_module.py +73 -20
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/modules/pulse/tags.py +35 -15
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/nodes.py +121 -36
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/py_module.py +1 -1
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/react_component.py +4 -11
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/transpiler.py +32 -26
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/user_session.py +10 -0
- pulse_framework-0.1.53/PKG-INFO +0 -18
- pulse_framework-0.1.53/README.md +0 -0
- pulse_framework-0.1.53/src/pulse/hooks/states.py +0 -285
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/_examples.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/channel.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/cli/__init__.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/cli/cmd.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/cli/dependencies.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/cli/folder_lock.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/cli/helpers.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/cli/logging.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/cli/models.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/cli/packages.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/cli/processes.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/cli/secrets.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/cli/uvicorn_log_config.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/codegen/__init__.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/codegen/js.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/codegen/templates/__init__.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/codegen/templates/layout.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/codegen/templates/routes_ts.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/codegen/utils.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/components/__init__.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/components/if_.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/components/react_router.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/context.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/decorators.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/dom/__init__.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/dom/elements.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/dom/events.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/dom/props.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/dom/svg.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/dom/tags.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/dom/tags.pyi +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/form.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/helpers.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/hooks/__init__.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/hooks/core.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/hooks/effects.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/hooks/runtime.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/hooks/setup.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/hooks/stable.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/__init__.pyi +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/_types.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/array.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/console.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/date.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/document.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/error.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/json.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/map.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/math.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/navigator.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/number.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/object.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/promise.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/regexp.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/set.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/string.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/weakmap.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/weakset.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/js/window.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/middleware.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/plugin.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/py.typed +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/queries/__init__.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/queries/client.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/queries/common.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/queries/effect.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/queries/infinite_query.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/queries/mutation.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/queries/protocol.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/queries/query.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/queries/store.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/react_component.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/reactive.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/reactive_extensions.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/request.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/routing.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/state.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/id.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/modules/__init__.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/modules/asyncio.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/modules/json.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/modules/math.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/modules/pulse/__init__.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/modules/typing.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/transpiler/vdom.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/types/__init__.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/types/event_handler.py +0 -0
- {pulse_framework-0.1.53 → pulse_framework-0.1.55}/src/pulse/version.py +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pulse-framework
|
|
3
|
+
Version: 0.1.55
|
|
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
|
|
@@ -1286,9 +1286,9 @@ from pulse.hooks.stable import (
|
|
|
1286
1286
|
stable as stable,
|
|
1287
1287
|
)
|
|
1288
1288
|
|
|
1289
|
-
# Hooks -
|
|
1290
|
-
from pulse.hooks.
|
|
1291
|
-
from pulse.hooks.
|
|
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.
|
|
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"] == "
|
|
737
|
-
render.
|
|
738
|
-
elif msg["type"] == "
|
|
739
|
-
render.
|
|
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"] == "
|
|
743
|
-
render.
|
|
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=
|
|
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=
|
|
888
|
+
secure=cookie_secure,
|
|
875
889
|
samesite=self.cookie.samesite,
|
|
876
890
|
max_age_seconds=self.cookie.max_age_seconds,
|
|
877
891
|
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Code analysis utilities for inspecting Python source."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import inspect
|
|
7
|
+
import textwrap
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_stub_function(fn: Callable[..., Any]) -> bool:
|
|
13
|
+
"""Check if function body is just ... or pass (no real implementation)."""
|
|
14
|
+
try:
|
|
15
|
+
source = inspect.getsource(fn)
|
|
16
|
+
tree = ast.parse(textwrap.dedent(source))
|
|
17
|
+
func_def = tree.body[0]
|
|
18
|
+
if not isinstance(func_def, ast.FunctionDef):
|
|
19
|
+
return False
|
|
20
|
+
body = func_def.body
|
|
21
|
+
# Skip docstring
|
|
22
|
+
if body and isinstance(body[0], ast.Expr):
|
|
23
|
+
if isinstance(body[0].value, ast.Constant) and isinstance(
|
|
24
|
+
body[0].value.value, str
|
|
25
|
+
):
|
|
26
|
+
body = body[1:]
|
|
27
|
+
if not body:
|
|
28
|
+
return True
|
|
29
|
+
if len(body) == 1:
|
|
30
|
+
stmt = body[0]
|
|
31
|
+
if isinstance(stmt, ast.Pass):
|
|
32
|
+
return True
|
|
33
|
+
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant):
|
|
34
|
+
if stmt.value.value is ...:
|
|
35
|
+
return True
|
|
36
|
+
return False
|
|
37
|
+
except (OSError, TypeError, SyntaxError):
|
|
38
|
+
return False
|