pulse-framework 0.1.55__py3-none-any.whl → 0.1.57__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. pulse/__init__.py +5 -6
  2. pulse/app.py +144 -57
  3. pulse/channel.py +139 -7
  4. pulse/cli/cmd.py +16 -2
  5. pulse/codegen/codegen.py +43 -12
  6. pulse/component.py +104 -0
  7. pulse/components/for_.py +30 -4
  8. pulse/components/if_.py +28 -5
  9. pulse/components/react_router.py +61 -3
  10. pulse/context.py +39 -5
  11. pulse/cookies.py +108 -4
  12. pulse/decorators.py +193 -24
  13. pulse/env.py +56 -2
  14. pulse/form.py +198 -5
  15. pulse/helpers.py +7 -1
  16. pulse/hooks/core.py +135 -5
  17. pulse/hooks/effects.py +61 -77
  18. pulse/hooks/init.py +60 -1
  19. pulse/hooks/runtime.py +241 -0
  20. pulse/hooks/setup.py +77 -0
  21. pulse/hooks/stable.py +58 -1
  22. pulse/hooks/state.py +107 -20
  23. pulse/js/__init__.py +40 -24
  24. pulse/js/array.py +9 -6
  25. pulse/js/console.py +15 -12
  26. pulse/js/date.py +9 -6
  27. pulse/js/document.py +5 -2
  28. pulse/js/error.py +7 -4
  29. pulse/js/json.py +9 -6
  30. pulse/js/map.py +8 -5
  31. pulse/js/math.py +9 -6
  32. pulse/js/navigator.py +5 -2
  33. pulse/js/number.py +9 -6
  34. pulse/js/obj.py +16 -13
  35. pulse/js/object.py +9 -6
  36. pulse/js/promise.py +19 -13
  37. pulse/js/pulse.py +28 -25
  38. pulse/js/react.py +94 -55
  39. pulse/js/regexp.py +7 -4
  40. pulse/js/set.py +8 -5
  41. pulse/js/string.py +9 -6
  42. pulse/js/weakmap.py +8 -5
  43. pulse/js/weakset.py +8 -5
  44. pulse/js/window.py +6 -3
  45. pulse/messages.py +5 -0
  46. pulse/middleware.py +147 -76
  47. pulse/plugin.py +76 -5
  48. pulse/queries/client.py +186 -39
  49. pulse/queries/common.py +52 -3
  50. pulse/queries/infinite_query.py +154 -2
  51. pulse/queries/mutation.py +127 -7
  52. pulse/queries/query.py +112 -11
  53. pulse/react_component.py +66 -3
  54. pulse/reactive.py +314 -30
  55. pulse/reactive_extensions.py +106 -26
  56. pulse/render_session.py +304 -173
  57. pulse/request.py +46 -11
  58. pulse/routing.py +140 -4
  59. pulse/serializer.py +71 -0
  60. pulse/state.py +177 -9
  61. pulse/test_helpers.py +15 -0
  62. pulse/transpiler/__init__.py +0 -3
  63. pulse/transpiler/py_module.py +1 -7
  64. pulse/user_session.py +119 -18
  65. {pulse_framework-0.1.55.dist-info → pulse_framework-0.1.57.dist-info}/METADATA +5 -5
  66. pulse_framework-0.1.57.dist-info/RECORD +127 -0
  67. pulse/transpiler/react_component.py +0 -44
  68. pulse_framework-0.1.55.dist-info/RECORD +0 -127
  69. {pulse_framework-0.1.55.dist-info → pulse_framework-0.1.57.dist-info}/WHEEL +0 -0
  70. {pulse_framework-0.1.55.dist-info → pulse_framework-0.1.57.dist-info}/entry_points.txt +0 -0
pulse/codegen/codegen.py CHANGED
@@ -24,13 +24,32 @@ logger = logging.getLogger(__file__)
24
24
 
25
25
  @dataclass
26
26
  class CodegenConfig:
