pulse-framework 0.1.54__tar.gz → 0.1.56__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.54 → pulse_framework-0.1.56}/PKG-INFO +5 -5
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/README.md +4 -4
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/pyproject.toml +1 -1
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/__init__.py +5 -6
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/app.py +144 -57
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/channel.py +139 -7
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/cli/cmd.py +16 -2
- pulse_framework-0.1.56/src/pulse/code_analysis.py +38 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/codegen/codegen.py +61 -62
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/codegen/templates/route.py +100 -56
- pulse_framework-0.1.56/src/pulse/component.py +237 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/components/for_.py +30 -4
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/components/if_.py +28 -5
- pulse_framework-0.1.56/src/pulse/components/react_router.py +96 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/context.py +39 -5
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/cookies.py +108 -4
- pulse_framework-0.1.56/src/pulse/decorators.py +344 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/env.py +56 -2
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/form.py +198 -5
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/helpers.py +7 -1
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/hooks/core.py +135 -5
- pulse_framework-0.1.56/src/pulse/hooks/effects.py +88 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/hooks/init.py +60 -1
- pulse_framework-0.1.56/src/pulse/hooks/runtime.py +464 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/hooks/setup.py +77 -0
- pulse_framework-0.1.56/src/pulse/hooks/stable.py +138 -0
- pulse_framework-0.1.56/src/pulse/hooks/state.py +192 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/__init__.py +41 -25
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/array.py +9 -6
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/console.py +15 -12
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/date.py +9 -6
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/document.py +5 -2
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/error.py +7 -4
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/json.py +9 -6
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/map.py +8 -5
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/math.py +9 -6
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/navigator.py +5 -2
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/number.py +9 -6
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/obj.py +16 -13
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/object.py +9 -6
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/promise.py +19 -13
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/pulse.py +28 -25
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/react.py +190 -44
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/regexp.py +7 -4
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/set.py +8 -5
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/string.py +9 -6
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/weakmap.py +8 -5
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/weakset.py +8 -5
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/window.py +6 -3
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/messages.py +5 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/middleware.py +147 -76
- pulse_framework-0.1.56/src/pulse/plugin.py +96 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/queries/client.py +186 -39
- pulse_framework-0.1.56/src/pulse/queries/common.py +101 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/queries/infinite_query.py +154 -2
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/queries/mutation.py +127 -7
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/queries/query.py +112 -11
- pulse_framework-0.1.56/src/pulse/react_component.py +68 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/reactive.py +314 -30
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/reactive_extensions.py +106 -26
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/render_session.py +304 -173
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/request.py +46 -11
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/routing.py +140 -4
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/serializer.py +71 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/state.py +177 -9
- pulse_framework-0.1.56/src/pulse/test_helpers.py +15 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/__init__.py +13 -3
- pulse_framework-0.1.56/src/pulse/transpiler/assets.py +66 -0
- pulse_framework-0.1.56/src/pulse/transpiler/dynamic_import.py +131 -0
- pulse_framework-0.1.56/src/pulse/transpiler/emit_context.py +49 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/function.py +6 -2
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/imports.py +33 -27
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/js_module.py +64 -8
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/py_module.py +1 -7
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/transpiler.py +4 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/user_session.py +119 -18
- pulse_framework-0.1.54/src/pulse/component.py +0 -115
- pulse_framework-0.1.54/src/pulse/components/react_router.py +0 -38
- pulse_framework-0.1.54/src/pulse/decorators.py +0 -175
- pulse_framework-0.1.54/src/pulse/hooks/effects.py +0 -104
- pulse_framework-0.1.54/src/pulse/hooks/runtime.py +0 -223
- pulse_framework-0.1.54/src/pulse/hooks/stable.py +0 -81
- pulse_framework-0.1.54/src/pulse/hooks/state.py +0 -105
- pulse_framework-0.1.54/src/pulse/js/react_dom.py +0 -30
- pulse_framework-0.1.54/src/pulse/plugin.py +0 -25
- pulse_framework-0.1.54/src/pulse/queries/common.py +0 -52
- pulse_framework-0.1.54/src/pulse/react_component.py +0 -5
- pulse_framework-0.1.54/src/pulse/transpiler/react_component.py +0 -51
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/_examples.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/cli/__init__.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/cli/dependencies.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/cli/folder_lock.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/cli/helpers.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/cli/logging.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/cli/models.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/cli/packages.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/cli/processes.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/cli/secrets.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/cli/uvicorn_log_config.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/codegen/__init__.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/codegen/js.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/codegen/templates/__init__.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/codegen/templates/layout.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/codegen/templates/routes_ts.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/codegen/utils.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/components/__init__.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/dom/__init__.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/dom/elements.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/dom/events.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/dom/props.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/dom/svg.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/dom/tags.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/dom/tags.pyi +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/hooks/__init__.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/__init__.pyi +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/js/_types.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/proxy.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/py.typed +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/queries/__init__.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/queries/effect.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/queries/protocol.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/queries/store.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/renderer.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/builtins.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/errors.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/id.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/modules/__init__.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/modules/asyncio.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/modules/json.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/modules/math.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/modules/pulse/__init__.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/modules/pulse/tags.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/modules/typing.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/nodes.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/transpiler/vdom.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/types/__init__.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/src/pulse/types/event_handler.py +0 -0
- {pulse_framework-0.1.54 → pulse_framework-0.1.56}/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.
|
|
3
|
+
Version: 0.1.56
|
|
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
|
|
@@ -154,10 +154,10 @@ def counter():
|
|
|
154
154
|
|
|
155
155
|
### Hooks
|
|
156
156
|
|
|
157
|
-
Server-side hooks via `ps.
|
|
158
|
-
- `
|
|
159
|
-
-
|
|
160
|
-
- `setup
|
|
157
|
+
Server-side hooks via `ps.state`, `ps.effect`, `ps.setup`:
|
|
158
|
+
- `ps.state(StateClass)` - reactive state (auto-keyed by callsite; use `key=` for manual control)
|
|
159
|
+
- `@ps.effect` - side effects decorator
|
|
160
|
+
- `ps.setup(fn)` - one-time initialization
|
|
161
161
|
|
|
162
162
|
### Queries
|
|
163
163
|
|
|
@@ -136,10 +136,10 @@ def counter():
|
|
|
136
136
|
|
|
137
137
|
### Hooks
|
|
138
138
|
|
|
139
|
-
Server-side hooks via `ps.
|
|
140
|
-
- `
|
|
141
|
-
-
|
|
142
|
-
- `setup
|
|
139
|
+
Server-side hooks via `ps.state`, `ps.effect`, `ps.setup`:
|
|
140
|
+
- `ps.state(StateClass)` - reactive state (auto-keyed by callsite; use `key=` for manual control)
|
|
141
|
+
- `@ps.effect` - side effects decorator
|
|
142
|
+
- `ps.setup(fn)` - one-time initialization
|
|
143
143
|
|
|
144
144
|
### Queries
|
|
145
145
|
|
|
@@ -1205,9 +1205,8 @@ from pulse.hooks.core import (
|
|
|
1205
1205
|
hooks as hooks,
|
|
1206
1206
|
)
|
|
1207
1207
|
|
|
1208
|
-
# Hooks - Effects
|
|
1209
|
-
from pulse.hooks.effects import
|
|
1210
|
-
from pulse.hooks.effects import effects as effects
|
|
1208
|
+
# Hooks - Effects (import to register inline_effect_hook before registry locks)
|
|
1209
|
+
from pulse.hooks.effects import InlineEffectHookState as InlineEffectHookState
|
|
1211
1210
|
|
|
1212
1211
|
# Hooks - Init
|
|
1213
1212
|
from pulse.hooks.init import (
|
|
@@ -1323,9 +1322,6 @@ from pulse.middleware import (
|
|
|
1323
1322
|
from pulse.middleware import (
|
|
1324
1323
|
Redirect as Redirect,
|
|
1325
1324
|
)
|
|
1326
|
-
from pulse.middleware import (
|
|
1327
|
-
RoutePrerenderResponse as RoutePrerenderResponse,
|
|
1328
|
-
)
|
|
1329
1325
|
from pulse.middleware import (
|
|
1330
1326
|
stack as stack,
|
|
1331
1327
|
)
|
|
@@ -1344,6 +1340,9 @@ from pulse.queries.infinite_query import infinite_query as infinite_query
|
|
|
1344
1340
|
from pulse.queries.mutation import mutation as mutation
|
|
1345
1341
|
from pulse.queries.protocol import QueryResult as QueryResult
|
|
1346
1342
|
from pulse.queries.query import query as query
|
|
1343
|
+
from pulse.react_component import (
|
|
1344
|
+
ReactComponent as ReactComponent,
|
|
1345
|
+
)
|
|
1347
1346
|
|
|
1348
1347
|
# React components (v2)
|
|
1349
1348
|
from pulse.react_component import (
|
|
@@ -47,7 +47,6 @@ from pulse.helpers import (
|
|
|
47
47
|
later,
|
|
48
48
|
)
|
|
49
49
|
from pulse.hooks.core import hooks
|
|
50
|
-
from pulse.hooks.runtime import NotFoundInterrupt, RedirectInterrupt
|
|
51
50
|
from pulse.messages import (
|
|
52
51
|
ClientChannelMessage,
|
|
53
52
|
ClientChannelRequestMessage,
|
|
@@ -56,7 +55,9 @@ from pulse.messages import (
|
|
|
56
55
|
ClientPulseMessage,
|
|
57
56
|
Prerender,
|
|
58
57
|
PrerenderPayload,
|
|
58
|
+
ServerInitMessage,
|
|
59
59
|
ServerMessage,
|
|
60
|
+
ServerNavigateToMessage,
|
|
60
61
|
)
|
|
61
62
|
from pulse.middleware import (
|
|
62
63
|
ConnectResponse,
|
|
@@ -67,13 +68,12 @@ from pulse.middleware import (
|
|
|
67
68
|
PrerenderResponse,
|
|
68
69
|
PulseMiddleware,
|
|
69
70
|
Redirect,
|
|
70
|
-
RoutePrerenderResponse,
|
|
71
71
|
)
|
|
72
72
|
from pulse.plugin import Plugin
|
|
73
73
|
from pulse.proxy import ReactProxy
|
|
74
74
|
from pulse.render_session import RenderSession
|
|
75
75
|
from pulse.request import PulseRequest
|
|
76
|
-
from pulse.routing import Layout, Route, RouteTree
|
|
76
|
+
from pulse.routing import Layout, Route, RouteTree, ensure_absolute_path
|
|
77
77
|
from pulse.serializer import Serialized, deserialize, serialize
|
|
78
78
|
from pulse.user_session import (
|
|
79
79
|
CookieSessionStore,
|
|
@@ -88,6 +88,16 @@ T = TypeVar("T")
|
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
class AppStatus(IntEnum):
|
|
91
|
+
"""Application lifecycle status.
|
|
92
|
+
|
|
93
|
+
Attributes:
|
|
94
|
+
created: App instance created but not yet initialized.
|
|
95
|
+
initialized: App.setup() has been called, routes configured.
|
|
96
|
+
running: App is actively serving requests.
|
|
97
|
+
draining: App is shutting down, draining connections.
|
|
98
|
+
stopped: App has been fully stopped.
|
|
99
|
+
"""
|
|
100
|
+
|
|
91
101
|
created = 0
|
|
92
102
|
initialized = 1
|
|
93
103
|
running = 2
|
|
@@ -96,6 +106,12 @@ class AppStatus(IntEnum):
|
|
|
96
106
|
|
|
97
107
|
|
|
98
108
|
PulseMode = Literal["subdomains", "single-server"]
|
|
109
|
+
"""Deployment mode for the application.
|
|
110
|
+
|
|
111
|
+
Values:
|
|
112
|
+
"single-server": Python and React served from the same origin (default).
|
|
113
|
+
"subdomains": Python API on a subdomain (e.g., api.example.com).
|
|
114
|
+
"""
|
|
99
115
|
|
|
100
116
|
|
|
101
117
|
@dataclass
|
|
@@ -118,21 +134,55 @@ class ConnectionStatusConfig:
|
|
|
118
134
|
|
|
119
135
|
|
|
120
136
|
class App:
|
|
121
|
-
"""
|
|
122
|
-
Pulse UI Application - the main entry point for defining your app.
|
|
137
|
+
"""Main Pulse application class.
|
|
123
138
|
|
|
139
|
+
Creates a server that handles routing, sessions, and WebSocket connections.
|
|
124
140
|
Similar to FastAPI, users create an App instance and define their routes.
|
|
125
141
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
142
|
+
Args:
|
|
143
|
+
routes: Route definitions for the application.
|
|
144
|
+
codegen: Code generation settings for React Router output.
|
|
145
|
+
middleware: Request middleware, either a single middleware or sequence.
|
|
146
|
+
plugins: Application plugins that can contribute routes, middleware,
|
|
147
|
+
and lifecycle hooks.
|
|
148
|
+
cookie: Session cookie configuration.
|
|
149
|
+
session_store: Session storage backend. Defaults to CookieSessionStore.
|
|
150
|
+
server_address: Public server URL. Required in production.
|
|
151
|
+
dev_server_address: Development server URL. Defaults to
|
|
152
|
+
"http://localhost:8000".
|
|
153
|
+
internal_server_address: Internal URL for server-side loader fetches.
|
|
154
|
+
Falls back to server_address if not provided.
|
|
155
|
+
not_found: Path for 404 page. Defaults to "/not-found".
|
|
156
|
+
mode: Deployment mode - "single-server" (default) or "subdomains".
|
|
157
|
+
api_prefix: API route prefix. Defaults to "/_pulse".
|
|
158
|
+
cors: CORS configuration. Auto-configured based on mode if not provided.
|
|
159
|
+
fastapi: Additional FastAPI constructor options.
|
|
160
|
+
session_timeout: Session cleanup timeout in seconds. Defaults to 60.0.
|
|
161
|
+
connection_status: Connection status UI timing configuration.
|
|
129
162
|
|
|
130
|
-
|
|
163
|
+
Attributes:
|
|
164
|
+
env: Current environment ("dev", "ci", or "prod").
|
|
165
|
+
mode: Deployment mode ("single-server" or "subdomains").
|
|
166
|
+
status: Current application lifecycle status.
|
|
167
|
+
routes: Parsed route tree containing all registered routes.
|
|
168
|
+
fastapi: Underlying FastAPI instance.
|
|
169
|
+
asgi: ASGI application (includes Socket.IO).
|
|
131
170
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
171
|
+
Example:
|
|
172
|
+
```python
|
|
173
|
+
import pulse as ps
|
|
174
|
+
|
|
175
|
+
app = ps.App(
|
|
176
|
+
routes=[
|
|
177
|
+
ps.Route("/", render=home),
|
|
178
|
+
ps.Route("/users/:id", render=user_detail),
|
|
179
|
+
],
|
|
180
|
+
session_timeout=120.0,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
app.run(port=8000)
|
|
185
|
+
```
|
|
136
186
|
"""
|
|
137
187
|
|
|
138
188
|
env: PulseEnv
|
|
@@ -162,6 +212,9 @@ class App:
|
|
|
162
212
|
_render_cleanups: dict[str, asyncio.TimerHandle]
|
|
163
213
|
session_timeout: float
|
|
164
214
|
connection_status: ConnectionStatusConfig
|
|
215
|
+
render_loop_limit: int
|
|
216
|
+
detach_queue_timeout: float
|
|
217
|
+
disconnect_queue_timeout: float
|
|
165
218
|
|
|
166
219
|
def __init__(
|
|
167
220
|
self,
|
|
@@ -181,7 +234,10 @@ class App:
|
|
|
181
234
|
cors: CORSOptions | None = None,
|
|
182
235
|
fastapi: dict[str, Any] | None = None,
|
|
183
236
|
session_timeout: float = 60.0,
|
|
237
|
+
detach_queue_timeout: float = 15.0,
|
|
238
|
+
disconnect_queue_timeout: float = 300.0,
|
|
184
239
|
connection_status: ConnectionStatusConfig | None = None,
|
|
240
|
+
render_loop_limit: int = 50,
|
|
185
241
|
):
|
|
186
242
|
# Resolve mode from environment and expose on the app instance
|
|
187
243
|
self.env = envvars.pulse_env
|
|
@@ -228,7 +284,10 @@ class App:
|
|
|
228
284
|
# Map render_id -> cleanup timer handle for timeout-based expiry
|
|
229
285
|
self._render_cleanups = {}
|
|
230
286
|
self.session_timeout = session_timeout
|
|
287
|
+
self.detach_queue_timeout = detach_queue_timeout
|
|
288
|
+
self.disconnect_queue_timeout = disconnect_queue_timeout
|
|
231
289
|
self.connection_status = connection_status or ConnectionStatusConfig()
|
|
290
|
+
self.render_loop_limit = render_loop_limit
|
|
232
291
|
|
|
233
292
|
self.codegen = Codegen(
|
|
234
293
|
self.routes,
|
|
@@ -290,7 +349,21 @@ class App:
|
|
|
290
349
|
|
|
291
350
|
def run_codegen(
|
|
292
351
|
self, address: str | None = None, internal_address: str | None = None
|
|
293
|
-
):
|
|
352
|
+
) -> None:
|
|
353
|
+
"""Generate React Router code for all routes.
|
|
354
|
+
|
|
355
|
+
Generates TypeScript/JSX files for React Router integration based on
|
|
356
|
+
the application's route definitions.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
address: Public server address. Updates server_address if provided.
|
|
360
|
+
internal_address: Internal server address for SSR fetches. Updates
|
|
361
|
+
internal_server_address if provided.
|
|
362
|
+
|
|
363
|
+
Raises:
|
|
364
|
+
RuntimeError: If no server address is available (neither passed
|
|
365
|
+
as argument nor set on the App instance).
|
|
366
|
+
"""
|
|
294
367
|
# Allow the CLI to disable codegen in specific scenarios (e.g., prod server-only)
|
|
295
368
|
if envvars.codegen_disabled:
|
|
296
369
|
return
|
|
@@ -309,11 +382,18 @@ class App:
|
|
|
309
382
|
connection_status=self.connection_status,
|
|
310
383
|
)
|
|
311
384
|
|
|
312
|
-
def asgi_factory(self):
|
|
313
|
-
"""
|
|
314
|
-
|
|
315
|
-
|
|
385
|
+
def asgi_factory(self) -> ASGIApp:
|
|
386
|
+
"""ASGI factory for production deployment.
|
|
387
|
+
|
|
388
|
+
Called on each uvicorn reload. Initializes code generation and sets up
|
|
389
|
+
the application with the appropriate server address.
|
|
316
390
|
|
|
391
|
+
Returns:
|
|
392
|
+
The ASGI application instance (includes Socket.IO).
|
|
393
|
+
|
|
394
|
+
Raises:
|
|
395
|
+
RuntimeError: If in prod/ci mode without an explicit server_address.
|
|
396
|
+
"""
|
|
317
397
|
# In prod/ci, use the server_address provided to App(...).
|
|
318
398
|
if self.env in ("prod", "ci"):
|
|
319
399
|
if not self.server_address:
|
|
@@ -347,13 +427,34 @@ class App:
|
|
|
347
427
|
port: int = 8000,
|
|
348
428
|
find_port: bool = True,
|
|
349
429
|
reload: bool = True,
|
|
350
|
-
):
|
|
430
|
+
) -> None:
|
|
431
|
+
"""Start the development server with uvicorn.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
address: Host address to bind to. Defaults to "localhost".
|
|
435
|
+
port: Port number to listen on. Defaults to 8000.
|
|
436
|
+
find_port: If True, automatically find an available port if the
|
|
437
|
+
specified port is in use. Defaults to True.
|
|
438
|
+
reload: If True, enable auto-reload on file changes. Defaults to True.
|
|
439
|
+
"""
|
|
351
440
|
if find_port:
|
|
352
441
|
port = find_available_port(port)
|
|
353
442
|
|
|
354
443
|
uvicorn.run(self.asgi_factory, reload=reload)
|
|
355
444
|
|
|
356
|
-
def setup(self, server_address: str):
|
|
445
|
+
def setup(self, server_address: str) -> None:
|
|
446
|
+
"""Initialize the app with a server address.
|
|
447
|
+
|
|
448
|
+
Configures FastAPI routes, middleware, CORS, and Socket.IO handlers.
|
|
449
|
+
Called automatically by asgi_factory().
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
server_address: The public URL where the server is accessible.
|
|
453
|
+
|
|
454
|
+
Note:
|
|
455
|
+
This method is idempotent - calling it multiple times on an already
|
|
456
|
+
initialized app will log a warning and return early.
|
|
457
|
+
"""
|
|
357
458
|
if self.status >= AppStatus.initialized:
|
|
358
459
|
logger.warning("Called App.setup() on an already initialized application")
|
|
359
460
|
return
|
|
@@ -436,6 +537,8 @@ class App:
|
|
|
436
537
|
raise HTTPException(
|
|
437
538
|
status_code=400, detail="'paths' must be a non-empty list"
|
|
438
539
|
)
|
|
540
|
+
paths = [ensure_absolute_path(path) for path in paths]
|
|
541
|
+
payload["paths"] = paths
|
|
439
542
|
route_info = payload.get("routeInfo")
|
|
440
543
|
|
|
441
544
|
client_addr: str | None = get_client_address(request)
|
|
@@ -453,8 +556,9 @@ class App:
|
|
|
453
556
|
# Schedule cleanup timeout (will cancel/reschedule on activity)
|
|
454
557
|
self._schedule_render_cleanup(render_id)
|
|
455
558
|
|
|
456
|
-
|
|
457
|
-
captured
|
|
559
|
+
def _normalize_prerender_result(
|
|
560
|
+
captured: ServerInitMessage | ServerNavigateToMessage,
|
|
561
|
+
) -> Ok[ServerInitMessage] | Redirect | NotFound:
|
|
458
562
|
if captured["type"] == "vdom_init":
|
|
459
563
|
return Ok(captured)
|
|
460
564
|
if captured["type"] == "navigate_to":
|
|
@@ -467,12 +571,6 @@ class App:
|
|
|
467
571
|
# Fallback: shouldn't happen, return not found to be safe
|
|
468
572
|
return NotFound()
|
|
469
573
|
|
|
470
|
-
def _normalize_prerender_response(res: Any) -> RoutePrerenderResponse:
|
|
471
|
-
if isinstance(res, (Ok, Redirect, NotFound)):
|
|
472
|
-
return res
|
|
473
|
-
# Treat any other value as a VDOM payload
|
|
474
|
-
return Ok(res)
|
|
475
|
-
|
|
476
574
|
with PulseContext.update(render=render):
|
|
477
575
|
# Call top-level prerender middleware, which wraps the route processing
|
|
478
576
|
async def _process_routes() -> PrerenderResponse:
|
|
@@ -487,37 +585,21 @@ class App:
|
|
|
487
585
|
},
|
|
488
586
|
}
|
|
489
587
|
|
|
490
|
-
|
|
588
|
+
captured = render.prerender(paths, route_info)
|
|
589
|
+
|
|
491
590
|
for p in paths:
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
#
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
request=PulseRequest.from_fastapi(request),
|
|
502
|
-
session=session.data,
|
|
503
|
-
next=_next,
|
|
504
|
-
)
|
|
505
|
-
res = _normalize_prerender_response(res)
|
|
506
|
-
if isinstance(res, Ok):
|
|
507
|
-
# Aggregate results
|
|
508
|
-
result_data["views"][p] = res.payload
|
|
509
|
-
elif isinstance(res, Redirect):
|
|
510
|
-
# Return redirect immediately
|
|
511
|
-
return Redirect(path=res.path or "/")
|
|
512
|
-
elif isinstance(res, NotFound):
|
|
513
|
-
# Return not found immediately
|
|
514
|
-
return NotFound()
|
|
515
|
-
else:
|
|
516
|
-
raise ValueError("Unexpected prerender response:", res)
|
|
517
|
-
except RedirectInterrupt as r:
|
|
518
|
-
return Redirect(path=r.path)
|
|
519
|
-
except NotFoundInterrupt:
|
|
591
|
+
res = _normalize_prerender_result(captured[p])
|
|
592
|
+
if isinstance(res, Ok):
|
|
593
|
+
# Aggregate results
|
|
594
|
+
result_data["views"][p] = res.payload
|
|
595
|
+
elif isinstance(res, Redirect):
|
|
596
|
+
# Return redirect immediately
|
|
597
|
+
return Redirect(path=res.path or "/")
|
|
598
|
+
elif isinstance(res, NotFound):
|
|
599
|
+
# Return not found immediately
|
|
520
600
|
return NotFound()
|
|
601
|
+
else:
|
|
602
|
+
raise ValueError("Unexpected prerender response:", res)
|
|
521
603
|
|
|
522
604
|
return Ok(result_data)
|
|
523
605
|
|
|
@@ -741,6 +823,8 @@ class App:
|
|
|
741
823
|
async def _handle_pulse_message(
|
|
742
824
|
self, render: RenderSession, session: UserSession, msg: ClientPulseMessage
|
|
743
825
|
) -> None:
|
|
826
|
+
print(f"[MSG] {msg}")
|
|
827
|
+
|
|
744
828
|
async def _next() -> Ok[None]:
|
|
745
829
|
if msg["type"] == "attach":
|
|
746
830
|
render.attach(msg["path"], msg["routeInfo"])
|
|
@@ -919,6 +1003,9 @@ class App:
|
|
|
919
1003
|
self.routes,
|
|
920
1004
|
server_address=self.server_address,
|
|
921
1005
|
client_address=client_address,
|
|
1006
|
+
detach_queue_timeout=self.detach_queue_timeout,
|
|
1007
|
+
disconnect_queue_timeout=self.disconnect_queue_timeout,
|
|
1008
|
+
render_loop_limit=self.render_loop_limit,
|
|
922
1009
|
)
|
|
923
1010
|
self.render_sessions[rid] = render
|
|
924
1011
|
self._render_to_user[rid] = session.sid
|
|
@@ -24,14 +24,40 @@ logger = logging.getLogger(__name__)
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
ChannelHandler = Callable[[Any], Any | Awaitable[Any]]
|
|
27
|
+
"""Handler function for channel events. Can be sync or async.
|
|
28
|
+
|
|
29
|
+
Type alias for ``Callable[[Any], Any | Awaitable[Any]]``.
|
|
30
|
+
"""
|
|
27
31
|
|
|
28
32
|
|
|
29
33
|
class ChannelClosed(RuntimeError):
|
|
30
|
-
"""Raised when interacting with a channel that has been closed.
|
|
34
|
+
"""Raised when interacting with a channel that has been closed.
|
|
35
|
+
|
|
36
|
+
This exception is raised when attempting to call ``on()``, ``emit()``,
|
|
37
|
+
or ``request()`` on a channel that has already been closed.
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
ch = ps.channel("my-channel")
|
|
43
|
+
ch.close()
|
|
44
|
+
ch.emit("event") # Raises ChannelClosed
|
|
45
|
+
```
|
|
46
|
+
"""
|
|
31
47
|
|
|
32
48
|
|
|
33
49
|
class ChannelTimeout(asyncio.TimeoutError):
|
|
34
|
-
"""Raised when a channel request times out waiting for a response.
|
|
50
|
+
"""Raised when a channel request times out waiting for a response.
|
|
51
|
+
|
|
52
|
+
This exception is raised by ``Channel.request()`` when the specified
|
|
53
|
+
timeout elapses before receiving a response from the client.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
result = await ch.request("get_value", timeout=5.0) # Raises if no response in 5s
|
|
59
|
+
```
|
|
60
|
+
"""
|
|
35
61
|
|
|
36
62
|
|
|
37
63
|
@dataclass(slots=True)
|
|
@@ -311,7 +337,32 @@ class ChannelsManager:
|
|
|
311
337
|
|
|
312
338
|
|
|
313
339
|
class Channel:
|
|
314
|
-
"""Bidirectional communication channel bound to a render session.
|
|
340
|
+
"""Bidirectional communication channel bound to a render session.
|
|
341
|
+
|
|
342
|
+
Channels enable real-time messaging between server and client. Use
|
|
343
|
+
``ps.channel()`` to create a channel within a component.
|
|
344
|
+
|
|
345
|
+
Attributes:
|
|
346
|
+
id: Channel identifier (auto-generated UUID or user-provided).
|
|
347
|
+
render_id: Associated render session ID.
|
|
348
|
+
session_id: Associated user session ID.
|
|
349
|
+
route_path: Route path this channel is bound to, or None.
|
|
350
|
+
closed: Whether the channel has been closed.
|
|
351
|
+
|
|
352
|
+
Example:
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
@ps.component
|
|
356
|
+
def ChatRoom():
|
|
357
|
+
ch = ps.channel("chat")
|
|
358
|
+
|
|
359
|
+
@ch.on("message")
|
|
360
|
+
def handle_message(payload):
|
|
361
|
+
ch.emit("broadcast", payload)
|
|
362
|
+
|
|
363
|
+
return ps.div("Chat room")
|
|
364
|
+
```
|
|
365
|
+
"""
|
|
315
366
|
|
|
316
367
|
_manager: ChannelsManager
|
|
317
368
|
id: str
|
|
@@ -344,7 +395,24 @@ class Channel:
|
|
|
344
395
|
def on(self, event: str, handler: ChannelHandler) -> Callable[[], None]:
|
|
345
396
|
"""Register a handler for an incoming event.
|
|
346
397
|
|
|
347
|
-
|
|
398
|
+
Args:
|
|
399
|
+
event: Event name to listen for.
|
|
400
|
+
handler: Callback function ``(payload: Any) -> Any | Awaitable[Any]``.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Callable that removes the handler when invoked.
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
ChannelClosed: If the channel is closed.
|
|
407
|
+
|
|
408
|
+
Example:
|
|
409
|
+
|
|
410
|
+
```python
|
|
411
|
+
ch = ps.channel()
|
|
412
|
+
remove_handler = ch.on("data", lambda payload: print(payload))
|
|
413
|
+
# Later, to unregister:
|
|
414
|
+
remove_handler()
|
|
415
|
+
```
|
|
348
416
|
"""
|
|
349
417
|
|
|
350
418
|
self._ensure_open()
|
|
@@ -368,7 +436,21 @@ class Channel:
|
|
|
368
436
|
# Outgoing messages
|
|
369
437
|
# ---------------------------------------------------------------------
|
|
370
438
|
def emit(self, event: str, payload: Any = None) -> None:
|
|
371
|
-
"""Send a fire-and-forget event to the client.
|
|
439
|
+
"""Send a fire-and-forget event to the client.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
event: Event name.
|
|
443
|
+
payload: Data to send (optional).
|
|
444
|
+
|
|
445
|
+
Raises:
|
|
446
|
+
ChannelClosed: If the channel is closed.
|
|
447
|
+
|
|
448
|
+
Example:
|
|
449
|
+
|
|
450
|
+
```python
|
|
451
|
+
ch.emit("notification", {"message": "Hello"})
|
|
452
|
+
```
|
|
453
|
+
"""
|
|
372
454
|
|
|
373
455
|
self._ensure_open()
|
|
374
456
|
msg = ServerChannelRequestMessage(
|
|
@@ -389,7 +471,26 @@ class Channel:
|
|
|
389
471
|
*,
|
|
390
472
|
timeout: float | None = None,
|
|
391
473
|
) -> Any:
|
|
392
|
-
"""Send a request to the client and await the response.
|
|
474
|
+
"""Send a request to the client and await the response.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
event: Event name.
|
|
478
|
+
payload: Data to send (optional).
|
|
479
|
+
timeout: Timeout in seconds (optional).
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Response payload from client.
|
|
483
|
+
|
|
484
|
+
Raises:
|
|
485
|
+
ChannelClosed: If the channel is closed.
|
|
486
|
+
ChannelTimeout: If the request times out.
|
|
487
|
+
|
|
488
|
+
Example:
|
|
489
|
+
|
|
490
|
+
```python
|
|
491
|
+
result = await ch.request("get_value", timeout=5.0)
|
|
492
|
+
```
|
|
493
|
+
"""
|
|
393
494
|
|
|
394
495
|
self._ensure_open()
|
|
395
496
|
request_id = uuid.uuid4().hex
|
|
@@ -421,6 +522,11 @@ class Channel:
|
|
|
421
522
|
|
|
422
523
|
# ---------------------------------------------------------------------
|
|
423
524
|
def close(self) -> None:
|
|
525
|
+
"""Close the channel and clean up resources.
|
|
526
|
+
|
|
527
|
+
After closing, any further operations on the channel will raise
|
|
528
|
+
``ChannelClosed``. Pending requests will be cancelled.
|
|
529
|
+
"""
|
|
424
530
|
if self.closed:
|
|
425
531
|
return
|
|
426
532
|
self.closed = True
|
|
@@ -458,7 +564,33 @@ class Channel:
|
|
|
458
564
|
|
|
459
565
|
|
|
460
566
|
def channel(identifier: str | None = None) -> Channel:
|
|
461
|
-
"""
|
|
567
|
+
"""Create a channel bound to the current render session.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
identifier: Optional channel ID. Auto-generated UUID if not provided.
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
Channel instance.
|
|
574
|
+
|
|
575
|
+
Raises:
|
|
576
|
+
RuntimeError: If called outside an active render session.
|
|
577
|
+
|
|
578
|
+
Example:
|
|
579
|
+
|
|
580
|
+
```python
|
|
581
|
+
import pulse as ps
|
|
582
|
+
|
|
583
|
+
@ps.component
|
|
584
|
+
def ChatRoom():
|
|
585
|
+
ch = ps.channel("chat")
|
|
586
|
+
|
|
587
|
+
@ch.on("message")
|
|
588
|
+
def handle_message(payload):
|
|
589
|
+
ch.emit("broadcast", payload)
|
|
590
|
+
|
|
591
|
+
return ps.div("Chat room")
|
|
592
|
+
```
|
|
593
|
+
"""
|
|
462
594
|
|
|
463
595
|
ctx = PulseContext.get()
|
|
464
596
|
if ctx.render is None:
|
|
@@ -210,6 +210,7 @@ def run(
|
|
|
210
210
|
mode=app_instance.env,
|
|
211
211
|
ready_pattern=r"localhost:\d+",
|
|
212
212
|
on_ready=mark_web_ready,
|
|
213
|
+
plain=plain,
|
|
213
214
|
)
|
|
214
215
|
commands.append(web_cmd)
|
|
215
216
|
# Set env var so app can read the React server address (only used in single-server mode)
|
|
@@ -228,6 +229,7 @@ def run(
|
|
|
228
229
|
verbose=verbose,
|
|
229
230
|
ready_pattern=r"Application startup complete",
|
|
230
231
|
on_ready=mark_server_ready,
|
|
232
|
+
plain=plain,
|
|
231
233
|
)
|
|
232
234
|
commands.append(server_cmd)
|
|
233
235
|
|
|
@@ -394,6 +396,7 @@ def build_uvicorn_command(
|
|
|
394
396
|
verbose: bool = False,
|
|
395
397
|
ready_pattern: str | None = None,
|
|
396
398
|
on_ready: Callable[[], None] | None = None,
|
|
399
|
+
plain: bool = False,
|
|
397
400
|
) -> CommandSpec:
|
|
398
401
|
app_import = f"{app_ctx.module_name}:{app_ctx.app_var}.asgi_factory"
|
|
399
402
|
args: list[str] = [
|
|
@@ -421,16 +424,22 @@ def build_uvicorn_command(
|
|
|
421
424
|
|
|
422
425
|
if extra_args:
|
|
423
426
|
args.extend(extra_args)
|
|
427
|
+
if plain:
|
|
428
|
+
args.append("--no-use-colors")
|
|
424
429
|
|
|
425
430
|
command_env = os.environ.copy()
|
|
426
431
|
command_env.update(
|
|
427
432
|
{
|
|
428
|
-
"FORCE_COLOR": "1",
|
|
429
433
|
"PYTHONUNBUFFERED": "1",
|
|
430
434
|
ENV_PULSE_HOST: address,
|
|
431
435
|
ENV_PULSE_PORT: str(port),
|
|
432
436
|
}
|
|
433
437
|
)
|
|
438
|
+
if plain:
|
|
439
|
+
command_env["NO_COLOR"] = "1"
|
|
440
|
+
command_env["FORCE_COLOR"] = "0"
|
|
441
|
+
else:
|
|
442
|
+
command_env["FORCE_COLOR"] = "1"
|
|
434
443
|
# Pass React server address to uvicorn process if set
|
|
435
444
|
if ENV_PULSE_REACT_SERVER_ADDRESS in os.environ:
|
|
436
445
|
command_env[ENV_PULSE_REACT_SERVER_ADDRESS] = os.environ[
|
|
@@ -471,6 +480,7 @@ def build_web_command(
|
|
|
471
480
|
mode: PulseEnv = "dev",
|
|
472
481
|
ready_pattern: str | None = None,
|
|
473
482
|
on_ready: Callable[[], None] | None = None,
|
|
483
|
+
plain: bool = False,
|
|
474
484
|
) -> CommandSpec:
|
|
475
485
|
command_env = os.environ.copy()
|
|
476
486
|
if mode == "prod":
|
|
@@ -493,10 +503,14 @@ def build_web_command(
|
|
|
493
503
|
|
|
494
504
|
command_env.update(
|
|
495
505
|
{
|
|
496
|
-
"FORCE_COLOR": "1",
|
|
497
506
|
"PYTHONUNBUFFERED": "1",
|
|
498
507
|
}
|
|
499
508
|
)
|
|
509
|
+
if plain:
|
|
510
|
+
command_env["NO_COLOR"] = "1"
|
|
511
|
+
command_env["FORCE_COLOR"] = "0"
|
|
512
|
+
else:
|
|
513
|
+
command_env["FORCE_COLOR"] = "1"
|
|
500
514
|
|
|
501
515
|
return CommandSpec(
|
|
502
516
|
name="web",
|