pulse-framework 0.1.38a1__py3-none-any.whl → 0.1.38a2__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/app.py CHANGED
@@ -165,7 +165,6 @@ class App:
165
165
  internal_server_address: str | None = None,
166
166
  not_found: str = "/not-found",
167
167
  # Deployment and integration options
168
- env: PulseEnv | None = None,
169
168
  mode: PulseMode = "single-server",
170
169
  api_prefix: str = "/_pulse",
171
170
  cors: CORSOptions | None = None,
@@ -179,7 +178,7 @@ class App:
179
178
  codegen: Optional codegen configuration.
180
179
  """
181
180
  # Resolve mode from environment and expose on the app instance
182
- self.env = env or envvars.pulse_env
181
+ self.env = envvars.pulse_env
183
182
  self.mode = mode
184
183
  self.status = AppStatus.created
185
184
  # Persist the server address for use by sessions (API calls, etc.)
@@ -326,9 +325,9 @@ class App:
326
325
  )
327
326
  # Production: use Node to serve the built bundle (Bun lacks renderToPipeableStream)
328
327
  cmd = [
329
- "node",
330
- str(cli_path),
331
- "./build/server/index.js",
328
+ "bun",
329
+ "run",
330
+ "start",
332
331
  "--port",
333
332
  str(port),
334
333
  ]
@@ -464,11 +463,35 @@ class App:
464
463
  self.fastapi.add_middleware(CORSMiddleware, **self.cors)
465
464
  else:
466
465
  # Use deployment-specific CORS settings
466
+ cors_config = cors_options(self.mode, self.server_address)
467
+ print(f"CORS config: {cors_config}")
467
468
  self.fastapi.add_middleware(
468
469
  CORSMiddleware,
469
- **cors_options(self.mode, self.server_address),
470
+ **cors_config,
470
471
  )
471
472
 
473
+ # Debug middleware to log CORS-related request details
474
+ @self.fastapi.middleware("http")
475
+ async def cors_debug_middleware( # pyright: ignore[reportUnusedFunction]
476
+ request: Request, call_next: Callable[[Request], Awaitable[Response]]
477
+ ):
478
+ origin = request.headers.get("origin")
479
+ method = request.method
480
+ path = request.url.path
481
+ print(
482
+ f"[CORS Debug] {method} {path} | Origin: {origin} | "
483
+ + f"Mode: {self.mode} | Server: {self.server_address}"
484
+ )
485
+ response = await call_next(request)
486
+ allow_origin = response.headers.get("access-control-allow-origin")
487
+ if allow_origin:
488
+ print(f"[CORS Debug] Response allows origin: {allow_origin}")
489
+ elif origin:
490
+ logger.warning(
491
+ f"[CORS Debug] Origin {origin} present but no Access-Control-Allow-Origin header set"
492
+ )
493
+ return response
494
+
472
495
  # Mount PulseContext for all FastAPI routes (no route info). Other API
473
496
  # routes / middleware should be added at the module-level, which means
474
497
  # this middleware will wrap all of them.
pulse/cli/cmd.py CHANGED
@@ -21,6 +21,7 @@ from pulse.cli.dependencies import (
21
21
  DependencyError,
22
22
  DependencyPlan,
23
23
  DependencyResolutionError,
24
+ check_web_dependencies,
24
25
  prepare_web_dependencies,
25
26
  )
26
27
  from pulse.cli.folder_lock import FolderLock
@@ -68,7 +69,7 @@ def run(
68
69
  prod: bool = typer.Option(False, "--prod", help="Run in production env"),
69
70
  server_only: bool = typer.Option(False, "--server-only", "--backend-only"),
70
71
  web_only: bool = typer.Option(False, "--web-only"),
71
- reload: bool = typer.Option(True, "--reload/--no-reload"),
72
+ reload: bool | None = typer.Option(None, "--reload/--no-reload"),
72
73
  find_port: bool = typer.Option(True, "--find-port/--no-find-port"),
73
74
  ):
74
75
  """Run the Pulse server and web development server together."""
@@ -88,6 +89,10 @@ def run(
88
89
  if len(env_flags) == 1:
89
90
  env.pulse_env = cast(PulseEnv, env_flags[0])
90
91
 
92
+ # Turn on reload in dev only
93
+ if reload is None:
94
+ reload = env.pulse_env == "dev"
95
+
91
96
  if server_only and web_only:
92
97
  typer.echo("❌ Cannot use --server-only and --web-only at the same time.")
93
98
  raise typer.Exit(1)
@@ -121,10 +126,10 @@ def run(
121
126
  )
122
127
 
123
128
  if not server_only:
124
- # Skip dependency installation in production mode (assumes pre-built)
125
- if app_instance.env != "prod":
129
+ # Skip dependency checking and installation in production/CI mode (assumes pre-built)
130
+ if env.pulse_env == "dev":
126
131
  try:
127
- dep_plan = prepare_web_dependencies(
132
+ to_add = check_web_dependencies(
128
133
  web_root,
129
134
  pulse_version=PULSE_PY_VERSION,
130
135
  )
@@ -135,9 +140,14 @@ def run(
135
140
  console.log(f"❌ {exc}")
136
141
  raise typer.Exit(1) from None
137
142
 
138
- if dep_plan:
143
+ if to_add:
139
144
  try:
140
- _run_dependency_plan(console, web_root, dep_plan)
145
+ dep_plan = prepare_web_dependencies(
146
+ web_root,
147
+ pulse_version=PULSE_PY_VERSION,
148
+ )
149
+ if dep_plan:
150
+ _run_dependency_plan(console, web_root, dep_plan)
141
151
  except subprocess.CalledProcessError:
142
152
  console.log("❌ Failed to install web dependencies with Bun.")
143
153
  raise typer.Exit(1) from None
@@ -219,6 +229,66 @@ def generate(
219
229
  console.log("⚠️ No routes found to generate")
220
230
 
221
231
 
232
+ @cli.command("check")
233
+ def check(
234
+ app_file: str = typer.Argument(
235
+ ..., help="App target: 'path.py[:var]' (default :app) or 'module:var'"
236
+ ),
237
+ fix: bool = typer.Option(
238
+ False, "--fix", help="Install missing or outdated dependencies"
239
+ ),
240
+ ):
241
+ """Check if web project dependencies are in sync with Pulse app requirements."""
242
+ console = Console()
243
+
244
+ console.log(f"📁 Loading app from: {app_file}")
245
+ app_ctx = load_app_from_target(app_file)
246
+ _apply_app_context_to_env(app_ctx)
247
+ app_instance = app_ctx.app
248
+
249
+ web_root = app_instance.codegen.cfg.web_root
250
+ if not web_root.exists():
251
+ console.log(f"❌ Directory not found: {web_root.absolute()}")
252
+ raise typer.Exit(1)
253
+
254
+ try:
255
+ to_add = check_web_dependencies(
256
+ web_root,
257
+ pulse_version=PULSE_PY_VERSION,
258
+ )
259
+ except DependencyResolutionError as exc:
260
+ console.log(f"❌ {exc}")
261
+ raise typer.Exit(1) from None
262
+ except DependencyError as exc:
263
+ console.log(f"❌ {exc}")
264
+ raise typer.Exit(1) from None
265
+
266
+ if not to_add:
267
+ console.log("✅ Web dependencies are in sync")
268
+ return
269
+
270
+ console.log("📦 Web dependencies are out of sync:")
271
+ for pkg in to_add:
272
+ console.log(f" - {pkg}")
273
+
274
+ if not fix:
275
+ console.log("💡 Run 'pulse check --fix' to install missing dependencies")
276
+ return
277
+
278
+ # Apply fix
279
+ try:
280
+ dep_plan = prepare_web_dependencies(
281
+ web_root,
282
+ pulse_version=PULSE_PY_VERSION,
283
+ )
284
+ if dep_plan:
285
+ _run_dependency_plan(console, web_root, dep_plan)
286
+ console.log("✅ Web dependencies synced successfully")
287
+ except subprocess.CalledProcessError:
288
+ console.log("❌ Failed to install web dependencies with Bun.")
289
+ raise typer.Exit(1) from None
290
+
291
+
222
292
  def build_uvicorn_command(
223
293
  *,
224
294
  app_ctx: AppLoadResult,
@@ -245,7 +315,7 @@ def build_uvicorn_command(
245
315
  "--factory",
246
316
  ]
247
317
 
248
- if reload_enabled and app_ctx.app.env != "prod":
318
+ if reload_enabled:
249
319
  args.append("--reload")
250
320
  args.extend(["--reload-include", "*.css"])
251
321
  app_dir = app_ctx.app_dir or Path.cwd()
pulse/cli/dependencies.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
3
4
  from collections.abc import Callable, Iterable, Sequence
4
5
  from dataclasses import dataclass
5
6
  from pathlib import Path
@@ -18,6 +19,53 @@ from pulse.cli.packages import (
18
19
  from pulse.react_component import ReactComponent, registered_react_components
19
20
 
20
21
 
22
+ def convert_pep440_to_semver(python_version: str) -> str:
23
+ """Convert PEP 440 version format to NPM semver format.
24
+
25
+ PEP 440 formats:
26
+ - 0.1.37a1 -> 0.1.37-alpha.1
27
+ - 0.1.37b1 -> 0.1.37-beta.1
28
+ - 0.1.37rc1 -> 0.1.37-rc.1
29
+ - 0.1.37.dev1 -> 0.1.37-dev.1
30
+
31
+ Non-pre-release versions are returned unchanged.
32
+ """
33
+ # Match pre-release patterns: version followed by a/b/rc/dev + number
34
+ # PEP 440: a1, b1, rc1, dev1, alpha1, beta1, etc.
35
+ pattern = r"^(\d+\.\d+\.\d+)([a-z]+)(\d+)$"
36
+ match = re.match(pattern, python_version)
37
+
38
+ if match:
39
+ base_version = match.group(1)
40
+ prerelease_type = match.group(2)
41
+ prerelease_num = match.group(3)
42
+
43
+ # Map PEP 440 prerelease types to NPM semver
44
+ type_map = {
45
+ "a": "alpha",
46
+ "alpha": "alpha",
47
+ "b": "beta",
48
+ "beta": "beta",
49
+ "rc": "rc",
50
+ "c": "rc", # PEP 440 also allows 'c' for release candidate
51
+ "dev": "dev",
52
+ }
53
+
54
+ npm_type = type_map.get(prerelease_type.lower(), prerelease_type)
55
+ return f"{base_version}-{npm_type}.{prerelease_num}"
56
+
57
+ # Also handle .dev format (e.g., 0.1.37.dev1)
58
+ pattern2 = r"^(\d+\.\d+\.\d+)\.dev(\d+)$"
59
+ match2 = re.match(pattern2, python_version)
60
+ if match2:
61
+ base_version = match2.group(1)
62
+ dev_num = match2.group(2)
63
+ return f"{base_version}-dev.{dev_num}"
64
+
65
+ # No pre-release, return as-is
66
+ return python_version
67
+
68
+
21
69
  class DependencyError(RuntimeError):
22
70
  """Base error for dependency preparation failures."""
23
71
 
@@ -38,15 +86,15 @@ class DependencyPlan:
38
86
  to_add: Sequence[str]
39
87
 
40
88
 
41
- def prepare_web_dependencies(
89
+ def get_required_dependencies(
42
90
  web_root: Path,
43
91
  *,
44
92
  pulse_version: str,
45
93
  component_provider: Callable[
46
94
  [], Iterable[ReactComponent[Any]]
47
95
  ] = registered_react_components,
48
- ) -> DependencyPlan | None:
49
- """Inspect registered components and return the Bun command needed to sync dependencies."""
96
+ ) -> dict[str, str | None]:
97
+ """Get the required dependencies for a Pulse app."""
50
98
  if not web_root.exists():
51
99
  raise DependencyError(f"Directory not found: {web_root}")
52
100
 
@@ -102,13 +150,30 @@ def prepare_web_dependencies(
102
150
  ]:
103
151
  desired.setdefault(pkg, "^7")
104
152
 
153
+ return desired
154
+
155
+
156
+ def check_web_dependencies(
157
+ web_root: Path,
158
+ *,
159
+ pulse_version: str,
160
+ component_provider: Callable[
161
+ [], Iterable[ReactComponent[Any]]
162
+ ] = registered_react_components,
163
+ ) -> list[str]:
164
+ """Check if web dependencies are in sync and return list of packages that need to be added/updated."""
165
+ desired = get_required_dependencies(
166
+ web_root=web_root,
167
+ pulse_version=pulse_version,
168
+ component_provider=component_provider,
169
+ )
105
170
  pkg_json = load_package_json(web_root)
106
171
 
107
172
  to_add: list[str] = []
108
173
  for name, req_ver in sorted(desired.items()):
109
174
  effective = req_ver
110
175
  if name == "pulse-ui-client":
111
- effective = pulse_version
176
+ effective = convert_pep440_to_semver(pulse_version)
112
177
 
113
178
  existing = get_pkg_spec(pkg_json, name)
114
179
  if existing is None:
@@ -123,6 +188,24 @@ def prepare_web_dependencies(
123
188
 
124
189
  to_add.append(f"{name}@{effective}" if effective else name)
125
190
 
191
+ return to_add
192
+
193
+
194
+ def prepare_web_dependencies(
195
+ web_root: Path,
196
+ *,
197
+ pulse_version: str,
198
+ component_provider: Callable[
199
+ [], Iterable[ReactComponent[Any]]
200
+ ] = registered_react_components,
201
+ ) -> DependencyPlan | None:
202
+ """Inspect registered components and return the Bun command needed to sync dependencies."""
203
+ to_add = check_web_dependencies(
204
+ web_root=web_root,
205
+ pulse_version=pulse_version,
206
+ component_provider=component_provider,
207
+ )
208
+
126
209
  if to_add:
127
210
  return DependencyPlan(command=["bun", "add", *to_add], to_add=to_add)
128
211
 
pulse/cookies.py CHANGED
@@ -155,10 +155,15 @@ def cors_options(mode: "PulseMode", server_address: str) -> CORSOptions:
155
155
  }
156
156
  if mode == "subdomains":
157
157
  base = _base_domain(host)
158
+ # Escape dots in base domain for regex (doesn't affect localhost since it has no dots)
159
+ base = base.replace(".", r"\.")
160
+ # Allow any subdomain and any port for the base domain
158
161
  opts["allow_origin_regex"] = rf"^https?://([a-z0-9-]+\\.)?{base}(:\\d+)?$"
159
162
  return opts
160
163
  elif mode == "single-server":
161
164
  # For single-server mode, allow same origin
165
+ # Escape dots in host for regex (doesn't affect localhost since it has no dots)
166
+ host = host.replace(".", r"\.")
162
167
  opts["allow_origin_regex"] = rf"^https?://{host}(:\\d+)?$"
163
168
  return opts
164
169
  else:
pulse/env.py CHANGED
@@ -39,13 +39,12 @@ class EnvVars:
39
39
  else:
40
40
  os.environ[key] = value
41
41
 
42
- # Pulse mode
43
42
  @property
44
43
  def pulse_env(self) -> PulseEnv:
45
44
  value = (self._get(ENV_PULSE_MODE) or "dev").lower()
46
45
  if value not in ("dev", "ci", "prod"):
47
46
  value = "dev"
48
- return value # type: ignore[return-value]
47
+ return value
49
48
 
50
49
  @pulse_env.setter
51
50
  def pulse_env(self, value: PulseEnv) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.38a1
3
+ Version: 0.1.38a2
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,9 +1,9 @@
1
1
  pulse/__init__.py,sha256=aJg4LvIeLYalXvEe1u47_A9ktS3HGIf4ZeAo6w1tbMQ,31463
2
- pulse/app.py,sha256=984KdVjyAiWjuRZq5vYw1tcHAx_muu9Pj4xkOQxrhqc,28849
2
+ pulse/app.py,sha256=rCCbRUK7lNzsASbnTmPVG-JRjG0v8LTKPLTvCnPS978,29675
3
3
  pulse/channel.py,sha256=DuD1mg_xWvkpAWSKZ-EtBYdUzJ8IuKH0fxdgGOvFXpg,13041
4
4
  pulse/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- pulse/cli/cmd.py,sha256=cASUDMb2yUh4bsx6W-Ee8_qrkxzhcBsjruD2k77pdqo,10126
6
- pulse/cli/dependencies.py,sha256=dH2HxbaFB5b8q7KbAs0sh9SDxXc9JlMAmRKnEtL88q4,3442
5
+ pulse/cli/cmd.py,sha256=_5vZL-7qSHA20CPocS5MH5KhG1giSn1t11T4kdYmzCY,11951
6
+ pulse/cli/dependencies.py,sha256=ZBqBAfMvMBQUvh4THdPDztTMQ_dyR52S1IuotP_eEZs,5623
7
7
  pulse/cli/folder_lock.py,sha256=kvUmZBg869lwCTIZFoge9dhorv8qPXHTWwVv_jQg1k8,3477
8
8
  pulse/cli/helpers.py,sha256=8bRlV3d7w3w-jHaFvFYt9Pzue6_CbKOq_Z3jBsBOeUk,8820
9
9
  pulse/cli/models.py,sha256=hRmIWmhXmGf2otzVm1do4Dm19rkWkmTwAA3Am3kw2tE,692
@@ -25,10 +25,10 @@ pulse/components/for_.py,sha256=LUyJEUlDM6b9oPjvUFgSsddxu6b6usF4BQdXe8FIiGI,1302
25
25
  pulse/components/if_.py,sha256=rQywsmdirNpkb-61ZEdF-tgzUh-37JWd4YFGblkzIdQ,1624
26
26
  pulse/components/react_router.py,sha256=TbRec-NVliUqrvAMeFXCrnDWV1rh6TGTPfRhqLuLubk,1129
27
27
  pulse/context.py,sha256=x_nCbCEUGygAdCZiTfko5uuYxVSAeCNhYa59zBq015M,1692
28
- pulse/cookies.py,sha256=TgPw193ODQpHfnJ0cFV_nQYhgdQzWSWrt3syyy5JDr0,4704
28
+ pulse/cookies.py,sha256=c7ua1Lv6mNe1nYnA4SFVvewvRQAbYy9fN5G3Hr_Dr5c,5000
29
29
  pulse/css.py,sha256=-FyQQQ0EZI1Ins30qiF3l4z9yDb1V9qWuJKWxHcKGkw,3910
30
30
  pulse/decorators.py,sha256=8At1HQTFs9KG7nd83miGMe3KkhTBVGDviaqZaY62bHI,6651
31
- pulse/env.py,sha256=NH2fB_EXXQJeRWMmWFN5KCzEcfM0HkyCkf-f9MaYevo,2619
31
+ pulse/env.py,sha256=BMEsIzR1_4c5bZzou7kYtSMk50gLnZ0Yf6l8U6Ve4IE,2575
32
32
  pulse/form.py,sha256=M87QwG4KFOrI8Nba7BTDoJ_wZ1-jzJW7QN4JweYCpuM,9004
33
33
  pulse/helpers.py,sha256=q54JGen1lBIEGyLnNKvmW_7nnTFqFZBDMYXsoXvth7o,11628
34
34
  pulse/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -65,7 +65,7 @@ pulse/types/event_handler.py,sha256=OF7sOgYBb6iUs59RH1vQIH7aOrGPfs3nAaF7how-4PQ,
65
65
  pulse/user_session.py,sha256=kCZtQpYZe2keDXzusd6jsjjw075am0dXrb25jKLg5JU,7578
66
66
  pulse/vdom.py,sha256=KTNBh2dVvDy9eXRzhneBJgk7F35MyWec8R_puQ4tSRY,12420
67
67
  pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
68
- pulse_framework-0.1.38a1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
69
- pulse_framework-0.1.38a1.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
70
- pulse_framework-0.1.38a1.dist-info/METADATA,sha256=FiNrclvlk25QFQHx9RHPOgQIeqC-HwAzb5edOMpEHn0,582
71
- pulse_framework-0.1.38a1.dist-info/RECORD,,
68
+ pulse_framework-0.1.38a2.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
69
+ pulse_framework-0.1.38a2.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
70
+ pulse_framework-0.1.38a2.dist-info/METADATA,sha256=Il4RlKJSqYB_RSQ5JyRJb0Qu897nUm-MACZA1VgrV3Y,582
71
+ pulse_framework-0.1.38a2.dist-info/RECORD,,