27
- """
28
- Configuration for code generation.
27
+ """Configuration for code generation output paths.
28
+
29
+ Controls where generated React Router files are written. All paths
30
+ can be relative (resolved against base_dir) or absolute.
31
+
32
+ Args:
33
+ web_dir: Root directory for web output. Defaults to "web".
34
+ pulse_dir: Subdirectory for generated Pulse files. Defaults to "pulse".
35
+ base_dir: Base directory for resolving relative paths. If not provided,
36
+ resolved from PULSE_APP_FILE, PULSE_APP_DIR, or cwd.
29
37
 
30
38
  Attributes:
31
- web_dir (str): Root directory for the web output.
32
- pulse_dir (str): Name of the Pulse app directory.
33
- pulse_path (Path): Full path to the generated app directory.
39
+ web_dir: Root directory for web output.
40
+ pulse_dir: Subdirectory name for generated files.
41
+ base_dir: Explicit base directory, if provided.
42
+
43
+ Example:
44
+ ```python
45
+ app = ps.App(
46
+ codegen=ps.CodegenConfig(
47
+ web_dir="frontend",
48
+ pulse_dir="generated",
49
+ ),
50
+ )
51
+ # Generated files will be at: frontend/app/generated/
52
+ ```
34
53
  """
35
54
 
36
55
  web_dir: Path | str = "web"
@@ -46,11 +65,14 @@ class CodegenConfig:
46
65
  def resolved_base_dir(self) -> Path:
47
66
  """Resolve the base directory where relative paths should be anchored.
48
67
 
