pulse-framework 0.1.48__tar.gz → 0.1.50__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 (118) hide show
  1. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/PKG-INFO +1 -1
  2. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/pyproject.toml +1 -1
  3. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/hooks/runtime.py +4 -2
  4. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/messages.py +1 -0
  5. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/proxy.py +2 -2
  6. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/render_session.py +9 -2
  7. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/imports.py +7 -2
  8. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/vdom.py +9 -11
  9. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/README.md +0 -0
  10. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/__init__.py +0 -0
  11. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/app.py +0 -0
  12. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/channel.py +0 -0
  13. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/cli/__init__.py +0 -0
  14. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/cli/cmd.py +0 -0
  15. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/cli/dependencies.py +0 -0
  16. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/cli/folder_lock.py +0 -0
  17. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/cli/helpers.py +0 -0
  18. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/cli/models.py +0 -0
  19. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/cli/packages.py +0 -0
  20. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/cli/processes.py +0 -0
  21. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/cli/secrets.py +0 -0
  22. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/cli/uvicorn_log_config.py +0 -0
  23. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/codegen/__init__.py +0 -0
  24. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/codegen/codegen.py +0 -0
  25. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/codegen/js.py +0 -0
  26. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/codegen/templates/__init__.py +0 -0
  27. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/codegen/templates/layout.py +0 -0
  28. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/codegen/templates/route.py +0 -0
  29. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/codegen/templates/routes_ts.py +0 -0
  30. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/codegen/utils.py +0 -0
  31. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/components/__init__.py +0 -0
  32. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/components/for_.py +0 -0
  33. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/components/if_.py +0 -0
  34. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/components/react_router.py +0 -0
  35. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/context.py +0 -0
  36. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/cookies.py +0 -0
  37. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/decorators.py +0 -0
  38. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/env.py +0 -0
  39. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/form.py +0 -0
  40. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/helpers.py +0 -0
  41. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/hooks/__init__.py +0 -0
  42. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/hooks/core.py +0 -0
  43. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/hooks/effects.py +0 -0
  44. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/hooks/init.py +0 -0
  45. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/hooks/setup.py +0 -0
  46. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/hooks/stable.py +0 -0
  47. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/hooks/states.py +0 -0
  48. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/html/__init__.py +0 -0
  49. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/html/elements.py +0 -0
  50. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/html/events.py +0 -0
  51. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/html/props.py +0 -0
  52. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/html/svg.py +0 -0
  53. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/html/tags.py +0 -0
  54. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/html/tags.pyi +0 -0
  55. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/__init__.py +0 -0
  56. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/__init__.pyi +0 -0
  57. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/_types.py +0 -0
  58. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/array.py +0 -0
  59. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/console.py +0 -0
  60. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/date.py +0 -0
  61. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/document.py +0 -0
  62. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/error.py +0 -0
  63. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/json.py +0 -0
  64. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/map.py +0 -0
  65. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/math.py +0 -0
  66. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/navigator.py +0 -0
  67. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/number.py +0 -0
  68. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/object.py +0 -0
  69. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/promise.py +0 -0
  70. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/regexp.py +0 -0
  71. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/set.py +0 -0
  72. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/string.py +0 -0
  73. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/weakmap.py +0 -0
  74. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/weakset.py +0 -0
  75. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/js/window.py +0 -0
  76. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/middleware.py +0 -0
  77. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/plugin.py +0 -0
  78. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/py.typed +0 -0
  79. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/queries/__init__.py +0 -0
  80. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/queries/client.py +0 -0
  81. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/queries/common.py +0 -0
  82. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/queries/effect.py +0 -0
  83. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/queries/infinite_query.py +0 -0
  84. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/queries/mutation.py +0 -0
  85. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/queries/protocol.py +0 -0
  86. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/queries/query.py +0 -0
  87. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/queries/store.py +0 -0
  88. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/react_component.py +0 -0
  89. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/reactive.py +0 -0
  90. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/reactive_extensions.py +0 -0
  91. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/renderer.py +0 -0
  92. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/request.py +0 -0
  93. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/routing.py +0 -0
  94. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/serializer.py +0 -0
  95. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/state.py +0 -0
  96. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/__init__.py +0 -0
  97. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/builtins.py +0 -0
  98. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/constants.py +0 -0
  99. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/context.py +0 -0
  100. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/errors.py +0 -0
  101. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/function.py +0 -0
  102. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/ids.py +0 -0
  103. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/js_module.py +0 -0
  104. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/modules/__init__.py +0 -0
  105. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/modules/asyncio.py +0 -0
  106. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/modules/json.py +0 -0
  107. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/modules/math.py +0 -0
  108. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/modules/re.py +0 -0
  109. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/modules/tags.py +0 -0
  110. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/modules/typing.py +0 -0
  111. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/nodes.py +0 -0
  112. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/py_module.py +0 -0
  113. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/transpiler.py +0 -0
  114. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/transpiler/utils.py +0 -0
  115. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/types/__init__.py +0 -0
  116. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/types/event_handler.py +0 -0
  117. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/user_session.py +0 -0
  118. {pulse_framework-0.1.48 → pulse_framework-0.1.50}/src/pulse/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.48
