pulse-framework 0.1.52__py3-none-any.whl → 0.1.54__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/transpiler/nodes.py CHANGED
@@ -88,19 +88,23 @@ class Expr(ABC):
88
88
  def transpile_call(
89
89
  self,
90
90
  args: list[ast.expr],
91
- kwargs: dict[str, ast.expr],
91
+ keywords: list[ast.keyword],
92
92
  ctx: Transpiler,
93
93
  ) -> Expr:
94
94
  """Called when this expression is used as a function: expr(args).
95
95
 
96
96
  Override to customize call behavior.
97
- Default raises - most expressions are not callable.
97
+ Default emits a Call expression with args transpiled.
98
98
 
99
- Args and kwargs are raw Python `ast.expr` nodes (not yet transpiled).
99
+ Args and keywords are raw Python AST nodes (not yet transpiled).
100
100
  Use ctx.emit_expr() to convert them to Expr as needed.
101
+ Keywords with kw.arg=None are **spread syntax.
101
102
  """
102
- if kwargs:
103
- raise TranspileError("Keyword arguments not yet supported in v2 transpiler")
103
+ if keywords:
104
+ has_spread = any(kw.arg is None for kw in keywords)
105
+ if has_spread:
106
+ raise TranspileError("Spread (**expr) not supported in this call")
107
+ raise TranspileError("Keyword arguments not supported in call")
104
108
  return Call(self, [ctx.emit_expr(a) for a in args])
105
109
 
106
110
  def transpile_getattr(self, attr: str, ctx: Transpiler) -> Expr:
@@ -341,10 +345,10 @@ class ExprWrapper(Expr):
341
345
  def transpile_call(
342
346
  self,
343
347
  args: list[ast.expr],
344
- kwargs: dict[str, ast.expr],
348
+ keywords: list[ast.keyword],
345
349
  ctx: Transpiler,
346
350
  ) -> Expr:
347
- return self.expr.transpile_call(args, kwargs, ctx)
351
+ return self.expr.transpile_call(args, keywords, ctx)
348
352
 
349
353
  @override
350
354
  def transpile_getattr(self, attr: str, ctx: Transpiler) -> Expr:
@@ -395,28 +399,33 @@ class Jsx(ExprWrapper):
395
399
  def transpile_call(
396
400
  self,
397
401
  args: list[ast.expr],
398
- kwargs: dict[str, ast.expr],
402
+ keywords: list[ast.keyword],
399
403
  ctx: "Transpiler",
400
404
  ) -> Expr:
401
405
  """Transpile a call to this JSX wrapper into an Element.
402
406
 
403
407
  Positional args become children, keyword args become props.
404
- The `key` kwarg is extracted specially.
408
+ The `key` kwarg is extracted specially. Spread (**expr) is supported.
405
409
  """
406
410
  children: list[Node] = [ctx.emit_expr(a) for a in args]
407
411
 
408
- props: dict[str, Prop] = {}
412
+ props: list[tuple[str, Prop] | Spread] = []
409
413
  key: str | Expr | None = None
410
- for k, v in kwargs.items():
411
- v = ctx.emit_expr(v)
412
- if k == "key":
413
- # Accept any expression as key for transpilation
414
- if isinstance(v, Literal) and isinstance(v.value, str):
415
- key = v.value # Optimize string literals
416
- else:
417
- key = v # Keep as expression
414
+ for kw in keywords:
415
+ if kw.arg is None:
416
+ # **spread syntax
417
+ props.append(spread_dict(ctx.emit_expr(kw.value)))
418
418
  else:
419
- props[k] = v
419
+ k = kw.arg
420
+ v = ctx.emit_expr(kw.value)
421
+ if k == "key":
422
+ # Accept any expression as key for transpilation
423
+ if isinstance(v, Literal) and isinstance(v.value, str):
424
+ key = v.value # Optimize string literals
425
+ else:
426
+ key = v # Keep as expression
427
+ else:
428
+ props.append((k, v))
420
429
 
