pulse-framework 0.1.47__py3-none-any.whl → 0.1.49__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
@@ -576,7 +576,10 @@ class App:
576
576
  + "Use 'pulse run' CLI command or set the environment variable."
577
577
  )
578
578
 
579
- proxy_handler = ReactProxy(react_server_address)
579
+ proxy_handler = ReactProxy(
580
+ react_server_address=react_server_address,
581
+ server_address=server_address,
582
+ )
580
583
 
581
584
  @self.fastapi.api_route(
582
585
  "/{path:path}",
pulse/cli/processes.py CHANGED
@@ -257,6 +257,7 @@ def _write_tagged_line(name: str, message: str, colors: Mapping[str, str]) -> No
257
257
  if (
258
258
  "Network: use --host to expose" in clean_message
259
259
  or "press h + enter to show help" in clean_message
260
+ or "➜ Local:" in clean_message
260
261
  ):
261
262
  return
262
263
 
pulse/helpers.py CHANGED
@@ -364,11 +364,11 @@ def get_client_address(request: Request) -> str | None:
364
364
  """Best-effort client origin/address from an HTTP request.
365
365
 
366
366
  Preference order:
367
- 1) Origin (full scheme://host:port)
368
- 1b) Referer (full URL) when Origin missing during prerender forwarding
367
+ 1) Origin header (full scheme://host:port)
368
+ 1b) Referer header (full URL) when Origin missing
369
369
  2) Forwarded header (proto + for)
370
370
  3) X-Forwarded-* headers
371
- 4) request.client host:port
371
+ 4) Host header (server address the client connected to)
372
372
  """
373
373
  try:
374
374
  origin = request.headers.get("origin")
@@ -402,14 +402,10 @@ def get_client_address(request: Request) -> str | None:
402
402
  host = "localhost"
403
403
  return f"{proto}://{host}:{xfp}" if xfp else f"{proto}://{host}"
404
404
 
405
- host = request.client.host if request.client else ""
406
- port = request.client.port if request.client else None
407
- if host in ("127.0.0.1", "::1"):
408
- host = "localhost"
409
- if host and port:
410
- return f"{proto}://{host}:{port}"
411
- if host:
412
- return f"{proto}://{host}"
405
+ # Fallback: use Host header which contains the server address the client connected to
406
+ host_header = request.headers.get("host")
407
+ if host_header:
408
+ return f"{proto}://{host_header}"
413
409
  return None
414
410
  except Exception:
415
411
  return None
@@ -447,14 +443,10 @@ def get_client_address_socketio(environ: dict[str, Any]) -> str | None:
447
443
  host = "localhost"
448
444
  return f"{proto}://{host}:{xfp}" if xfp else f"{proto}://{host}"
449
445
 
450
- host = environ.get("REMOTE_ADDR", "")
451
- port = environ.get("REMOTE_PORT")
452
- if host in ("127.0.0.1", "::1"):
453
- host = "localhost"
454
- if host and port:
455
- return f"{proto}://{host}:{port}"
456
- if host:
457
- return f"{proto}://{host}"
446
+ # Fallback: use HTTP_HOST which contains the server address the client connected to
447
+ host_header = environ.get("HTTP_HOST")
448
+ if host_header:
449
+ return f"{proto}://{host_header}"
458
450
  return None
459
451
  except Exception:
460
452
  return None
pulse/proxy.py CHANGED
@@ -21,19 +21,32 @@ logger = logging.getLogger(__name__)
21
21
  class ReactProxy:
22
22
  """
23
23
  Handles proxying HTTP requests and WebSocket connections to React Router server.
24
+
25
+ In single-server mode, the Python server proxies unmatched routes to the React
26
+ dev server. This proxy rewrites URLs in responses to use the external server
27
+ address instead of the internal React server address.
24
28
  """
25
29
 
26
30
  react_server_address: str
31
+ server_address: str
27
32
  _client: httpx.AsyncClient | None
28
33
 
29
- def __init__(self, react_server_address: str):
34
+ def __init__(self, react_server_address: str, server_address: str):
30
35
  """
31
36
  Args:
32
- react_server_address: React Router server full URL (required in single-server mode)
37
+ react_server_address: Internal React Router server URL (e.g., http://localhost:5173)
38
+ server_address: External server URL exposed to clients (e.g., http://localhost:8000)
33
39
  """
34
40
  self.react_server_address = react_server_address
41
+ self.server_address = server_address
35
42
  self._client = None
36
43
 
44
+ def rewrite_url(self, url: str) -> str:
45
+ """Rewrite internal React server URLs to external server address."""
46
+ if self.react_server_address in url:
47
+ return url.replace(self.react_server_address, self.server_address)
48
+ return url
49
+
37
50
  @property
38
51
  def client(self) -> httpx.AsyncClient:
39
52
  """Lazy initialization of HTTP client."""
@@ -190,12 +203,12 @@ class ReactProxy:
190
203
  # Send request with streaming
191
204
  r = await self.client.send(req, stream=True)
192
205
 
193
- # Filter out headers that shouldn't be present in streaming responses
194
- response_headers = {
195
- k: v
196
- for k, v in r.headers.items()
197
- # if k.lower() not in ("content-length", "transfer-encoding")
198
- }
206
+ # Rewrite headers that may contain internal React server URLs
207
+ response_headers: dict[str, str] = {}
208
+ for k, v in r.headers.items():
209
+ if k.lower() in ("location", "content-location"):
210
+ v = self.rewrite_url(v)
211
+ response_headers[k] = v
199
212
 
200
213
  return StreamingResponse(
201
214
  r.aiter_raw(),
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.47
3
+ Version: 0.1.49
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,5 +1,5 @@
1
1
  pulse/__init__.py,sha256=F97Jf6i99NXZe05ICjGbBjIaT4JURdh7dC1T1-Gnb0o,32790
2
- pulse/app.py,sha256=57o_FrJjKxMDkbxKTzeCUtBRXHfGrCbSpJUnP0Z8evg,31474
2
+ pulse/app.py,sha256=lHlGKQ8kTRTx30cvL3EaBjBZ7rNYVjeItsganPsLqoQ,31540
3
3
  pulse/channel.py,sha256=d9eLxgyB0P9UBVkPkXV7MHkC4LWED1Cq3GKsEu_SYy4,13056
4
4
  pulse/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  pulse/cli/cmd.py,sha256=UBT7OoqWRU-idLOKkA9TDN8m8ugi1gwRMiUJTUmkVfU,14853
@@ -8,7 +8,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=NBV5byBDNoAQSk0vKwibLjoxuA85XBYIyOVJn64L8oU,858
10
10
  pulse/cli/packages.py,sha256=e7ycwwJfdmB4pzrai4DHos6-JzyUgmE4DCZp0BqjdeI,6792
11
- pulse/cli/processes.py,sha256=C1xU72oUanj-1Mkc9WmqESTsVUn_aUHG8URiPyRHSFM,7016
11
+ pulse/cli/processes.py,sha256=5Z8UXzw5rHco7_W67NVhp0fEOKmW8Ewb-_inDz32zMI,7052
12
12
  pulse/cli/secrets.py,sha256=dNfQe6AzSYhZuWveesjCRHIbvaPd3-F9lEJ-kZA7ROw,921
13
13
  pulse/cli/uvicorn_log_config.py,sha256=f7ikDc5foXh3TmFMrnfnW8yev48ZAdlo8F4F_aMVoVk,2391
14
14
  pulse/codegen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -28,7 +28,7 @@ pulse/cookies.py,sha256=c7ua1Lv6mNe1nYnA4SFVvewvRQAbYy9fN5G3Hr_Dr5c,5000
28
28
  pulse/decorators.py,sha256=ywNgLN6VFcKOM5fbFdUUzh-DWk4BuSXdD1BTfd1N-0U,4827
29
29
  pulse/env.py,sha256=p3XI8KG1ZCcXPD3LJP7fW8JPYfyvoYY5ENwae2o0PiA,2889
30
30
  pulse/form.py,sha256=UHVyp9fIZaM-Bi9_he8FBT8A2tI7bnRCn1dJkOat0zw,9022
31
- pulse/helpers.py,sha256=sAIA_X2p6-AEg1Y87NejCZvV82_FtNpePX1Z0sFsdh8,15005
31
+ pulse/helpers.py,sha256=v054teQPOFNJZMgs_G7-BIGsvTLvolTEABgtoSUR3_c,14890
32
32
  pulse/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  pulse/hooks/core.py,sha256=QfYRz2O8-drNSQx_xnv8mK8ksWcw3LNM1H2hoInT0Rk,7457
34
34
  pulse/hooks/effects.py,sha256=pVq5OndlhFLHLpM9Pn9Bp5rEpnpmJEpbIp2UaHHyJFQ,2428
@@ -68,7 +68,7 @@ pulse/js/window.py,sha256=ayx3lBl54hTVanlkiC2wCVGNh0IDJqzPO7OlO11YUtI,4081
68
68
  pulse/messages.py,sha256=PDsb07QDKvkMitAMgLmOk2c4JDb58Cq9WWCwbQ8unvg,3979
69
69
  pulse/middleware.py,sha256=9uyAhVUEGMSwqWC3WXqs7x5JMMNEcSTTu3g7DjsR8w8,9812
70
70
  pulse/plugin.py,sha256=RfGl6Vtr7VRHb8bp4Ob4dOX9dVzvc4Riu7HWnStMPpk,580
71
- pulse/proxy.py,sha256=zh4v5lmYNg5IBE_xdHHmGPwbMQNSXb2npeLXvw_O1Oc,6591
71
+ pulse/proxy.py,sha256=jv2IdOEbF-qbtN5hmSqnyhZedOX1597XBye8cerWIyE,7253
72
72
  pulse/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
73
  pulse/queries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
74
  pulse/queries/client.py,sha256=GGckE0P3YCBO4Mj-08AO_I9eXVC4sIDSNw_xTLrBFuE,15224
@@ -95,7 +95,7 @@ pulse/transpiler/context.py,sha256=e-Nh0AKsq9_wVOI8gL_gn-UAP6HzcYN14zWLfNNzjWw,7
95
95
  pulse/transpiler/errors.py,sha256=JC6tTEmnHf6JdyW4GIvfXB0IBLe7p3FvCLh14PocH28,43
96
96
  pulse/transpiler/function.py,sha256=rP-MZl15_mwaGwPToPcFVYHXqyVdMn91cxtF7MKFPHA,7526
97
97
  pulse/transpiler/ids.py,sha256=d91B_LFaAALKXHjGPmL8tJmDqGDFz7-GquYmnV9IZ0o,327
98
- pulse/transpiler/imports.py,sha256=C4lSi5cRQcoo559_urDqbHAdeTiSeU3eEh89o2YDWhI,10848
98
+ pulse/transpiler/imports.py,sha256=zTBd4uyWlSE0dzS8muWwuhnmZKKZ3Ezjg4qa5MokIKo,11041
99
99
  pulse/transpiler/js_module.py,sha256=AjguCbKV_FnoanzojrW5-vlW-DQCTtyd-4FfBYQLxxQ,9603
100
100
  pulse/transpiler/modules/__init__.py,sha256=YPM2WhILHXQFDSxDcbs-hHdhFh6i3N5ZJLEVGDZeR3Y,1065
101
101
  pulse/transpiler/modules/asyncio.py,sha256=FetybIKGJhVJ3uEQJBw6Z2fsh6g7vA-9tqec4nn5FtI,1409
@@ -113,7 +113,7 @@ pulse/types/event_handler.py,sha256=psQCydj-WEtBcFU5JU4mDwvyzkW8V2O0g_VFRU2EOHI,
113
113
  pulse/user_session.py,sha256=FITxLSEl3JU-jod6UWuUYC6EpnPG2rbaLCnIOdkQPtg,7803
114
114
  pulse/vdom.py,sha256=yHMOYiXcaP3IVnYOAZrd_7cXWULKQ2fRQAiPDPSsOyU,18494
115
115
  pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
116
- pulse_framework-0.1.47.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
117
- pulse_framework-0.1.47.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
118
- pulse_framework-0.1.47.dist-info/METADATA,sha256=AMFl4QOZ07Oxunrd6GrbkKyUiFW6C04HOQuixmt_hcE,580
119
- pulse_framework-0.1.47.dist-info/RECORD,,
116
+ pulse_framework-0.1.49.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
117
+ pulse_framework-0.1.49.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
118
+ pulse_framework-0.1.49.dist-info/METADATA,sha256=HKBRKLM8CJdL2a7oguyCDzayWPqIBQirMMN40jFrSTw,580
119
+ pulse_framework-0.1.49.dist-info/RECORD,,