49
- Precedence:
50
- 1) Explicit `base_dir` if provided
51
- 2) Env var `PULSE_APP_FILE` (directory of the file)
52
- 3) Env var `PULSE_APP_DIR`
53
- 4) Current working directory
68
+ Returns:
69
+ Resolved base directory path.
70
+
71
+ Resolution precedence:
72
+ 1. Explicit `base_dir` if provided
73
+ 2. Directory of PULSE_APP_FILE env var
74
+ 3. PULSE_APP_DIR env var
75
+ 4. Current working directory
54
76
  """
55
77
  if isinstance(self.base_dir, Path):
56
78
  return self.base_dir
@@ -64,7 +86,11 @@ class CodegenConfig:
64
86
 
65
87
  @property
66
88
  def web_root(self) -> Path:
67
- """Absolute path to the web root directory (e.g. `<app_dir>/pulse-web`)."""
89
+ """Absolute path to the web root directory.
90
+
91
+ Returns:
92
+ Absolute path to web_dir (e.g., `<base_dir>/web`).
93
+ """
68
94
  wd = Path(self.web_dir)
69
95
  if wd.is_absolute():
70
96
  return wd
@@ -72,7 +98,12 @@ class CodegenConfig:
72
98
 
73
99
  @property
74
100
  def pulse_path(self) -> Path:
75
- """Full path to the generated app directory."""
101
+ """Full path to the generated Pulse app directory.
102
+
103
+ Returns:
104
+ Absolute path where generated files are written
105
+ (e.g., `<web_root>/app/<pulse_dir>`).
106
+ """
76
107
  return self.web_root / "app" / self.pulse_dir
77
108
 
78
109
 
pulse/component.py CHANGED
@@ -1,7 +1,14 @@
1
+ """Component definition and VDOM node types for Pulse.
2
+
3
+ This module provides the core component abstraction for building Pulse UIs,
4
+ including the `@component` decorator and the `Component` class.
5
+ """
6
+
1
7
  from __future__ import annotations
2
8
 
3
9
  from collections.abc import Callable
4
10
  from inspect import Parameter, signature
11
+ from types import CodeType
5
12
  from typing import Any, Generic, ParamSpec, TypeVar, overload, override
6
13
 
7
14
  from pulse.code_analysis import is_stub_function
@@ -19,14 +26,47 @@ from pulse.transpiler.vdom import VDOMNode
19
26
  P = ParamSpec("P")
20
27
  _T = TypeVar("_T")
21
28
 
29
+ _COMPONENT_CODES: set[CodeType] = set()
30
+
31
+
32
+ def is_component_code(code: CodeType) -> bool:
33
+ return code in _COMPONENT_CODES
34
+
22
35
 
23
36
  class Component(Generic[P]):
37
+ """A callable wrapper that turns a function into a Pulse component.
38
+
39
+ Component instances are created by the `@component` decorator. When called,
40
+ they return a `PulseNode` that represents the component in the virtual DOM.
41
+
42
+ Attributes:
43
+ name: Display name of the component (defaults to function name).
44
+ fn: The underlying render function (lazily initialized for stubs).
45
+
46
+ Example:
47
+
48
+ ```python
49
+ @ps.component
50
+ def Card(title: str):
51
+ return ps.div(ps.h3(title))
52
+
53
+ Card(title="Hello") # Returns a PulseNode
54
+ Card(title="Hello", key="card-1") # With reconciliation key
55
+ ```
56
+ """
57
+
24
58
  _raw_fn: Callable[P, Any]
25
59
  _fn: Callable[P, Any] | None
26
60
  name: str
27
61
  _takes_children: bool | None
28
62
 
29
63
  def __init__(self, fn: Callable[P, Any], name: str | None = None) -> None:
64
+ """Initialize a Component.
65
+
66
+ Args:
67
+ fn: The function to wrap as a component.
68
+ name: Custom display name. Defaults to the function's `__name__`.
69
+ """
30
70
  self._raw_fn = fn
31
71
  self.name = name or _infer_component_name(fn)
32
72
  # Only lazy-init for stubs (avoid heavy work for JS module bindings)
@@ -37,15 +77,31 @@ class Component(Generic[P]):
37
77
  else:
38
78
  self._fn = rewrite_init_blocks(fn)
39
79
  self._takes_children = _takes_children(fn)
80
+ _COMPONENT_CODES.add(self._fn.__code__)
40
81
 
41
82
  @property
42
83
  def fn(self) -> Callable[P, Any]:
84
+ """The render function (lazily initialized for stub functions)."""
43
85
  if self._fn is None:
44
86
  self._fn = rewrite_init_blocks(self._raw_fn)
45
87
  self._takes_children = _takes_children(self._raw_fn)
88
+ _COMPONENT_CODES.add(self._fn.__code__)
46
89
  return self._fn
47
90
 
48
91
  def __call__(self, *args: P.args, **kwargs: P.kwargs) -> PulseNode:
92
+ """Invoke the component to create a PulseNode.
93
+
94
+ Args:
95
+ *args: Positional arguments passed to the component function.
96
+ **kwargs: Keyword arguments passed to the component function.
97
+ The special `key` kwarg is used for reconciliation.
98
+
99
+ Returns:
100
+ A PulseNode representing this component invocation in the VDOM.
101
+
102
+ Raises:
103
+ ValueError: If `key` is provided but is not a string.
104
+ """
49
105
  key = kwargs.get("key")
50
106
  if key is not None and not isinstance(key, str):
51
107
  raise ValueError("key must be a string or None")
@@ -85,6 +141,53 @@ def component(
85
141
  def component(
86
142
  fn: Callable[P, Any] | None = None, *, name: str | None = None
87
143
  ) -> Component[P] | Callable[[Callable[P, Any]], Component[P]]:
144
+ """Decorator that creates a Pulse component from a function.
145
+
146
+ Can be used with or without parentheses. The decorated function becomes
147
+ callable and returns a `PulseNode` when invoked.
148
+
149
+ Args:
150
+ fn: Function to wrap as a component. When used as `@component` without
151
+ parentheses, this is the decorated function.
152
+ name: Custom component name for debugging/dev tools. Defaults to the
153
+ function's `__name__`.
154
+
155
+ Returns:
156
+ A `Component` instance if `fn` is provided, otherwise a decorator.
157
+
158
+ Example:
159
+
160
+ Basic usage:
161
+
162
+ ```python
163
+ @ps.component
164
+ def Card(title: str):
165
+ return ps.div(ps.h3(title))
166
+ ```
167
+
168
+ With custom name:
169
+
170
+ ```python
171
+ @ps.component(name="MyCard")
172
+ def card_impl(title: str):
173
+ return ps.div(ps.h3(title))
174
+ ```
175
+
176
+ With children (use `*children` parameter):
177
+
178
+ ```python
179
+ @ps.component
180
+ def Container(*children):
181
+ return ps.div(*children, className="container")
182
+
183
+ # Children can be passed via subscript syntax:
184
+ Container()[
185
+ Card(title="First"),
186
+ Card(title="Second"),
187
+ ]
188
+ ```
189
+ """
190
+
88
191
  def decorator(fn: Callable[P, Any]) -> Component[P]:
89
192
  return Component(fn, name)
90
193
 
@@ -130,4 +233,5 @@ __all__ = [
130
233
  "Primitive",
131
234
  "VDOMNode",
132
235
  "component",
236
+ "is_component_code",
133
237
  ]
pulse/components/for_.py CHANGED
@@ -1,3 +1,8 @@
1
+ """For loop component for mapping items to elements.
2
+
3
+ Provides a declarative way to render lists, similar to JavaScript's Array.map().
4
+ """
5
+
1
6
  from collections.abc import Callable, Iterable
2
7
  from inspect import Parameter, signature
3
8
  from typing import TYPE_CHECKING, Any, TypeVar, overload
@@ -27,11 +32,32 @@ def For(items: Iterable[T], fn: Callable[[T, int], Element]) -> list[Element]: .
27
32
 
28
33
 
29
34
  def For(items: Iterable[T], fn: Callable[..., Element]) -> list[Element]:
30
- """Map items to elements, passing `(item)` or `(item, index)`.
35
+ """Map items to elements, like JavaScript's Array.map().
36
+
37
+ Iterates over `items` and calls `fn` for each one, returning a list of
38
+ elements. The mapper function can accept either one argument (item) or
39
+ two arguments (item, index).
40
+
41
+ Args:
42
+ items: Iterable of items to map over.
43
+ fn: Mapper function that receives `(item)` or `(item, index)` and
44
+ returns an Element. If `fn` has a `*args` parameter, it receives
45
+ both item and index.
46
+
47
+ Returns:
48
+ A list of Elements, one for each item.
49
+
50
+ Example:
51
+ Single argument (item only)::
52
+
53
+ ps.For(users, lambda user: UserCard(user=user, key=user.id))
54
+
55
+ With index::
56
+
57
+ ps.For(items, lambda item, i: ps.li(f"{i}: {item}", key=str(i)))
31
58
 
32
- The callable `fn` may accept either a single positional argument (the item)
33
- or two positional arguments (the item and its index), similar to JavaScript's
34
- Array.map. If `fn` declares `*args`, it will receive `(item, index)`.
59
+ Note:
60
+ In transpiled `@javascript` code, `For` compiles to `.map()`.
35
61
  """