421
430
  return Element(
422
431
  tag=self.expr,
@@ -435,7 +444,7 @@ class Jsx(ExprWrapper):
435
444
  """
436
445
 
437
446
  # Normal call: build Element
438
- props: dict[str, object] = {}
447
+ props: dict[str, Any] = {}
439
448
  key: str | None = None
440
449
  children: list[Node] = list(args)
441
450
 
@@ -490,7 +499,9 @@ class Value(Expr):
490
499
 
491
500
  @override
492
501
  def render(self) -> VDOMNode:
493
- raise TypeError("Value cannot be rendered as VDOMExpr; use coerce_json instead")
502
+ raise TypeError(
503
+ "Value cannot be rendered as VDOMExpr; unwrap with .value instead"
504
+ )
494
505
 
495
506
 
496
507
  class Element(Expr):
@@ -501,19 +512,23 @@ class Element(Expr):
501
512
  - "div", "span", etc.: HTML element
502
513
  - "$$ComponentId": Client component (registered in JS registry)
503
514
  - Expr (Import, Member, etc.): Direct component reference for transpilation
515
+
516
+ Props can be either:
517
+ - tuple[str, Prop]: key-value pair
518
+ - Spread: spread expression (...expr)
504
519
  """
505
520
 
506
521
  __slots__: tuple[str, ...] = ("tag", "props", "children", "key")
507
522
 
508
523
  tag: str | Expr
509
- props: dict[str, Any] | None
524
+ props: Sequence[tuple[str, Prop] | Spread] | dict[str, Any] | None
510
525
  children: Sequence[Node] | None
511
526
  key: str | Expr | None
512
527
 
513
528
  def __init__(
514
529
  self,
515
530
  tag: str | Expr,
516
- props: dict[str, Any] | None = None,
531
+ props: Sequence[tuple[str, Prop] | Spread] | dict[str, Any] | None = None,
517
532
  children: Sequence[Node] | None = None,
518
533
  key: str | Expr | None = None,
519
534
  ) -> None:
@@ -534,8 +549,6 @@ class Element(Expr):
534
549
  warn_stacklevel=5,
535
550
  )
536
551
  self.key = key
537
- if self.key is None and self.props:
538
- self.key = self.props.pop("key", None)
539
552
 
540
553
  def _emit_key(self, out: list[str]) -> None:
541
554
  """Emit key prop (string or expression)."""
@@ -583,10 +596,24 @@ class Element(Expr):
583
596
  if self.key is not None:
584
597
  self._emit_key(props_out)
585
598
  if self.props:
586
- for name, value in self.props.items():
599
+ # Handle both dict (from render path) and sequence (from transpilation)
600
+ # Dict case: items() yields tuple[str, Any], never Spread
601
+ # Sequence case: already list[tuple[str, Prop] | Spread]
602
+ props_iter: Iterable[tuple[str, Any]] | Sequence[tuple[str, Prop] | Spread]
603
+ if isinstance(self.props, dict):
604
+ props_iter = self.props.items()
605
+ else:
606
+ props_iter = self.props
607
+ for prop in props_iter:
587
608
  if props_out:
588
609
  props_out.append(" ")
589
- _emit_jsx_prop(name, value, props_out)
610
+ if isinstance(prop, Spread):
611
+ props_out.append("{...")
612
+ prop.expr.emit(props_out)
613
+ props_out.append("}")
614
+ else:
615
+ name, value = prop
616
+ _emit_jsx_prop(name, value, props_out)
590
617
 
591
618
  # Build children into a separate buffer to check if empty
592
619
  children_out: list[str] = []
@@ -682,6 +709,27 @@ class Element(Expr):
682
709
  key=self.key,
683
710
  )
684
711
 
712
+ def props_dict(self) -> dict[str, Any]:
713
+ """Convert props to dict for rendering.
714
+
715
+ Raises TypeError if props contain Spread (only valid in transpilation).
716
+ """
717
+ if not self.props:
718
+ return {}
719
+ # Already a dict (from renderer reconciliation)
720
+ if isinstance(self.props, dict):
721
+ return self.props
722
+ # Sequence of (key, value) pairs or Spread
723
+ result: dict[str, Any] = {}
724
+ for prop in self.props:
725
+ if isinstance(prop, Spread):
726
+ raise TypeError(
727
+ "Element with spread props cannot be rendered; spread is only valid during transpilation"
728
+ )
729
+ k, v = prop
730
+ result[k] = v
731
+ return result
732
+
685
733
  @override
686
734
  def render(self) -> VDOMNode:
687
735
  """Element rendering is handled by Renderer.render_node(), not render().
@@ -938,25 +986,40 @@ class Array(Expr):
938
986
 
939
987
  @dataclass(slots=True)
940
988
  class Object(Expr):
941
- """JS object: { key: value }"""
989
+ """JS object: { key: value, ...spread }
990
+
991
+ Props can be either:
992
+ - tuple[str, Expr]: key-value pair
993
+ - Spread: spread expression (...expr)
994
+ """
942
995
 
943
- props: Sequence[tuple[str, Expr]]
996
+ props: Sequence[tuple[str, Expr] | Spread]
944
997
 
945
998
  @override
946
999
  def emit(self, out: list[str]) -> None:
947
1000
  out.append("{")
948
- for i, (k, v) in enumerate(self.props):
1001
+ for i, prop in enumerate(self.props):
949
1002
  if i > 0:
950
1003
  out.append(", ")
951
- out.append('"')
952
- out.append(_escape_string(k))
953
- out.append('": ')
954
- v.emit(out)
1004
+ if isinstance(prop, Spread):
1005
+ prop.emit(out)
1006
+ else:
1007
+ k, v = prop
1008
+ out.append('"')
1009
+ out.append(_escape_string(k))
1010
+ out.append('": ')
1011
+ v.emit(out)
955
1012
  out.append("}")
956
1013
 
957
1014
  @override
958
1015
  def render(self) -> VDOMNode:
959
- return {"t": "object", "props": {k: v.render() for k, v in self.props}}
1016
+ rendered_props: dict[str, VDOMNode] = {}
1017
+ for prop in self.props:
1018
+ if isinstance(prop, Spread):
1019
+ raise TypeError("Object spread cannot be rendered to VDOM")
1020
+ k, v = prop
1021
+ rendered_props[k] = v.render()
1022
+ return {"t": "object", "props": rendered_props}
960
1023
 
961
1024
 
962
1025
  @dataclass(slots=True)
@@ -1214,6 +1277,21 @@ class Spread(Expr):
1214
1277
  raise TypeError("Spread cannot be rendered as VDOMExpr directly")
1215
1278
 
1216
1279
 
1280
+ def spread_dict(expr: Expr) -> Spread:
1281
+ """Wrap a spread expression with Map-to-object conversion.
1282
+
1283
+ Python dicts transpile to Map, which has no enumerable own properties.
1284
+ This wraps the spread with an IIFE that converts Map to object:
1285
+ (...expr) -> ...($s => $s instanceof Map ? Object.fromEntries($s) : $s)(expr)
1286
+
1287
+ The IIFE ensures expr is evaluated only once.
1288
+ """
1289
+ s = Identifier("$s")
1290
+ is_map = Binary(s, "instanceof", Identifier("Map"))
1291
+ as_obj = Call(Member(Identifier("Object"), "fromEntries"), [s])
1292
+ return Spread(Call(Arrow(["$s"], Ternary(is_map, as_obj, s)), [expr]))
1293
+
1294
+
1217
1295
  @dataclass(slots=True)
1218
1296
  class New(Expr):
1219
1297
  """JS new expression: new Ctor(args)"""
@@ -1272,9 +1350,16 @@ class Transformer(Expr, Generic[_F]):
1272
1350
  def transpile_call(
1273
1351
  self,
1274
1352
  args: list[ast.expr],
1275
- kwargs: dict[str, ast.expr],
1353
+ keywords: list[ast.keyword],
1276
1354
  ctx: Transpiler,
1277
1355
  ) -> Expr:
1356
+ # Convert keywords to dict, reject spreads
1357
+ kwargs: dict[str, ast.expr] = {}
1358
+ for kw in keywords:
1359
+ if kw.arg is None:
1360
+ label = self.name or "Function"
1361
+ raise TranspileError(f"{label} does not support **spread")
1362
+ kwargs[kw.arg] = kw.value
1278
1363
  if kwargs:
1279
1364
  return self.fn(*args, ctx=ctx, **kwargs)
1280
1365
  return self.fn(*args, ctx=ctx)
@@ -70,7 +70,7 @@ class PyModule(Expr):
70
70
  def transpile_call(
71
71
  self,
72
72
  args: list[ast.expr],
73
- kwargs: dict[str, ast.expr],
73
+ keywords: list[ast.keyword],
74
74
  ctx: Transpiler,
75
75
  ) -> Expr:
76
76
  label = self.name or "PyModule"
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
  import ast
11
11
  import re
12
12
  from collections.abc import Callable, Mapping
13
- from typing import Any
13
+ from typing import Any, cast
14
14
 
15
15
  from pulse.transpiler.builtins import BUILTINS, emit_method
16
16
  from pulse.transpiler.errors import TranspileError
@@ -31,6 +31,7 @@ from pulse.transpiler.nodes import (
31
31
  If,
32
32
  Literal,
33
33
  Member,
34
+ New,
34
35
  Return,
35
36
  Spread,
36
37
  Stmt,
@@ -465,7 +466,7 @@ class Transpiler:
465
466
  return self._emit_dict(node)
466
467
 
467
468
  if isinstance(node, ast.Set):
468
- return Call(
469
+ return New(
469
470
  Identifier("Set"),
470
471
  [Array([self.emit_expr(e) for e in node.elts])],
471
472
  )
@@ -515,14 +516,14 @@ class Transpiler:
515
516
  arr = self._emit_comprehension_chain(
516
517
  node.generators, lambda: self.emit_expr(node.elt)
517
518
  )
518
- return Call(Identifier("Set"), [arr])
519
+ return New(Identifier("Set"), [arr])
519
520
 
520
521
  if isinstance(node, ast.DictComp):
521
522
  pairs = self._emit_comprehension_chain(
522
523
  node.generators,
523
524
  lambda: Array([self.emit_expr(node.key), self.emit_expr(node.value)]),
524
525
  )
525
- return Call(Identifier("Map"), [pairs])
526
+ return New(Identifier("Map"), [pairs])
526
527
 
527
528
  if isinstance(node, ast.Lambda):
528
529
  return self._emit_lambda(node)
@@ -596,7 +597,7 @@ class Transpiler:
596
597
  key_expr = self.emit_expr(k)
597
598
  val_expr = self.emit_expr(v)
598
599
  entries.append(Array([key_expr, val_expr]))
599
- return Call(Identifier("Map"), [Array(entries)])
600
+ return New(Identifier("Map"), [Array(entries)])
600
601
 
601
602
  def _emit_binop(self, node: ast.BinOp) -> Expr:
602
603
  """Emit a binary operation."""
@@ -720,37 +721,38 @@ class Transpiler:
720
721
 
721
722
  def _emit_call(self, node: ast.Call) -> Expr:
722
723
  """Emit a function call."""
723
- # Collect args and kwargs as raw AST values
724
- args_raw = list(node.args)
725
- kwargs_raw: dict[str, Any] = {}
726
- for kw in node.keywords:
727
- if kw.arg is None:
728
- raise TranspileError(
729
- "Spread props (**kwargs) not yet supported in v2 transpiler"
730
- )
731
- kwargs_raw[kw.arg] = kw.value
732
-
733
724
  # Method call: obj.method(args) - try builtin method dispatch
734
725
  if isinstance(node.func, ast.Attribute):
726
+ # Check for spreads - if present, skip builtin method handling
727
+ # (let transpile_call decide on spread support)
728
+ has_spread = any(kw.arg is None for kw in node.keywords)
729
+
735
730
  obj = self.emit_expr(node.func.value)
736
731
  method = node.func.attr
737
- args: list[Expr] = [self.emit_expr(a) for a in args_raw]
738
- kwargs: dict[str, Expr] = {
739
- k: self.emit_expr(v) for k, v in kwargs_raw.items()
740
- }
741
732
 
742
- # Try builtin method handling with runtime checks
743
- result = emit_method(obj, method, args, kwargs)
744
- if result is not None:
745
- return result
733
+ # Try builtin method handling only if no spreads
734
+ if not has_spread:
735
+ # Safe to cast: has_spread=False means all kw.arg are str (not None)
736
+ kwargs_raw: dict[str, Any] = {
737
+ cast(str, kw.arg): kw.value for kw in node.keywords
738
+ }
739
+ args: list[Expr] = [self.emit_expr(a) for a in node.args]
740
+ kwargs: dict[str, Expr] = {
741
+ k: self.emit_expr(v) for k, v in kwargs_raw.items()
742
+ }
743
+ result = emit_method(obj, method, args, kwargs)
744
+ if result is not None:
745
+ return result
746
746
 
747
747
  # IMPORTANT: derive method expr via transpile_getattr
748
748
  method_expr = obj.transpile_getattr(method, self)
749
- return method_expr.transpile_call(args_raw, kwargs_raw, self)
749
+ return method_expr.transpile_call(
750
+ list(node.args), list(node.keywords), self
751
+ )
750
752
 
751
- # Function call (or other callable expr)
753
+ # Function call - pass raw keywords (let callee decide on spread support)
752
754
  callee = self.emit_expr(node.func)
753
- return callee.transpile_call(args_raw, kwargs_raw, self)
755
+ return callee.transpile_call(list(node.args), list(node.keywords), self)
754
756
 
755
757
  def _emit_attribute(self, node: ast.Attribute) -> Expr:
756
758
  """Emit an attribute access."""
pulse/user_session.py CHANGED
@@ -61,6 +61,10 @@ class UserSession(Disposable):
61
61
  # unwrap subscribes the effect to all signals in the session ReactiveDict
62
62
  data = unwrap(self.data)
63
63
  signed_cookie = app.session_store.encode(self.sid, data)
64
+ if app.cookie.secure is None:
65
+ raise RuntimeError(
66
+ "Cookie.secure is not resolved. This is likely an internal error. Ensure App.setup() ran before sessions."
67
+ )
64
68
  self.set_cookie(
65
69
  name=app.cookie.name,
66
70
  value=signed_cookie,
@@ -83,6 +87,12 @@ class UserSession(Disposable):
83
87
  self._queued_cookies.clear()
84
88
  self.scheduled_cookie_refresh = False
85
89
 
90
+ def get_cookie_value(self, name: str) -> str | None:
91
+ cookie = self._queued_cookies.get(name)
92
+ if cookie is None:
93
+ return None
94
+ return cookie.value
95
+
86
96
  def set_cookie(
87
97
  self,
88
98
  name: str,
@@ -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