pulse-framework 0.1.71__tar.gz → 0.1.73__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.
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/PKG-INFO +4 -4
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/pyproject.toml +4 -4
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/__init__.py +19 -4
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/app.py +27 -24
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/cli/cmd.py +1 -1
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/cli/folder_lock.py +25 -6
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/cli/processes.py +2 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/codegen/templates/layout.py +3 -1
- pulse_framework-0.1.73/src/pulse/debounce.py +79 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/decorators.py +4 -3
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/hooks/effects.py +20 -6
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/hooks/runtime.py +25 -8
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/hooks/setup.py +6 -10
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/hooks/stable.py +5 -9
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/hooks/state.py +4 -8
- pulse_framework-0.1.73/src/pulse/proxy.py +783 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/queries/common.py +17 -5
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/queries/infinite_query.py +14 -3
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/queries/mutation.py +2 -1
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/queries/query.py +4 -2
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/render_session.py +7 -4
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/renderer.py +30 -2
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/routing.py +19 -5
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/serializer.py +38 -19
- pulse_framework-0.1.73/src/pulse/state/__init__.py +1 -0
- pulse_framework-0.1.73/src/pulse/state/property.py +218 -0
- pulse_framework-0.1.73/src/pulse/state/query_param.py +538 -0
- {pulse_framework-0.1.71/src/pulse → pulse_framework-0.1.73/src/pulse/state}/state.py +66 -220
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/nodes.py +26 -2
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/transpiler.py +86 -5
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/vdom.py +1 -1
- pulse_framework-0.1.71/src/pulse/proxy.py +0 -249
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/README.md +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/_examples.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/channel.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/cli/__init__.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/cli/dependencies.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/cli/helpers.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/cli/logging.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/cli/models.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/cli/packages.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/cli/secrets.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/cli/uvicorn_log_config.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/code_analysis.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/codegen/__init__.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/codegen/codegen.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/codegen/templates/__init__.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/codegen/templates/route.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/codegen/templates/routes_ts.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/codegen/utils.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/component.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/components/__init__.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/components/for_.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/components/if_.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/components/react_router.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/context.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/cookies.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/dom/__init__.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/dom/elements.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/dom/events.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/dom/props.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/dom/svg.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/dom/tags.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/dom/tags.pyi +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/env.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/forms.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/helpers.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/hooks/__init__.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/hooks/core.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/hooks/init.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/__init__.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/__init__.pyi +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/_types.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/array.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/console.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/date.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/document.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/error.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/json.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/map.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/math.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/navigator.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/number.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/obj.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/object.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/promise.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/pulse.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/react.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/regexp.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/set.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/string.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/weakmap.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/weakset.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/js/window.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/messages.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/middleware.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/plugin.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/py.typed +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/queries/__init__.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/queries/client.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/queries/effect.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/queries/protocol.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/queries/store.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/react_component.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/reactive.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/reactive_extensions.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/request.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/requirements.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/scheduling.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/test_helpers.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/__init__.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/assets.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/builtins.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/dynamic_import.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/emit_context.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/errors.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/function.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/id.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/imports.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/js_module.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/modules/__init__.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/modules/asyncio.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/modules/json.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/modules/math.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/modules/pulse/__init__.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/modules/pulse/tags.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/modules/typing.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/transpiler/py_module.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/types/__init__.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/types/event_handler.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/user_session.py +0 -0
- {pulse_framework-0.1.71 → pulse_framework-0.1.73}/src/pulse/version.py +0 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pulse-framework
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.73
|
|
4
4
|
Summary: Pulse - Full-stack framework for building real-time React applications in Python
|
|
5
|
-
Requires-Dist: websockets>=12.0
|
|
6
5
|
Requires-Dist: fastapi>=0.128.0
|
|
7
6
|
Requires-Dist: uvicorn>=0.24.0
|
|
8
7
|
Requires-Dist: mako>=1.3.10
|
|
9
8
|
Requires-Dist: typer>=0.16.0
|
|
10
9
|
Requires-Dist: python-socketio>=5.16.0
|
|
11
10
|
Requires-Dist: rich>=13.7.1
|
|
12
|
-
Requires-Dist: python-multipart>=0.0.
|
|
11
|
+
Requires-Dist: python-multipart>=0.0.22
|
|
13
12
|
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
14
13
|
Requires-Dist: starlette>=0.50.0,<0.51.0
|
|
15
14
|
Requires-Dist: urllib3>=2.6.3
|
|
16
15
|
Requires-Dist: watchfiles>=1.1.0
|
|
17
16
|
Requires-Dist: httpx>=0.28.1
|
|
18
|
-
Requires-
|
|
17
|
+
Requires-Dist: aiohttp>=3.12.0
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
|
|
21
21
|
# Pulse Python
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pulse-framework"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.73"
|
|
4
4
|
description = "Pulse - Full-stack framework for building real-time React applications in Python"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"websockets>=12.0",
|
|
9
8
|
"fastapi>=0.128.0",
|
|
10
9
|
"uvicorn>=0.24.0",
|
|
11
10
|
"mako>=1.3.10",
|
|
12
11
|
"typer>=0.16.0",
|
|
13
12
|
"python-socketio>=5.16.0",
|
|
14
13
|
"rich>=13.7.1",
|
|
15
|
-
"python-multipart>=0.0.
|
|
14
|
+
"python-multipart>=0.0.22",
|
|
16
15
|
"python-dateutil>=2.9.0.post0",
|
|
17
16
|
"starlette>=0.50.0,<0.51.0",
|
|
18
17
|
"urllib3>=2.6.3",
|
|
19
18
|
"watchfiles>=1.1.0",
|
|
20
19
|
"httpx>=0.28.1",
|
|
20
|
+
"aiohttp>=3.12.0",
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
[tool.uv]
|
|
@@ -60,6 +60,14 @@ from pulse.context import PulseContext as PulseContext
|
|
|
60
60
|
from pulse.cookies import Cookie as Cookie
|
|
61
61
|
from pulse.cookies import SetCookie as SetCookie
|
|
62
62
|
|
|
63
|
+
# Debounce
|
|
64
|
+
from pulse.debounce import (
|
|
65
|
+
Debounced as Debounced,
|
|
66
|
+
)
|
|
67
|
+
from pulse.debounce import (
|
|
68
|
+
debounced as debounced,
|
|
69
|
+
)
|
|
70
|
+
|
|
63
71
|
# Decorators
|
|
64
72
|
from pulse.decorators import computed as computed
|
|
65
73
|
from pulse.decorators import effect as effect
|
|
@@ -1200,7 +1208,7 @@ from pulse.hooks.core import (
|
|
|
1200
1208
|
)
|
|
1201
1209
|
|
|
1202
1210
|
# Hooks - Effects (import to register inline_effect_hook before registry locks)
|
|
1203
|
-
from pulse.hooks.effects import
|
|
1211
|
+
from pulse.hooks.effects import EffectState as EffectState
|
|
1204
1212
|
|
|
1205
1213
|
# Hooks - Init
|
|
1206
1214
|
from pulse.hooks.init import (
|
|
@@ -1235,6 +1243,9 @@ from pulse.hooks.runtime import (
|
|
|
1235
1243
|
from pulse.hooks.runtime import (
|
|
1236
1244
|
not_found as not_found,
|
|
1237
1245
|
)
|
|
1246
|
+
from pulse.hooks.runtime import (
|
|
1247
|
+
pulse_route as pulse_route,
|
|
1248
|
+
)
|
|
1238
1249
|
from pulse.hooks.runtime import (
|
|
1239
1250
|
redirect as redirect,
|
|
1240
1251
|
)
|
|
@@ -1259,7 +1270,7 @@ from pulse.hooks.runtime import (
|
|
|
1259
1270
|
|
|
1260
1271
|
# Hooks - Setup
|
|
1261
1272
|
from pulse.hooks.setup import (
|
|
1262
|
-
|
|
1273
|
+
SetupState as SetupState,
|
|
1263
1274
|
)
|
|
1264
1275
|
from pulse.hooks.setup import (
|
|
1265
1276
|
setup as setup,
|
|
@@ -1271,7 +1282,7 @@ from pulse.hooks.stable import (
|
|
|
1271
1282
|
StableEntry as StableEntry,
|
|
1272
1283
|
)
|
|
1273
1284
|
from pulse.hooks.stable import (
|
|
1274
|
-
|
|
1285
|
+
StableState as StableState,
|
|
1275
1286
|
)
|
|
1276
1287
|
|
|
1277
1288
|
# Hooks - Stable
|
|
@@ -1322,6 +1333,9 @@ from pulse.middleware import (
|
|
|
1322
1333
|
|
|
1323
1334
|
# Plugin
|
|
1324
1335
|
from pulse.plugin import Plugin as Plugin
|
|
1336
|
+
|
|
1337
|
+
# Proxy
|
|
1338
|
+
from pulse.proxy import ProxyConfig as ProxyConfig
|
|
1325
1339
|
from pulse.queries.client import QueryClient as QueryClient
|
|
1326
1340
|
from pulse.queries.client import QueryFilter as QueryFilter
|
|
1327
1341
|
from pulse.queries.client import queries as queries
|
|
@@ -1430,7 +1444,8 @@ from pulse.serializer import deserialize as deserialize
|
|
|
1430
1444
|
from pulse.serializer import serialize as serialize
|
|
1431
1445
|
|
|
1432
1446
|
# State and routing
|
|
1433
|
-
from pulse.state import
|
|
1447
|
+
from pulse.state.query_param import QueryParam as QueryParam
|
|
1448
|
+
from pulse.state.state import State as State
|
|
1434
1449
|
|
|
1435
1450
|
# Transpiler v2
|
|
1436
1451
|
from pulse.transpiler.function import JsFunction as JsFunction
|
|
@@ -67,7 +67,7 @@ from pulse.middleware import (
|
|
|
67
67
|
Redirect,
|
|
68
68
|
)
|
|
69
69
|
from pulse.plugin import Plugin
|
|
70
|
-
from pulse.proxy import ReactProxy
|
|
70
|
+
from pulse.proxy import ProxyConfig, ReactProxy
|
|
71
71
|
from pulse.render_session import RenderSession
|
|
72
72
|
from pulse.request import PulseRequest
|
|
73
73
|
from pulse.routing import Layout, Route, RouteTree, ensure_absolute_path
|
|
@@ -211,6 +211,7 @@ class App:
|
|
|
211
211
|
_tasks: TaskRegistry
|
|
212
212
|
_timers: TimerRegistry
|
|
213
213
|
_proxy: ReactProxy | None
|
|
214
|
+
proxy_config: ProxyConfig | None
|
|
214
215
|
session_timeout: float
|
|
215
216
|
connection_status: ConnectionStatusConfig
|
|
216
217
|
render_loop_limit: int
|
|
@@ -232,6 +233,7 @@ class App:
|
|
|
232
233
|
not_found: str = "/not-found",
|
|
233
234
|
# Deployment and integration options
|
|
234
235
|
mode: PulseMode = "single-server",
|
|
236
|
+
proxy: ProxyConfig | None = None,
|
|
235
237
|
api_prefix: str = "/_pulse",
|
|
236
238
|
cors: CORSOptions | None = None,
|
|
237
239
|
fastapi: dict[str, Any] | None = None,
|
|
@@ -245,6 +247,7 @@ class App:
|
|
|
245
247
|
# Resolve mode from environment and expose on the app instance
|
|
246
248
|
self.env = envvars.pulse_env
|
|
247
249
|
self.mode = mode
|
|
250
|
+
self.proxy_config = proxy
|
|
248
251
|
self.status = AppStatus.created
|
|
249
252
|
# Persist the server address for use by sessions (API calls, etc.) in ci/prod.
|
|
250
253
|
self.server_address = server_address if self.env in ("ci", "prod") else None
|
|
@@ -506,15 +509,22 @@ class App:
|
|
|
506
509
|
)
|
|
507
510
|
render_id = request.headers.get("x-pulse-render-id")
|
|
508
511
|
render = self._get_render_for_session(render_id, session)
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
512
|
+
try:
|
|
513
|
+
with PulseContext.update(session=session, render=render):
|
|
514
|
+
res: Response = await call_next(request)
|
|
515
|
+
session.handle_response(res)
|
|
516
|
+
return res
|
|
517
|
+
except RuntimeError as exc:
|
|
518
|
+
# Client disconnected before response was sent. This happens when
|
|
519
|
+
# ASGI handlers (like the proxy) return early on disconnect without
|
|
520
|
+
# sending a response, which is valid ASGI but breaks BaseHTTPMiddleware.
|
|
521
|
+
if "No response returned" in str(exc):
|
|
522
|
+
return Response(status_code=499)
|
|
523
|
+
raise
|
|
524
|
+
finally:
|
|
525
|
+
self._sessions_in_request[session.sid] -= 1
|
|
526
|
+
if self._sessions_in_request[session.sid] == 0:
|
|
527
|
+
del self._sessions_in_request[session.sid]
|
|
518
528
|
|
|
519
529
|
# Apply prefix to all routes
|
|
520
530
|
prefix = self.api_prefix
|
|
@@ -654,10 +664,8 @@ class App:
|
|
|
654
664
|
for plugin in self.plugins:
|
|
655
665
|
plugin.on_setup(self)
|
|
656
666
|
|
|
657
|
-
# In single-server mode, add catch-all route to proxy unmatched requests to React server
|
|
658
|
-
# This route must be registered last so FastAPI tries all specific routes first
|
|
659
|
-
# FastAPI will match specific routes before this catch-all, but we add an explicit check
|
|
660
|
-
# as a safety measure to ensure API routes are never proxied
|
|
667
|
+
# In single-server mode, add catch-all route to proxy unmatched requests to React server.
|
|
668
|
+
# This route must be registered last so FastAPI tries all specific routes first.
|
|
661
669
|
if self.mode == "single-server":
|
|
662
670
|
react_server_address = envvars.react_server_address
|
|
663
671
|
if not react_server_address:
|
|
@@ -666,11 +674,12 @@ class App:
|
|
|
666
674
|
+ "Use 'pulse run' CLI command or set the environment variable."
|
|
667
675
|
)
|
|
668
676
|
|
|
669
|
-
|
|
677
|
+
proxy_handler = ReactProxy(
|
|
670
678
|
react_server_address=react_server_address,
|
|
671
679
|
server_address=server_address,
|
|
680
|
+
config=self.proxy_config,
|
|
672
681
|
)
|
|
673
|
-
|
|
682
|
+
self._proxy = proxy_handler
|
|
674
683
|
|
|
675
684
|
# In dev mode, proxy WebSocket connections to React Router (e.g. Vite HMR)
|
|
676
685
|
# Socket.IO handles /socket.io/ at ASGI level before reaching FastAPI
|
|
@@ -680,14 +689,8 @@ class App:
|
|
|
680
689
|
async def websocket_proxy(websocket: WebSocket, path: str): # pyright: ignore[reportUnusedFunction]
|
|
681
690
|
await proxy_handler.proxy_websocket(websocket)
|
|
682
691
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
methods=["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
|
|
686
|
-
include_in_schema=False,
|
|
687
|
-
)
|
|
688
|
-
async def proxy_catch_all(request: Request, path: str): # pyright: ignore[reportUnusedFunction]
|
|
689
|
-
# Proxy all unmatched HTTP requests to React Router
|
|
690
|
-
return await proxy_handler(request)
|
|
692
|
+
# Register ASGI-level catch-all last.
|
|
693
|
+
self.fastapi.mount("/", proxy_handler, name="react-proxy")
|
|
691
694
|
|
|
692
695
|
@self.sio.event
|
|
693
696
|
async def connect( # pyright: ignore[reportUnusedFunction]
|
|
@@ -52,7 +52,7 @@ def _write_gitignore_for_lock(lock_path: Path) -> None:
|
|
|
52
52
|
ensure_gitignore_has(lock_path.parent, lock_path.name)
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def _create_lock_file(lock_path: Path) -> None:
|
|
55
|
+
def _create_lock_file(lock_path: Path, *, address: str, port: int) -> None:
|
|
56
56
|
"""Create a lock file with current process information."""
|
|
57
57
|
lock_path = Path(lock_path)
|
|
58
58
|
_write_gitignore_for_lock(lock_path)
|
|
@@ -61,18 +61,26 @@ def _create_lock_file(lock_path: Path) -> None:
|
|
|
61
61
|
info = _read_lock(lock_path) or {}
|
|
62
62
|
pid = int(info.get("pid", 0) or 0)
|
|
63
63
|
if pid and is_process_alive(pid):
|
|
64
|
+
existing_addr = info.get("address", address)
|
|
65
|
+
existing_port = info.get("port", port)
|
|
66
|
+
protocol = (
|
|
67
|
+
"http" if existing_addr in ("127.0.0.1", "localhost") else "https"
|
|
68
|
+
)
|
|
69
|
+
url = f"{protocol}://{existing_addr}:{existing_port}"
|
|
64
70
|
raise RuntimeError(
|
|
65
|
-
f"Another Pulse dev instance
|
|
71
|
+
f"Another Pulse dev instance is running at {url} (pid={pid})"
|
|
66
72
|
)
|
|
67
73
|
# Stale lock; continue to overwrite
|
|
68
74
|
|
|
69
|
-
payload = {
|
|
75
|
+
payload: dict[str, Any] = {
|
|
70
76
|
"pid": os.getpid(),
|
|
71
77
|
"created_at": int(time.time()),
|
|
72
78
|
"hostname": socket.gethostname(),
|
|
73
79
|
"platform": platform.platform(),
|
|
74
80
|
"python": platform.python_version(),
|
|
75
81
|
"cwd": os.getcwd(),
|
|
82
|
+
"address": address,
|
|
83
|
+
"port": port,
|
|
76
84
|
}
|
|
77
85
|
try:
|
|
78
86
|
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -105,23 +113,34 @@ class FolderLock:
|
|
|
105
113
|
and know not to delete the lock on exit.
|
|
106
114
|
|
|
107
115
|
Example:
|
|
108
|
-
with FolderLock(web_root):
|
|
116
|
+
with FolderLock(web_root, address="localhost", port=8000):
|
|
109
117
|
# Protected region
|
|
110
118
|
pass
|
|
111
119
|
"""
|
|
112
120
|
|
|
113
|
-
def __init__(
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
web_root: Path,
|
|
124
|
+
*,
|
|
125
|
+
address: str,
|
|
126
|
+
port: int,
|
|
127
|
+
filename: str = ".pulse/lock",
|
|
128
|
+
):
|
|
114
129
|
"""
|
|
115
130
|
Initialize FolderLock.
|
|
116
131
|
|
|
117
132
|
Args:
|
|
118
133
|
web_root: Path to the web root directory
|
|
134
|
+
address: Server address to store in lock file
|
|
135
|
+
port: Server port to store in lock file
|
|
119
136
|
filename: Name of the lock file (default: ".pulse/lock")
|
|
120
137
|
"""
|
|
121
138
|
self.lock_path: Path = lock_path_for_web_root(web_root, filename)
|
|
139
|
+
self.address: str = address
|
|
140
|
+
self.port: int = port
|
|
122
141
|
|
|
123
142
|
def __enter__(self):
|
|
124
|
-
_create_lock_file(self.lock_path)
|
|
143
|
+
_create_lock_file(self.lock_path, address=self.address, port=self.port)
|
|
125
144
|
return self
|
|
126
145
|
|
|
127
146
|
def __exit__(
|
|
@@ -272,6 +272,8 @@ def _write_tagged_line(name: str, message: str, tag_mode: TagMode) -> None:
|
|
|
272
272
|
"Network: use --host to expose" in clean_message
|
|
273
273
|
or "press h + enter to show help" in clean_message
|
|
274
274
|
or "➜ Local:" in clean_message
|
|
275
|
+
or "/__manifest" in clean_message
|
|
276
|
+
or "?import" in clean_message
|
|
275
277
|
):
|
|
276
278
|
return
|
|
277
279
|
|
|
@@ -32,7 +32,9 @@ export async function loader(args: LoaderFunctionArgs) {
|
|
|
32
32
|
if (cookie) fwd.set("cookie", cookie);
|
|
33
33
|
if (authorization) fwd.set("authorization", authorization);
|
|
34
34
|
fwd.set("content-type", "application/json");
|
|
35
|
-
|
|
35
|
+
// Internal server address for server-side loader requests.
|
|
36
|
+
const internalServerAddress = "${internal_server_address}";
|
|
37
|
+
const res = await fetch(`$${"{"}internalServerAddress}$${"{"}config.apiPrefix}/prerender`, {
|
|
36
38
|
method: "POST",
|
|
37
39
|
headers: fwd,
|
|
38
40
|
body: JSON.stringify({ paths, routeInfo: extractServerRouteInfo(args) }),
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import math
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Generic, ParamSpec, TypeVar
|
|
8
|
+
|
|
9
|
+
from pulse.context import PULSE_CONTEXT
|
|
10
|
+
from pulse.scheduling import TimerHandleLike, later
|
|
11
|
+
|
|
12
|
+
P = ParamSpec("P")
|
|
13
|
+
R = TypeVar("R")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class Debounced(Generic[P, R]):
|
|
18
|
+
fn: Callable[P, R]
|
|
19
|
+
delay_ms: float
|
|
20
|
+
_handle: TimerHandleLike | asyncio.Handle | None = field(
|
|
21
|
+
default=None, init=False, repr=False, compare=False
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Any:
|
|
25
|
+
if self._handle is not None:
|
|
26
|
+
self._handle.cancel()
|
|
27
|
+
|
|
28
|
+
delay = self.delay_ms / 1000.0
|
|
29
|
+
|
|
30
|
+
def _run() -> None:
|
|
31
|
+
object.__setattr__(self, "_handle", None)
|
|
32
|
+
result = self.fn(*args, **kwargs)
|
|
33
|
+
if asyncio.iscoroutine(result):
|
|
34
|
+
loop = asyncio.get_running_loop()
|
|
35
|
+
task = loop.create_task(result)
|
|
36
|
+
|
|
37
|
+
def _log_task_exception(t: asyncio.Task[Any]) -> None:
|
|
38
|
+
try:
|
|
39
|
+
t.result()
|
|
40
|
+
except asyncio.CancelledError:
|
|
41
|
+
pass
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
loop.call_exception_handler(
|
|
44
|
+
{
|
|
45
|
+
"message": "Unhandled exception in debounced() task",
|
|
46
|
+
"exception": exc,
|
|
47
|
+
"context": {"callback": self.fn},
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
task.add_done_callback(_log_task_exception)
|
|
52
|
+
|
|
53
|
+
if PULSE_CONTEXT.get() is not None:
|
|
54
|
+
handle = later(delay, _run)
|
|
55
|
+
else:
|
|
56
|
+
try:
|
|
57
|
+
loop = asyncio.get_running_loop()
|
|
58
|
+
except RuntimeError:
|
|
59
|
+
try:
|
|
60
|
+
loop = asyncio.get_event_loop()
|
|
61
|
+
except RuntimeError as exc:
|
|
62
|
+
raise RuntimeError("debounced() requires an event loop") from exc
|
|
63
|
+
handle = loop.call_later(delay, _run)
|
|
64
|
+
|
|
65
|
+
object.__setattr__(self, "_handle", handle)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def debounced(fn: Callable[P, R], delay_ms: int | float) -> Debounced[P, R]:
|
|
69
|
+
"""Return a debounced callback marker (delay in milliseconds)."""
|
|
70
|
+
if not callable(fn):
|
|
71
|
+
raise TypeError("debounced() requires a callable")
|
|
72
|
+
if isinstance(delay_ms, bool) or not isinstance(delay_ms, (int, float)):
|
|
73
|
+
raise TypeError("debounced() delay must be a number (ms)")
|
|
74
|
+
if not math.isfinite(delay_ms) or delay_ms < 0:
|
|
75
|
+
raise ValueError("debounced() delay must be finite and >= 0")
|
|
76
|
+
return Debounced(fn=fn, delay_ms=float(delay_ms))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ["Debounced", "debounced"]
|
|
@@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable
|
|
|
5
5
|
from typing import Any, ParamSpec, Protocol, TypeVar, cast, overload
|
|
6
6
|
|
|
7
7
|
from pulse.hooks.core import HOOK_CONTEXT
|
|
8
|
-
from pulse.hooks.effects import
|
|
8
|
+
from pulse.hooks.effects import effect_state
|
|
9
9
|
from pulse.hooks.state import collect_component_identity
|
|
10
10
|
from pulse.reactive import (
|
|
11
11
|
AsyncEffect,
|
|
@@ -16,7 +16,8 @@ from pulse.reactive import (
|
|
|
16
16
|
EffectFn,
|
|
17
17
|
Signal,
|
|
18
18
|
)
|
|
19
|
-
from pulse.state import ComputedProperty,
|
|
19
|
+
from pulse.state.property import ComputedProperty, StateEffect
|
|
20
|
+
from pulse.state.state import State
|
|
20
21
|
|
|
21
22
|
T = TypeVar("T")
|
|
22
23
|
TState = TypeVar("TState", bound=State)
|
|
@@ -336,7 +337,7 @@ def effect(
|
|
|
336
337
|
else:
|
|
337
338
|
identity = key
|
|
338
339
|
|
|
339
|
-
state =
|
|
340
|
+
state = effect_state()
|
|
340
341
|
return state.get_or_create(cast(Any, identity), key, create_effect)
|
|
341
342
|
|
|
342
343
|
if fn is not None:
|
|
@@ -2,10 +2,10 @@ from collections.abc import Callable
|
|
|
2
2
|
from typing import Any, override
|
|
3
3
|
|
|
4
4
|
from pulse.hooks.core import HookMetadata, HookState, hooks
|
|
5
|
-
from pulse.reactive import AsyncEffect, Effect
|
|
5
|
+
from pulse.reactive import REACTIVE_CONTEXT, AsyncEffect, Effect
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
class
|
|
8
|
+
class EffectState(HookState):
|
|
9
9
|
"""Stores inline effects keyed by function identity or explicit key."""
|
|
10
10
|
|
|
11
11
|
__slots__ = ("effects", "_seen_this_render") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
@@ -33,6 +33,20 @@ class InlineEffectHookState(HookState):
|
|
|
33
33
|
if key not in self._seen_this_render:
|
|
34
34
|
self.effects[key].dispose()
|
|
35
35
|
del self.effects[key]
|
|
36
|
+
# Remove inline effects from the active render scope to avoid parent cleanup.
|
|
37
|
+
rc = REACTIVE_CONTEXT.get()
|
|
38
|
+
scope = rc.scope
|
|
39
|
+
if scope is None or not scope.effects:
|
|
40
|
+
return
|
|
41
|
+
for key in self._seen_this_render:
|
|
42
|
+
effect = self.effects.get(key)
|
|
43
|
+
if effect is None:
|
|
44
|
+
continue
|
|
45
|
+
try:
|
|
46
|
+
scope.effects.remove(effect)
|
|
47
|
+
effect.parent = None
|
|
48
|
+
except ValueError:
|
|
49
|
+
pass
|
|
36
50
|
|
|
37
51
|
def get_or_create(
|
|
38
52
|
self,
|
|
@@ -72,9 +86,9 @@ class InlineEffectHookState(HookState):
|
|
|
72
86
|
self._seen_this_render.clear()
|
|
73
87
|
|
|
74
88
|
|
|
75
|
-
|
|
89
|
+
effect_state = hooks.create(
|
|
76
90
|
"pulse:core.inline_effects",
|
|
77
|
-
|
|
91
|
+
factory=EffectState,
|
|
78
92
|
metadata=HookMetadata(
|
|
79
93
|
owner="pulse.core",
|
|
80
94
|
description="Storage for inline @ps.effect decorators in components",
|
|
@@ -83,6 +97,6 @@ inline_effect_hook = hooks.create(
|
|
|
83
97
|
|
|
84
98
|
|
|
85
99
|
__all__ = [
|
|
86
|
-
"
|
|
87
|
-
"
|
|
100
|
+
"EffectState",
|
|
101
|
+
"effect_state",
|
|
88
102
|
]
|
|
@@ -13,8 +13,8 @@ from typing import (
|
|
|
13
13
|
from pulse.context import PulseContext
|
|
14
14
|
from pulse.hooks.core import HOOK_CONTEXT
|
|
15
15
|
from pulse.reactive_extensions import ReactiveDict
|
|
16
|
-
from pulse.routing import
|
|
17
|
-
from pulse.state import State
|
|
16
|
+
from pulse.routing import Layout, Route, RouteInfo
|
|
17
|
+
from pulse.state.state import State
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class RedirectInterrupt(Exception):
|
|
@@ -47,11 +47,11 @@ class NotFoundInterrupt(Exception):
|
|
|
47
47
|
pass
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
def route() ->
|
|
51
|
-
"""Get the current route
|
|
50
|
+
def route() -> RouteInfo:
|
|
51
|
+
"""Get the current route info.
|
|
52
52
|
|
|
53
53
|
Returns:
|
|
54
|
-
|
|
54
|
+
RouteInfo: Mapping with access to route parameters, path, and query.
|
|
55
55
|
|
|
56
56
|
Raises:
|
|
57
57
|
RuntimeError: If called outside of a component render context.
|
|
@@ -61,8 +61,8 @@ def route() -> RouteContext:
|
|
|
61
61
|
```python
|
|
62
62
|
def user_page():
|
|
63
63
|
r = ps.route()
|
|
64
|
-
user_id = r.
|
|
65
|
-
page = r.
|
|
64
|
+
user_id = r["pathParams"].get("user_id") # From /users/:user_id
|
|
65
|
+
page = r["queryParams"].get("page", "1") # From ?page=2
|
|
66
66
|
return m.Text(f"User {user_id}, Page {page}")
|
|
67
67
|
```
|
|
68
68
|
"""
|
|
@@ -71,7 +71,24 @@ def route() -> RouteContext:
|
|
|
71
71
|
raise RuntimeError(
|
|
72
72
|
"`pulse.route` can only be called within a component during rendering."
|
|
73
73
|
)
|
|
74
|
-
return ctx.route
|
|
74
|
+
return ctx.route.info
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def pulse_route() -> Route | Layout:
|
|
78
|
+
"""Get the current route definition.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Route | Layout: The active route or layout definition.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
RuntimeError: If called outside of a component render context.
|
|
85
|
+
"""
|
|
86
|
+
ctx = PulseContext.get()
|
|
87
|
+
if not ctx or not ctx.route:
|
|
88
|
+
raise RuntimeError(
|
|
89
|
+
"`pulse.pulse_route` can only be called within a component during rendering."
|
|
90
|
+
)
|
|
91
|
+
return ctx.route.pulse_route
|
|
75
92
|
|
|
76
93
|
|
|
77
94
|
def session() -> ReactiveDict[str, Any]:
|
|
@@ -10,7 +10,7 @@ P = ParamSpec("P")
|
|
|
10
10
|
T = TypeVar("T")
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class
|
|
13
|
+
class SetupState(HookState):
|
|
14
14
|
"""Internal hook state for the setup hook.
|
|
15
15
|
|
|
16
16
|
Manages the initialization, argument tracking, and lifecycle of
|
|
@@ -140,13 +140,9 @@ class SetupHookState(HookState):
|
|
|
140
140
|
return key
|
|
141
141
|
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
return SetupHookState()
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
_setup_hook = hooks.create(
|
|
143
|
+
setup_state = hooks.create(
|
|
148
144
|
"pulse:core.setup",
|
|
149
|
-
|
|
145
|
+
factory=SetupState,
|
|
150
146
|
metadata=HookMetadata(
|
|
151
147
|
owner="pulse.core",
|
|
152
148
|
description="Internal storage for pulse.setup hook",
|
|
@@ -195,7 +191,7 @@ def setup(init_func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
|
|
|
195
191
|
- Use ``ps.setup()`` directly when AST rewriting is problematic
|
|
196
192
|
- Arguments must be consistent across renders (same count and names)
|
|
197
193
|
"""
|
|
198
|
-
state =
|
|
194
|
+
state = setup_state()
|
|
199
195
|
state.ensure_not_called()
|
|
200
196
|
|
|
201
197
|
key = state.consume_pending_key()
|
|
@@ -245,10 +241,10 @@ def setup_key(key: str) -> None:
|
|
|
245
241
|
"""
|
|
246
242
|
if not isinstance(key, str):
|
|
247
243
|
raise TypeError("setup_key() requires a string key")
|
|
248
|
-
state =
|
|
244
|
+
state = setup_state()
|
|
249
245
|
if state.called_this_render:
|
|
250
246
|
raise RuntimeError("setup_key() must be called before setup() in a render")
|
|
251
247
|
state.set_pending_key(key)
|
|
252
248
|
|
|
253
249
|
|
|
254
|
-
__all__ = ["setup", "setup_key", "
|
|
250
|
+
__all__ = ["setup", "setup_key", "SetupState"]
|
|
@@ -35,7 +35,7 @@ class StableEntry:
|
|
|
35
35
|
self.wrapper = wrapper
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
class
|
|
38
|
+
class StableState(HookState):
|
|
39
39
|
"""Internal hook state that stores stable entries by key.
|
|
40
40
|
|
|
41
41
|
Maintains a dictionary of StableEntry objects, allowing stable
|
|
@@ -50,13 +50,9 @@ class StableRegistry(HookState):
|
|
|
50
50
|
self.entries: dict[str, StableEntry] = {}
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
return StableRegistry()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
_stable_hook = hooks.create(
|
|
53
|
+
stable_state = hooks.create(
|
|
58
54
|
"pulse:core.stable",
|
|
59
|
-
|
|
55
|
+
factory=StableState,
|
|
60
56
|
metadata=HookMetadata(
|
|
61
57
|
owner="pulse.core",
|
|
62
58
|
description="Internal registry for pulse.stable values",
|
|
@@ -119,7 +115,7 @@ def stable(key: str, value: Any = MISSING) -> Any:
|
|
|
119
115
|
if not key:
|
|
120
116
|
raise ValueError("stable() requires a non-empty string key")
|
|
121
117
|
|
|
122
|
-
registry =
|
|
118
|
+
registry = stable_state()
|
|
123
119
|
entry = registry.entries.get(key)
|
|
124
120
|
|
|
125
121
|
if value is not MISSING:
|
|
@@ -135,4 +131,4 @@ def stable(key: str, value: Any = MISSING) -> Any:
|
|
|
135
131
|
return entry.wrapper
|
|
136
132
|
|
|
137
133
|
|
|
138
|
-
__all__ = ["stable", "
|
|
134
|
+
__all__ = ["stable", "StableState", "StableEntry"]
|