36
62
  try:
37
63
  sig = signature(fn)
pulse/components/if_.py CHANGED
@@ -1,3 +1,8 @@
1
+ """Conditional rendering component.
2
+
3
+ Provides a declarative way to conditionally render elements based on a condition.
4
+ """
5
+
1
6
  from collections.abc import Iterable
2
7
  from typing import Any, TypeVar
3
8
 
@@ -37,15 +42,33 @@ def If(
37
42
  then: T1,
38
43
  else_: T2 = None,
39
44
  ) -> T1 | T2:
40
- """Conditional rendering helper that returns either then or else_ based on condition.
45
+ """Conditional rendering helper.
46
+
47
+ Returns `then` if the condition is truthy, otherwise returns `else_`.
48
+ Automatically unwraps reactive values (Signal, Computed) before evaluation.
41
49
 
42
50
  Args:
43
- condition: Value to test truthiness
44
- then: Element to render if condition is truthy
45
- else_: Optional element to render if condition is falsy
51
+ condition: A boolean or reactive value to evaluate. Supports `Signal[bool]`
52
+ and `Computed[bool]` which are automatically unwrapped.
53
+ then: Element to render when condition is truthy.
54
+ else_: Element to render when condition is falsy. Defaults to None.
46
55
 
47
56
  Returns:
48
- The then value if condition is truthy, else_ if provided and condition is falsy, None otherwise
57
+ The `then` value if condition is truthy, otherwise `else_`.
58
+
59
+ Example:
60
+ Basic conditional::
61
+
62
+ ps.If(
63
+ user.is_admin,
64
+ then=AdminPanel(),
65
+ else_=ps.p("Access denied"),
66
+ )
67
+
68
+ With reactive condition::
69
+
70
+ is_visible = ps.Signal(True)
71
+ ps.If(is_visible, then=ps.div("Content"))
49
72
  """
50
73
  # Unwrap reactive condition if needed and coerce to bool explicitly with guards
51
74
  if isinstance(condition, (Signal, Computed)):
@@ -1,12 +1,19 @@
1
+ """React Router components for client-side navigation.
2
+
3
+ Provides Pulse bindings for react-router's Link and Outlet components.
4
+ """
5
+
1
6
  from typing import Literal, TypedDict, Unpack
2
7
 
3
8
  from pulse.dom.props import HTMLAnchorProps
9
+ from pulse.react_component import react_component
4
10
  from pulse.transpiler import Import
5
11
  from pulse.transpiler.nodes import Node
6
- from pulse.transpiler.react_component import react_component
7
12
 
8
13
 
9
14
  class LinkPath(TypedDict):
15
+ """TypedDict for Link's `to` prop when using an object instead of string."""
16
+
10
17
  pathname: str
11
18
  search: str
12
19
  hash: str
@@ -27,12 +34,63 @@ def Link(
27
34
  state: dict[str, object] | None = None,
28
35
  viewTransition: bool | None = None,
29
36
  **props: Unpack[HTMLAnchorProps],
30
- ): ...
37
+ ) -> None:
38
+ """Client-side navigation link using react-router.
39
+
40
+ Renders an anchor tag that performs client-side navigation without a full
41
+ page reload. Supports prefetching and various navigation behaviors.
42
+
43
+ Args:
44
+ *children: Content to render inside the link.
45
+ key: React reconciliation key.
46
+ to: The target URL path (e.g., "/dashboard", "/users/123").
47
+ discover: Route discovery behavior. "render" discovers on render,
48
+ "none" disables discovery.
49
+ prefetch: Prefetch strategy. "intent" (default) prefetches on hover/focus,
50
+ "render" prefetches immediately, "viewport" when visible, "none" disables.
51
+ preventScrollReset: If True, prevents scroll position reset on navigation.
52
+ relative: Path resolution mode. "route" resolves relative to route hierarchy,
53
+ "path" resolves relative to URL path.
54
+ reloadDocument: If True, performs a full page navigation instead of SPA.
55
+ replace: If True, replaces current history entry instead of pushing.
56
+ state: Arbitrary state to pass to the destination location.
57
+ viewTransition: If True, enables View Transitions API for the navigation.
58
+ **props: Additional HTML anchor attributes (className, onClick, etc.).
59
+
60
+ Example:
61
+ Basic navigation::
62
+
63
+ ps.Link(to="/dashboard")["Go to Dashboard"]
64
+
65
+ With prefetching disabled::
66
+
67
+ ps.Link(to="/settings", prefetch="none")["Settings"]
68
+ """
69
+ ...
31
70
 
32
71
 
33
72
  # @react_component(Import("Outlet", "react-router", version="^7"))
34
73
  @react_component(Import("Outlet", "react-router"))
35
- def Outlet(key: str | None = None): ...
74
+ def Outlet(key: str | None = None) -> None:
75
+ """Renders the matched child route's element.
76
+
77
+ Outlet is used in parent route components to render their child routes.
78
+ It acts as a placeholder where nested route content will be displayed.
79
+
80
+ Args:
81
+ key: React reconciliation key.
82
+
83
+ Example:
84
+ Layout with outlet for child routes::
85
+
86
+ @ps.component
87
+ def Layout():
88
+ return ps.div(
89
+ ps.nav("Navigation"),
90
+ ps.Outlet(), # Child route renders here
91
+ )
92
+ """
93
+ ...
36
94
 
37
95
 
38
96
  __all__ = ["Link", "Outlet"]
pulse/context.py CHANGED
@@ -16,9 +16,23 @@ if TYPE_CHECKING:
16
16
  class PulseContext:
17
17
  """Composite context accessible to hooks and internals.
18
18
 
19
- - session: per-user session ReactiveDict
20
- - render: per-connection RenderSession
21
- - route: active RouteContext for this render/effect scope
19
+ Manages per-request state via context variables. Provides access to the
20
+ application instance, user session, render session, and route context.
21
+
22
+ Attributes:
23
+ app: Application instance.
24
+ session: Per-user session (UserSession or None).
25
+ render: Per-connection render session (RenderSession or None).
26
+ route: Active route context (RouteContext or None).
27
+
28
+ Example:
29
+ ```python
30
+ ctx = PulseContext(app=app, session=session)
31
+ with ctx:
32
+ # Context is active here
33
+ current = PulseContext.get()
34
+ # Previous context restored
35
+ ```
22
36
  """