3
+ Version: 0.1.50
4
4
  Summary: Pulse - Full-stack framework for building real-time React applications in Python
5
5
  Requires-Dist: websockets>=12.0
6
6
  Requires-Dist: fastapi>=0.104.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pulse-framework"
3
- version = "0.1.48"
3
+ version = "0.1.50"
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"
@@ -103,11 +103,13 @@ async def set_cookie(
103
103
  )
104
104
 
105
105
 
106
- def navigate(path: str, *, replace: bool = False) -> None:
106
+ def navigate(path: str, *, replace: bool = False, hard: bool = False) -> None:
107
107
  ctx = PulseContext.get()
108
108
  if ctx.render is None:
109
109
  raise RuntimeError("navigate() must be invoked inside a Pulse callback context")
110
- ctx.render.send({"type": "navigate_to", "path": path, "replace": replace})
110
+ ctx.render.send(
111
+ {"type": "navigate_to", "path": path, "replace": replace, "hard": hard}
112
+ )
111
113
 
112
114
 
113
115
  def redirect(path: str, *, replace: bool = False) -> NoReturn:
@@ -48,6 +48,7 @@ class ServerNavigateToMessage(TypedDict):
48
48
  type: Literal["navigate_to"]
49
49
  path: str
50
50
  replace: bool
51
+ hard: bool
51
52
 
52
53
 
53
54
  class ServerApiCallMessage(TypedDict):
@@ -41,7 +41,7 @@ class ReactProxy:
41
41
  self.server_address = server_address
42
42
  self._client = None
43
43
 
44
- def _rewrite_url(self, url: str) -> str:
44
+ def rewrite_url(self, url: str) -> str:
45
45
  """Rewrite internal React server URLs to external server address."""
46
46
  if self.react_server_address in url:
47
47
  return url.replace(self.react_server_address, self.server_address)
@@ -207,7 +207,7 @@ class ReactProxy:
207
207
  response_headers: dict[str, str] = {}
208
208
  for k, v in r.headers.items():
209
209
  if k.lower() in ("location", "content-location"):
210
- v = self._rewrite_url(v)
210
+ v = self.rewrite_url(v)
211
211
  response_headers[k] = v
212
212
 
213
213
  return StreamingResponse(
@@ -440,6 +440,7 @@ class RenderSession:
440
440
  type="navigate_to",
441
441
  path=msg["path"],
442
442
  replace=msg["replace"],
443
+ hard=msg.get("hard", False),
443
444
  )
444
445
 
445
446
  prev_sender = self._send_message
@@ -563,14 +564,20 @@ class RenderSession:
563
564
  # Prefer client-side navigation over emitting VDOM operations
564
565
  self.send(
565
566
  ServerNavigateToMessage(
566
- type="navigate_to", path=r.path, replace=r.replace
567
+ type="navigate_to",
568
+ path=r.path,
569
+ replace=r.replace,
570
+ hard=False,
567
571
  )
568
572
  )
569
573
  except NotFoundInterrupt:
570
574
  # Use app-configured not-found path; fallback to '/404'
