pulse-framework 0.1.37__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/__init__.py +2 -2
- pulse/app.py +224 -75
- pulse/cli/cmd.py +294 -394
- pulse/cli/dependencies.py +212 -0
- pulse/cli/folder_lock.py +134 -0
- pulse/cli/helpers.py +82 -43
- pulse/cli/models.py +33 -0
- pulse/cli/processes.py +225 -0
- pulse/cli/secrets.py +39 -0
- pulse/cli/uvicorn_log_config.py +87 -0
- pulse/codegen/codegen.py +16 -3
- pulse/codegen/templates/layout.py +3 -2
- pulse/cookies.py +17 -16
- pulse/env.py +8 -18
- pulse/helpers.py +19 -122
- pulse/proxy.py +192 -0
- pulse/user_session.py +2 -2
- {pulse_framework-0.1.37.dist-info → pulse_framework-0.1.38a2.dist-info}/METADATA +2 -1
- {pulse_framework-0.1.37.dist-info → pulse_framework-0.1.38a2.dist-info}/RECORD +21 -14
- {pulse_framework-0.1.37.dist-info → pulse_framework-0.1.38a2.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.37.dist-info → pulse_framework-0.1.38a2.dist-info}/entry_points.txt +0 -0
pulse/helpers.py
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import inspect
|
|
3
|
-
import json
|
|
4
3
|
import os
|
|
5
|
-
import platform
|
|
6
4
|
import socket
|
|
7
|
-
import time
|
|
8
5
|
from collections.abc import Awaitable, Callable, Coroutine
|
|
9
|
-
from
|
|
10
|
-
from typing import (
|
|
11
|
-
Any,
|
|
12
|
-
ParamSpec,
|
|
13
|
-
Protocol,
|
|
14
|
-
TypedDict,
|
|
15
|
-
TypeVar,
|
|
16
|
-
overload,
|
|
17
|
-
override,
|
|
18
|
-
)
|
|
6
|
+
from typing import Any, ParamSpec, Protocol, TypedDict, TypeVar, overload, override
|
|
19
7
|
from urllib.parse import urlsplit
|
|
20
8
|
|
|
21
9
|
from anyio import from_thread
|
|
@@ -380,115 +368,8 @@ def get_client_address_socketio(environ: dict[str, Any]) -> str | None:
|
|
|
380
368
|
return None
|
|
381
369
|
|
|
382
370
|
|
|
383
|
-
# --- Runtime lock helpers
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
def _is_process_alive(pid: int) -> bool:
|
|
387
|
-
try:
|
|
388
|
-
# On POSIX, signal 0 checks for existence without killing
|
|
389
|
-
os.kill(pid, 0)
|
|
390
|
-
except ProcessLookupError:
|
|
391
|
-
return False
|
|
392
|
-
except PermissionError:
|
|
393
|
-
# Process exists but we may not have permission
|
|
394
|
-
return True
|
|
395
|
-
except Exception:
|
|
396
|
-
# Best-effort: assume alive if uncertain
|
|
397
|
-
return True
|
|
398
|
-
return True
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
def lock_path_for_web_root(web_root: Path, filename: str = ".pulse.lock") -> Path:
|
|
402
|
-
return Path(web_root) / filename
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
def write_gitignore_for_lock(lock_path: Path) -> None:
|
|
406
|
-
try:
|
|
407
|
-
gitignore_path = lock_path.parent / ".gitignore"
|
|
408
|
-
pattern = f"\n{lock_path.name}\n"
|
|
409
|
-
if gitignore_path.exists():
|
|
410
|
-
try:
|
|
411
|
-
content = gitignore_path.read_text()
|
|
412
|
-
except Exception:
|
|
413
|
-
content = ""
|
|
414
|
-
if lock_path.name not in content.split():
|
|
415
|
-
gitignore_path.write_text(content + pattern)
|
|
416
|
-
else:
|
|
417
|
-
gitignore_path.write_text(pattern.lstrip("\n"))
|
|
418
|
-
except Exception:
|
|
419
|
-
# Non-fatal
|
|
420
|
-
pass
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
def _read_lock(lock_path: Path) -> dict[str, Any] | None:
|
|
424
|
-
try:
|
|
425
|
-
data = json.loads(lock_path.read_text())
|
|
426
|
-
if isinstance(data, dict):
|
|
427
|
-
return data
|
|
428
|
-
except Exception:
|
|
429
|
-
return None
|
|
430
|
-
return None
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
def ensure_web_lock(lock_path: Path, *, owner: str = "server") -> tuple[Path, bool]:
|
|
434
|
-
"""Create a lock file or raise if an active one exists.
|
|
435
|
-
|
|
436
|
-
Returns (lock_path, created_now)
|
|
437
|
-
"""
|
|
438
|
-
lock_path = Path(lock_path)
|
|
439
|
-
write_gitignore_for_lock(lock_path)
|
|
440
|
-
|
|
441
|
-
if lock_path.exists():
|
|
442
|
-
info = _read_lock(lock_path) or {}
|
|
443
|
-
pid = int(info.get("pid", 0) or 0)
|
|
444
|
-
if pid and _is_process_alive(pid):
|
|
445
|
-
raise RuntimeError(
|
|
446
|
-
f"Another Pulse dev instance appears to be running (pid={pid}) for {lock_path.parent}."
|
|
447
|
-
)
|
|
448
|
-
# Stale lock; continue to overwrite
|
|
449
|
-
|
|
450
|
-
payload = {
|
|
451
|
-
"pid": os.getpid(),
|
|
452
|
-
"owner": owner,
|
|
453
|
-
"created_at": int(time.time()),
|
|
454
|
-
"hostname": socket.gethostname(),
|
|
455
|
-
"platform": platform.platform(),
|
|
456
|
-
"python": platform.python_version(),
|
|
457
|
-
"cwd": os.getcwd(),
|
|
458
|
-
}
|
|
459
|
-
try:
|
|
460
|
-
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
461
|
-
lock_path.write_text(json.dumps(payload))
|
|
462
|
-
except Exception as exc:
|
|
463
|
-
raise RuntimeError(f"Failed to create lock file at {lock_path}: {exc}") from exc
|
|
464
|
-
return lock_path, True
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
def validate_existing_lock(lock_path: Path) -> bool:
|
|
468
|
-
"""Validate an existing lock. Returns True if an active other instance exists.
|
|
469
|
-
|
|
470
|
-
If the file is missing or stale, returns False. If an active other instance is
|
|
471
|
-
detected, raises RuntimeError.
|
|
472
|
-
"""
|
|
473
|
-
lock_path = Path(lock_path)
|
|
474
|
-
if not lock_path.exists():
|
|
475
|
-
return False
|
|
476
|
-
info = _read_lock(lock_path) or {}
|
|
477
|
-
pid = int(info.get("pid", 0) or 0)
|
|
478
|
-
if pid and _is_process_alive(pid):
|
|
479
|
-
# Active lock
|
|
480
|
-
raise RuntimeError(
|
|
481
|
-
f"Another Pulse dev instance appears to be running (pid={pid}) for {lock_path.parent}."
|
|
482
|
-
)
|
|
483
|
-
return False
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
def remove_web_lock(lock_path: Path) -> None:
|
|
487
|
-
try:
|
|
488
|
-
Path(lock_path).unlink(missing_ok=True)
|
|
489
|
-
except Exception:
|
|
490
|
-
# Best-effort cleanup
|
|
491
|
-
pass
|
|
371
|
+
# --- Runtime lock helpers moved to pulse.cli.web_lock ---
|
|
372
|
+
# Use WebLock context manager for idempotent lock management
|
|
492
373
|
|
|
493
374
|
|
|
494
375
|
@overload
|
|
@@ -532,3 +413,19 @@ async def maybe_await(value: T | Awaitable[T]) -> T:
|
|
|
532
413
|
if inspect.isawaitable(value):
|
|
533
414
|
return await value
|
|
534
415
|
return value
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def find_available_port(start_port: int = 8000, max_attempts: int = 100) -> int:
|
|
419
|
+
"""Find an available port starting from start_port."""
|
|
420
|
+
for port in range(start_port, start_port + max_attempts):
|
|
421
|
+
try:
|
|
422
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
423
|
+
# Allow reuse of addresses in TIME_WAIT state (matches uvicorn behavior)
|
|
424
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
425
|
+
s.bind(("localhost", port))
|
|
426
|
+
return port
|
|
427
|
+
except OSError:
|
|
428
|
+
continue
|
|
429
|
+
raise RuntimeError(
|
|
430
|
+
f"Could not find an available port after {max_attempts} attempts starting from {start_port}"
|
|
431
|
+
)
|
pulse/proxy.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Proxy ASGI app for forwarding requests to React Router server in single-server mode.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from typing import Callable, cast
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from starlette.datastructures import Headers
|
|
11
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PulseProxy:
|
|
17
|
+
"""
|
|
18
|
+
ASGI app that proxies non-API requests to React Router server.
|
|
19
|
+
|
|
20
|
+
In single-server mode, Python FastAPI handles /_pulse/* routes and
|
|
21
|
+
proxies everything else to the React Router server running on an internal port.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
app: ASGIApp,
|
|
27
|
+
get_web_port: Callable[[], int | None],
|
|
28
|
+
api_prefix: str = "/_pulse",
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initialize proxy ASGI app.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
app: The ASGI application to wrap (socketio.ASGIApp)
|
|
35
|
+
get_web_port: Callable that returns the React Router port (or None if not started)
|
|
36
|
+
api_prefix: Prefix for API routes that should NOT be proxied (default: "/_pulse")
|
|
37
|
+
"""
|
|
38
|
+
self.app: ASGIApp = app
|
|
39
|
+
self.get_web_port: Callable[[], int | None] = get_web_port
|
|
40
|
+
self.api_prefix: str = api_prefix
|
|
41
|
+
self._client: httpx.AsyncClient | None = None
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def client(self) -> httpx.AsyncClient:
|
|
45
|
+
"""Lazy initialization of HTTP client."""
|
|
46
|
+
if self._client is None:
|
|
47
|
+
self._client = httpx.AsyncClient(
|
|
48
|
+
timeout=httpx.Timeout(30.0),
|
|
49
|
+
follow_redirects=False,
|
|
50
|
+
)
|
|
51
|
+
return self._client
|
|
52
|
+
|
|
53
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
54
|
+
"""
|
|
55
|
+
ASGI application handler.
|
|
56
|
+
|
|
57
|
+
Routes starting with api_prefix or WebSocket connections go to FastAPI.
|
|
58
|
+
Everything else is proxied to React Router.
|
|
59
|
+
"""
|
|
60
|
+
if scope["type"] != "http":
|
|
61
|
+
# Pass through non-HTTP requests (WebSocket, lifespan, etc.)
|
|
62
|
+
await self.app(scope, receive, send)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
path = scope["path"]
|
|
66
|
+
|
|
67
|
+
# Check if path starts with API prefix or is a WebSocket upgrade
|
|
68
|
+
if path.startswith(self.api_prefix):
|
|
69
|
+
# This is an API route, pass through to FastAPI
|
|
70
|
+
await self.app(scope, receive, send)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# Check if this is a WebSocket upgrade request (even if not prefixed)
|
|
74
|
+
headers = Headers(scope=scope)
|
|
75
|
+
if headers.get("upgrade", "").lower() == "websocket":
|
|
76
|
+
# WebSocket request, pass through to FastAPI
|
|
77
|
+
await self.app(scope, receive, send)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Proxy to React Router server
|
|
81
|
+
await self._proxy_request(scope, receive, send)
|
|
82
|
+
|
|
83
|
+
async def _proxy_request(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Forward HTTP request to React Router server and stream response back.
|
|
86
|
+
"""
|
|
87
|
+
# Get the web server port
|
|
88
|
+
port = self.get_web_port()
|
|
89
|
+
if port is None:
|
|
90
|
+
# Web server not started yet, return error
|
|
91
|
+
await send(
|
|
92
|
+
{
|
|
93
|
+
"type": "http.response.start",
|
|
94
|
+
"status": 503,
|
|
95
|
+
"headers": [(b"content-type", b"text/plain")],
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
await send(
|
|
99
|
+
{
|
|
100
|
+
"type": "http.response.body",
|
|
101
|
+
"body": b"Service Unavailable: Web server not ready",
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
# Build target URL
|
|
107
|
+
path = scope["path"]
|
|
108
|
+
query_string = scope.get("query_string", b"").decode("utf-8")
|
|
109
|
+
target_url = f"http://localhost:{port}"
|
|
110
|
+
target_path = f"{target_url}{path}"
|
|
111
|
+
if query_string:
|
|
112
|
+
target_path += f"?{query_string}"
|
|
113
|
+
|
|
114
|
+
# Extract headers
|
|
115
|
+
headers: dict[str, str] = {}
|
|
116
|
+
for name, value in cast(Iterable[tuple[bytes, bytes]], scope["headers"]):
|
|
117
|
+
name = name.decode("latin1")
|
|
118
|
+
value = value.decode("latin1")
|
|
119
|
+
|
|
120
|
+
# Skip host header (will be set by httpx)
|
|
121
|
+
if name.lower() == "host":
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
# Collect headers (handle multiple values)
|
|
125
|
+
existing = headers.get(name)
|
|
126
|
+
if existing:
|
|
127
|
+
headers[name] = f"{existing},{value}"
|
|
128
|
+
else:
|
|
129
|
+
headers[name] = value
|
|
130
|
+
|
|
131
|
+
# Read request body
|
|
132
|
+
body_parts: list[bytes] = []
|
|
133
|
+
while True:
|
|
134
|
+
message = await receive()
|
|
135
|
+
if message["type"] == "http.request":
|
|
136
|
+
body_parts.append(message.get("body", b""))
|
|
137
|
+
if not message.get("more_body", False):
|
|
138
|
+
break
|
|
139
|
+
body = b"".join(body_parts)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
# Forward request to React Router
|
|
143
|
+
method = scope["method"]
|
|
144
|
+
response = await self.client.request(
|
|
145
|
+
method=method,
|
|
146
|
+
url=target_path,
|
|
147
|
+
headers=headers,
|
|
148
|
+
content=body,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Send response status
|
|
152
|
+
await send(
|
|
153
|
+
{
|
|
154
|
+
"type": "http.response.start",
|
|
155
|
+
"status": response.status_code,
|
|
156
|
+
"headers": [
|
|
157
|
+
(name.encode("latin1"), value.encode("latin1"))
|
|
158
|
+
for name, value in response.headers.items()
|
|
159
|
+
],
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Stream response body
|
|
164
|
+
await send(
|
|
165
|
+
{
|
|
166
|
+
"type": "http.response.body",
|
|
167
|
+
"body": response.content,
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
except httpx.RequestError as e:
|
|
172
|
+
logger.error(f"Proxy request failed: {e}")
|
|
173
|
+
|
|
174
|
+
# Send error response
|
|
175
|
+
await send(
|
|
176
|
+
{
|
|
177
|
+
"type": "http.response.start",
|
|
178
|
+
"status": 502,
|
|
179
|
+
"headers": [(b"content-type", b"text/plain")],
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
await send(
|
|
183
|
+
{
|
|
184
|
+
"type": "http.response.body",
|
|
185
|
+
"body": b"Bad Gateway: Could not reach React Router server",
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
async def close(self):
|
|
190
|
+
"""Close the HTTP client."""
|
|
191
|
+
if self._client is not None:
|
|
192
|
+
await self._client.aclose()
|
pulse/user_session.py
CHANGED
|
@@ -189,8 +189,8 @@ class CookieSessionStore:
|
|
|
189
189
|
if not secret:
|
|
190
190
|
secret = env.pulse_secret or ""
|
|
191
191
|
if not secret:
|
|
192
|
-
|
|
193
|
-
if
|
|
192
|
+
pulse_env = env.pulse_env
|
|
193
|
+
if pulse_env == "prod":
|
|
194
194
|
# In CI/production, require an explicit secret
|
|
195
195
|
raise RuntimeError(
|
|
196
196
|
"PULSE_SECRET must be set when using CookieSessionStore in production.\nCookieSessionStore is the default way of storing sessions in Pulse. Providing a secret is necessary to not invalidate all sessions on reload."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pulse-framework
|
|
3
|
-
Version: 0.1.
|
|
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
|
|
@@ -12,6 +12,7 @@ Requires-Dist: rich>=13.7.1
|
|
|
12
12
|
Requires-Dist: python-multipart>=0.0.20
|
|
13
13
|
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
14
14
|
Requires-Dist: watchfiles>=1.1.0
|
|
15
|
+
Requires-Dist: httpx>=0.28.1
|
|
15
16
|
Requires-Python: >=3.11
|
|
16
17
|
Description-Content-Type: text/markdown
|
|
17
18
|
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
pulse/__init__.py,sha256=
|
|
2
|
-
pulse/app.py,sha256=
|
|
1
|
+
pulse/__init__.py,sha256=aJg4LvIeLYalXvEe1u47_A9ktS3HGIf4ZeAo6w1tbMQ,31463
|
|
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=
|
|
6
|
-
pulse/cli/
|
|
5
|
+
pulse/cli/cmd.py,sha256=_5vZL-7qSHA20CPocS5MH5KhG1giSn1t11T4kdYmzCY,11951
|
|
6
|
+
pulse/cli/dependencies.py,sha256=ZBqBAfMvMBQUvh4THdPDztTMQ_dyR52S1IuotP_eEZs,5623
|
|
7
|
+
pulse/cli/folder_lock.py,sha256=kvUmZBg869lwCTIZFoge9dhorv8qPXHTWwVv_jQg1k8,3477
|
|
8
|
+
pulse/cli/helpers.py,sha256=8bRlV3d7w3w-jHaFvFYt9Pzue6_CbKOq_Z3jBsBOeUk,8820
|
|
9
|
+
pulse/cli/models.py,sha256=hRmIWmhXmGf2otzVm1do4Dm19rkWkmTwAA3Am3kw2tE,692
|
|
7
10
|
pulse/cli/packages.py,sha256=e7ycwwJfdmB4pzrai4DHos6-JzyUgmE4DCZp0BqjdeI,6792
|
|
11
|
+
pulse/cli/processes.py,sha256=rv1FZ0aynNsYwAbyirqOOeTp-6gqfks8CETOAVPGwS0,5599
|
|
12
|
+
pulse/cli/secrets.py,sha256=dNfQe6AzSYhZuWveesjCRHIbvaPd3-F9lEJ-kZA7ROw,921
|
|
13
|
+
pulse/cli/uvicorn_log_config.py,sha256=Ip0iCeMUoY1ruv3Amf2SF84lW2DDpJFqdsLflZNxmeY,2407
|
|
8
14
|
pulse/codegen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
pulse/codegen/codegen.py,sha256=
|
|
15
|
+
pulse/codegen/codegen.py,sha256=RMc2NkldX0dmxRG59gdulCMEzywvHMo2akeiHQMTu4I,10681
|
|
10
16
|
pulse/codegen/imports.py,sha256=13f0uzJsotw069aP_COUUPMuTXXhRKRwUzfxsSCq_6A,6070
|
|
11
17
|
pulse/codegen/js.py,sha256=7MuiECSJ-DulSqKuMZ8z1q_d7e3AbK6MYiNTYALZCLA,881
|
|
12
18
|
pulse/codegen/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
pulse/codegen/templates/layout.py,sha256=
|
|
19
|
+
pulse/codegen/templates/layout.py,sha256=zvNAOo0rt0p6pduxZJ-nSZlzOYNySTV2KHkfUyU84RM,3676
|
|
14
20
|
pulse/codegen/templates/route.py,sha256=cwmNHYkuecZ5M986hmm6SxisIoVSc656dtpqAvPjMjM,7824
|
|
15
21
|
pulse/codegen/templates/routes_ts.py,sha256=nPgKCvU0gzue2k6KlOL1TJgrBqqRLmyy7K_qKAI8zAE,1129
|
|
16
22
|
pulse/codegen/utils.py,sha256=QoXcV-h-DLLmq_t03hDNUePS0fNnofUQLoR-TXzDFCY,539
|
|
@@ -19,12 +25,12 @@ pulse/components/for_.py,sha256=LUyJEUlDM6b9oPjvUFgSsddxu6b6usF4BQdXe8FIiGI,1302
|
|
|
19
25
|
pulse/components/if_.py,sha256=rQywsmdirNpkb-61ZEdF-tgzUh-37JWd4YFGblkzIdQ,1624
|
|
20
26
|
pulse/components/react_router.py,sha256=TbRec-NVliUqrvAMeFXCrnDWV1rh6TGTPfRhqLuLubk,1129
|
|
21
27
|
pulse/context.py,sha256=x_nCbCEUGygAdCZiTfko5uuYxVSAeCNhYa59zBq015M,1692
|
|
22
|
-
pulse/cookies.py,sha256=
|
|
28
|
+
pulse/cookies.py,sha256=c7ua1Lv6mNe1nYnA4SFVvewvRQAbYy9fN5G3Hr_Dr5c,5000
|
|
23
29
|
pulse/css.py,sha256=-FyQQQ0EZI1Ins30qiF3l4z9yDb1V9qWuJKWxHcKGkw,3910
|
|
24
30
|
pulse/decorators.py,sha256=8At1HQTFs9KG7nd83miGMe3KkhTBVGDviaqZaY62bHI,6651
|
|
25
|
-
pulse/env.py,sha256=
|
|
31
|
+
pulse/env.py,sha256=BMEsIzR1_4c5bZzou7kYtSMk50gLnZ0Yf6l8U6Ve4IE,2575
|
|
26
32
|
pulse/form.py,sha256=M87QwG4KFOrI8Nba7BTDoJ_wZ1-jzJW7QN4JweYCpuM,9004
|
|
27
|
-
pulse/helpers.py,sha256=
|
|
33
|
+
pulse/helpers.py,sha256=q54JGen1lBIEGyLnNKvmW_7nnTFqFZBDMYXsoXvth7o,11628
|
|
28
34
|
pulse/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
35
|
pulse/hooks/core.py,sha256=Ksb-vSYbigL2GnUooxdLVUTfnT7cHOlYEKgKtrihvJo,7344
|
|
30
36
|
pulse/hooks/effects.py,sha256=CQvt5viAweGLSxaGGlWm155GlEQiwQnGussw7OfiCGc,2393
|
|
@@ -42,6 +48,7 @@ pulse/html/tags.pyi,sha256=I8dFoft9w4RvneZ3li1weAdijY1krj9jfO_p2SU6e04,13953
|
|
|
42
48
|
pulse/messages.py,sha256=SKfNHYCDkoRa5X0CPlBQbsFfqIVMZQ7z34vWxQY-bUs,3206
|
|
43
49
|
pulse/middleware.py,sha256=26ETGBLrIj8m0DOOUkafhLueQDzUurgjcqtI1L-6DNs,6518
|
|
44
50
|
pulse/plugin.py,sha256=rYk1tR4SSoQQM8ZD2rWC-AnMfcN7xOscW92RbyK6LKA,594
|
|
51
|
+
pulse/proxy.py,sha256=jQ9XRiRYHSeHA6GOVuYhXgLWU7HoKNtobQWn_bt07uQ,4984
|
|
45
52
|
pulse/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
53
|
pulse/query.py,sha256=u0KVFNt0d36sfmoxpQXvJ8DjctIHZaM_szlEonRiyRg,12849
|
|
47
54
|
pulse/react_component.py,sha256=Rw1J6cHOX8-K3BnkswVOu2COgneVvRz1OYmyXkX17RM,25993
|
|
@@ -55,10 +62,10 @@ pulse/serializer.py,sha256=8RAITNoSNm5-U38elHpWmkBpcM_rxZFMCluJSfldfk4,5420
|
|
|
55
62
|
pulse/state.py,sha256=nbnN9wGrHTP0AOFBfb4zKNzYaSkmLR5u0t4TVHg-UDA,9972
|
|
56
63
|
pulse/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
57
64
|
pulse/types/event_handler.py,sha256=OF7sOgYBb6iUs59RH1vQIH7aOrGPfs3nAaF7how-4PQ,1658
|
|
58
|
-
pulse/user_session.py,sha256=
|
|
65
|
+
pulse/user_session.py,sha256=kCZtQpYZe2keDXzusd6jsjjw075am0dXrb25jKLg5JU,7578
|
|
59
66
|
pulse/vdom.py,sha256=KTNBh2dVvDy9eXRzhneBJgk7F35MyWec8R_puQ4tSRY,12420
|
|
60
67
|
pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
|
|
61
|
-
pulse_framework-0.1.
|
|
62
|
-
pulse_framework-0.1.
|
|
63
|
-
pulse_framework-0.1.
|
|
64
|
-
pulse_framework-0.1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|