23
37
 
24
38
  app: "App"
@@ -28,7 +42,15 @@ class PulseContext:
28
42
  _token: "Token[PulseContext | None] | None" = None
29
43
 
30
44
  @classmethod
31
- def get(cls):
45
+ def get(cls) -> "PulseContext":
46
+ """Get the current context.
47
+
48
+ Returns:
49
+ Current PulseContext instance.
50
+
51
+ Raises:
52
+ RuntimeError: If no context is active.
53
+ """
32
54
  ctx = PULSE_CONTEXT.get()
33
55
  if ctx is None:
34
56
  raise RuntimeError("Internal error: PULSE_CONTEXT is not set")
@@ -40,7 +62,19 @@ class PulseContext:
40
62
  session: "UserSession | None" = None,
41
63
  render: "RenderSession | None" = None,
42
64
  route: "RouteContext | None" = None,
43
- ):
65
+ ) -> "PulseContext":
66
+ """Create a new context with updated values.
67
+
68
+ Inherits unspecified values from the current context.
69
+
70
+ Args:
71
+ session: New session (optional, inherits if not provided).
72
+ render: New render session (optional, inherits if not provided).
73
+ route: New route context (optional, inherits if not provided).
74
+
75
+ Returns:
76
+ New PulseContext instance with updated values.
77
+ """
44
78
  ctx = cls.get()