571
575
  self.send(
572
576
  ServerNavigateToMessage(
573
- type="navigate_to", path=ctx.app.not_found, replace=True
577
+ type="navigate_to",
578
+ path=ctx.app.not_found,
579
+ replace=True,
580
+ hard=False,
574
581
  )
575
582
  )
576
583
 
@@ -293,6 +293,7 @@ class CssImport(Import):
293
293
  - Absolute path (e.g., "/path/to/styles.css")
294
294
  module: If True, import as a CSS module (default export for class access).
295
295
  If False, import for side effects only (global styles).
296
+ Automatically set to True if path ends with ".module.css".
296
297
  relative: If True, resolve path relative to the caller's file.
297
298
  before: List of import sources that should come after this import.
298
299
 
@@ -300,8 +301,8 @@ class CssImport(Import):
300
301
  # Side-effect CSS import (global styles)
301
302
  CssImport("@mantine/core/styles.css")
302
303
 
303
- # CSS module for class access
304
- styles = CssImport("./styles.module.css", module=True, relative=True)
304
+ # CSS module for class access (module=True auto-detected from .module.css)
305
+ styles = CssImport("./styles.module.css", relative=True)
305
306
  styles.container # Returns JSMember for 'container' class
306
307
 
307
308
  # Local CSS file (will be copied during codegen)
@@ -316,6 +317,10 @@ class CssImport(Import):
316
317
  relative: bool = False,
317
318
  before: Sequence[str] = (),
318
319
  ) -> None:
320
+ # Auto-detect CSS modules based on filename
321
+ if path.endswith(".module.css"):
322
+ module = True
323
+
319
324
  source_path: Path | None = None
320
325
  import_src = path
321
326
 
@@ -265,7 +265,7 @@ class Component(Generic[P]):
265
265
  if key is not None and not isinstance(key, str):
266
266
  raise ValueError("key must be a string or None")
267
267
 
268
- # Flatten children if component takes children (has *children parameter)
268
+ # Flatten children if component accepts them via `*children` parameter
269
269
  if self._takes_children and args:
270
270
  flattened = _flatten_children(
271
271
  args, # pyright: ignore[reportArgumentType]
@@ -356,11 +356,11 @@ class ComponentNode:
356
356
  )
357
357
  if not isinstance(children_arg, tuple):
358
358
  children_arg = (children_arg,)
359
- # Flatten children for ComponentNode as well
359
+ # Flatten children when component accepts them via `*children` parameter
360
360
  flattened_children = _flatten_children(
361
361
  children_arg, parent_name=f"<{self.name}>", warn_stacklevel=4
362
362
  )
363
- result = ComponentNode(
363
+ return ComponentNode(
364
364
  fn=self.fn,
365
365
  args=tuple(flattened_children),
366
366
  kwargs=self.kwargs,
@@ -368,7 +368,6 @@ class ComponentNode:
368
368
  key=self.key,
369
369
  takes_children=self.takes_children,
370
370
  )
371
- return result
372
371
 
373
372
  @override
374
373
  def __repr__(self) -> str:
@@ -606,19 +605,18 @@ def _callable_qualname(fn: Callable[..., Any]) -> str:
606
605
 
607
606
 
608
607
  def _takes_children(fn: Callable[..., Any]) -> bool:
609
- # Lightweight check: children allowed if function accepts positional
610
- # arguments
608
+ """Return True if function accepts children via `*children` parameter.
609
+
610
+ Convention: A component accepts children if and only if it has a VAR_POSITIONAL
611
+ parameter named "children". This convention should be documented in user-facing docs.
612
+ """
611
613
  try:
612
614
  sig = signature(fn)
613
615
  except (ValueError, TypeError):
614
616
  # Builtins or callables without inspectable signature: assume no children
615
617
  return False
616
618
  for p in sig.parameters.values():
617
- if p.kind in (
618
- Parameter.VAR_POSITIONAL,
619
- Parameter.POSITIONAL_ONLY,
620
- Parameter.POSITIONAL_OR_KEYWORD,
621
- ):
619
+ if p.kind is Parameter.VAR_POSITIONAL and p.name == "children":
622
620
  return True
623
621
  return False
624
622