45
79
  return PulseContext(
46
80
  app=ctx.app,
pulse/cookies.py CHANGED
@@ -14,6 +14,26 @@ if TYPE_CHECKING:
14
14
 
15
15
  @dataclass
16
16
  class Cookie:
17
+ """Configuration for HTTP cookies used in session management.
18
+
19
+ Attributes:
20
+ name: Cookie name.
21
+ domain: Cookie domain. Set automatically in subdomain mode.
22
+ secure: HTTPS-only flag. Auto-resolved from server address if None.
23
+ samesite: SameSite attribute ("lax", "strict", or "none").
24
+ max_age_seconds: Cookie lifetime in seconds (default 7 days).
25
+
26
+ Example:
27
+ ```python
28
+ cookie = Cookie(
29
+ name="session",
30
+ secure=True,
31
+ samesite="strict",
32
+ max_age_seconds=3600,
33
+ )
34
+ ```
35
+ """
36
+
17
37
  name: str
18
38
  _: KW_ONLY
19
39
  domain: str | None = None
@@ -22,18 +42,45 @@ class Cookie:
22
42
  max_age_seconds: int = 7 * 24 * 3600
23
43
 
24
44
  def get_from_fastapi(self, request: Request) -> str | None:
25
- """Extract sid from a FastAPI Request (by reading Cookie header)."""
45
+ """Extract cookie value from a FastAPI Request.
46
+
47
+ Reads the Cookie header and parses it to find this cookie's value.
48
+
49
+ Args:
50
+ request: FastAPI/Starlette Request object.
51
+
52
+ Returns:
53
+ Cookie value if found, None otherwise.
54
+ """
26
55
  header = request.headers.get("cookie")
27
56
  cookies = parse_cookie_header(header)
28
57
  return cookies.get(self.name)
29
58
 
30
59
  def get_from_socketio(self, environ: dict[str, Any]) -> str | None:
31
- """Extract sid from a socket.io environ mapping."""
60
+ """Extract cookie value from a Socket.IO environ mapping.
61
+
62
+ Args:
63
+ environ: Socket.IO environ dictionary.
64
+
65
+ Returns:
66
+ Cookie value if found, None otherwise.
67
+ """
32
68
  raw = environ.get("HTTP_COOKIE") or environ.get("COOKIE")
33
69
  cookies = parse_cookie_header(raw)
34
70
  return cookies.get(self.name)
35
71
 
36
- async def set_through_api(self, value: str):
72
+ async def set_through_api(self, value: str) -> None:
73
+ """Set the cookie on the client via WebSocket.
74
+
75
+ Must be called during a callback context.
76
+
77
+ Args:
78
+ value: Cookie value to set.
79
+
80
+ Raises:
81
+ RuntimeError: If Cookie.secure is not resolved (ensure App.setup()
82
+ ran first).
83
+ """
37
84
  if self.secure is None:
38
85
  raise RuntimeError(
39
86
  "Cookie.secure is not resolved. Ensure App.setup() ran or set Cookie(secure=True/False)."
@@ -48,7 +95,17 @@ class Cookie:
48
95
  )
49
96
 
50
97
  def set_on_fastapi(self, response: Response, value: str) -> None:
51
- """Set the session cookie on a FastAPI Response-like object."""
98
+ """Set the cookie on a FastAPI Response object.
99
+
100
+ Configured with httponly=True and path="/".
101
+
102
+ Args:
103
+ response: FastAPI Response object.
104
+ value: Cookie value to set.
105
+
106
+ Raises:
107
+ RuntimeError: If Cookie.secure is not resolved.
108
+ """
52
109
  if self.secure is None:
53
110
  raise RuntimeError(
54
111
  "Cookie.secure is not resolved. Ensure App.setup() ran or set Cookie(secure=True/False)."
@@ -67,10 +124,31 @@ class Cookie:
67
124
 
68
125
  @dataclass
69
126
  class SetCookie(Cookie):
127
+ """Extended Cookie dataclass that includes the cookie value.
128
+
129
+ Used for setting cookies with a specific value. Inherits all configuration
130
+ from Cookie.
131
+
132
+ Attributes:
133
+ value: The cookie value to set.
134
+ """
135
+
70
136
  value: str
71
137
 
72
138
  @classmethod
73
139
  def from_cookie(cls, cookie: Cookie, value: str) -> "SetCookie":
140
+ """Create a SetCookie from an existing Cookie configuration.
141
+
142
+ Args:
143
+ cookie: Cookie configuration to copy settings from.
144
+ value: Cookie value to set.
145
+
146
+ Returns:
147
+ SetCookie instance with the same configuration and specified value.
148
+
149
+ Raises:
150
+ RuntimeError: If cookie.secure is not resolved.
151
+ """
74
152
  if cookie.secure is None:
75
153
  raise RuntimeError(
76
154
  "Cookie.secure is not resolved. Ensure App.setup() ran or set Cookie(secure=True/False)."
@@ -111,6 +189,18 @@ def session_cookie(
111
189
 
112
190
 
113
191
  class CORSOptions(TypedDict, total=False):
192
+ """TypedDict for CORS middleware configuration.
193
+
194
+ Attributes:
195
+ allow_origins: List of allowed origins. Use ['*'] for all. Default: ().
196
+ allow_methods: List of allowed HTTP methods. Default: ('GET',).
197
+ allow_headers: List of allowed HTTP headers. Default: ().
198
+ allow_credentials: Whether to allow credentials. Default: False.
199
+ allow_origin_regex: Regex pattern for allowed origins. Default: None.
200
+ expose_headers: List of headers to expose to browser. Default: ().
201
+ max_age: Browser CORS cache duration in seconds. Default: 600.
202
+ """
203
+
114
204
  allow_origins: Sequence[str]
115
205
  "List of allowed origins. Use ['*'] to allow all origins. Default: ()"
116
206
 
@@ -207,6 +297,20 @@ def cors_options(mode: "PulseMode", server_address: str) -> CORSOptions:
207
297
 
208
298
 
209
299
  def parse_cookie_header(header: str | None) -> dict[str, str]:
300
+ """Parse a raw Cookie header string into a dictionary.
301
+
302
+ Args:
303
+ header: Raw Cookie header string (e.g., "session=abc123; theme=dark").
304
+
305
+ Returns:
306
+ Dictionary of cookie name-value pairs.
307
+
308
+ Example:
309
+ ```python
310
+ cookies = parse_cookie_header("session=abc123; theme=dark")
311
+ # {"session": "abc123", "theme": "dark"}
312
+ ```
313
+ """
210
314
  cookies: dict[str, str] = {}
211
315
  if not header:
212
316
  return cookies