pulse-framework 0.1.0__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 +175 -0
- pulse/app.py +349 -0
- pulse/cmd.py +324 -0
- pulse/codegen.py +147 -0
- pulse/components/__init__.py +1 -0
- pulse/components/react_router.py +43 -0
- pulse/context.py +15 -0
- pulse/decorators.py +187 -0
- pulse/diff.py +252 -0
- pulse/flags.py +5 -0
- pulse/flatted.py +159 -0
- pulse/helpers.py +27 -0
- pulse/hooks.py +441 -0
- pulse/html/__init__.py +304 -0
- pulse/html/attributes.py +930 -0
- pulse/html/elements.py +1024 -0
- pulse/html/events.py +419 -0
- pulse/html/tags.py +171 -0
- pulse/html/tags.pyi +390 -0
- pulse/messages.py +109 -0
- pulse/middleware.py +158 -0
- pulse/query.py +286 -0
- pulse/react_component.py +803 -0
- pulse/reactive.py +514 -0
- pulse/reactive_extensions.py +626 -0
- pulse/reconciler.py +575 -0
- pulse/request.py +162 -0
- pulse/routing.py +350 -0
- pulse/session.py +310 -0
- pulse/state.py +309 -0
- pulse/templates.py +171 -0
- pulse/tests/__init__.py +0 -0
- pulse/tests/old_test_diff.py +174 -0
- pulse/tests/test_codegen.py +224 -0
- pulse/tests/test_flatted.py +297 -0
- pulse/tests/test_nodes.py +439 -0
- pulse/tests/test_query.py +391 -0
- pulse/tests/test_react.py +797 -0
- pulse/tests/test_reactive.py +1203 -0
- pulse/tests/test_reconciler.py +1759 -0
- pulse/tests/test_routing.py +167 -0
- pulse/tests/test_session.py +267 -0
- pulse/tests/test_state.py +569 -0
- pulse/tests/test_utils.py +101 -0
- pulse/vdom.py +381 -0
- pulse_framework-0.1.0.dist-info/METADATA +38 -0
- pulse_framework-0.1.0.dist-info/RECORD +50 -0
- pulse_framework-0.1.0.dist-info/WHEEL +4 -0
- pulse_framework-0.1.0.dist-info/entry_points.txt +2 -0
- pulse_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
pulse/__init__.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from .app import App, Session
|
|
2
|
+
from .state import State
|
|
3
|
+
from .routing import Route, Layout
|
|
4
|
+
from .reactive import Signal, Computed, Effect, Batch, Untrack, IgnoreBatch
|
|
5
|
+
from .reactive_extensions import ReactiveDict, ReactiveList, ReactiveSet, reactive
|
|
6
|
+
from .hooks import (
|
|
7
|
+
states,
|
|
8
|
+
effects,
|
|
9
|
+
setup,
|
|
10
|
+
route_info,
|
|
11
|
+
session_context,
|
|
12
|
+
call_api,
|
|
13
|
+
navigate,
|
|
14
|
+
)
|
|
15
|
+
from .hooks import global_state
|
|
16
|
+
from .html import * # noqa: F403
|
|
17
|
+
from .middleware import (
|
|
18
|
+
PulseMiddleware,
|
|
19
|
+
Ok,
|
|
20
|
+
Redirect,
|
|
21
|
+
NotFound,
|
|
22
|
+
Deny,
|
|
23
|
+
PulseRequest,
|
|
24
|
+
ConnectResponse,
|
|
25
|
+
PrerenderResponse,
|
|
26
|
+
MiddlewareStack,
|
|
27
|
+
stack,
|
|
28
|
+
)
|
|
29
|
+
from .decorators import computed, effect, query
|
|
30
|
+
|
|
31
|
+
# Import HTML tags and other UI components
|
|
32
|
+
from .vdom import (
|
|
33
|
+
Node,
|
|
34
|
+
Element,
|
|
35
|
+
Primitive,
|
|
36
|
+
VDOMNode,
|
|
37
|
+
component,
|
|
38
|
+
Component,
|
|
39
|
+
ComponentNode,
|
|
40
|
+
Child,
|
|
41
|
+
)
|
|
42
|
+
from .html.tags import (
|
|
43
|
+
# Standard HTML tags
|
|
44
|
+
a,
|
|
45
|
+
abbr,
|
|
46
|
+
address,
|
|
47
|
+
article,
|
|
48
|
+
aside,
|
|
49
|
+
audio,
|
|
50
|
+
b,
|
|
51
|
+
bdi,
|
|
52
|
+
bdo,
|
|
53
|
+
blockquote,
|
|
54
|
+
body,
|
|
55
|
+
button,
|
|
56
|
+
canvas,
|
|
57
|
+
caption,
|
|
58
|
+
cite,
|
|
59
|
+
code,
|
|
60
|
+
colgroup,
|
|
61
|
+
data,
|
|
62
|
+
datalist,
|
|
63
|
+
dd,
|
|
64
|
+
del_,
|
|
65
|
+
details,
|
|
66
|
+
dfn,
|
|
67
|
+
dialog,
|
|
68
|
+
div,
|
|
69
|
+
dl,
|
|
70
|
+
dt,
|
|
71
|
+
em,
|
|
72
|
+
fieldset,
|
|
73
|
+
figcaption,
|
|
74
|
+
figure,
|
|
75
|
+
footer,
|
|
76
|
+
form,
|
|
77
|
+
h1,
|
|
78
|
+
h2,
|
|
79
|
+
h3,
|
|
80
|
+
h4,
|
|
81
|
+
h5,
|
|
82
|
+
h6,
|
|
83
|
+
head,
|
|
84
|
+
header,
|
|
85
|
+
hgroup,
|
|
86
|
+
html,
|
|
87
|
+
i,
|
|
88
|
+
iframe,
|
|
89
|
+
ins,
|
|
90
|
+
kbd,
|
|
91
|
+
label,
|
|
92
|
+
legend,
|
|
93
|
+
li,
|
|
94
|
+
main,
|
|
95
|
+
map_,
|
|
96
|
+
mark,
|
|
97
|
+
menu,
|
|
98
|
+
meter,
|
|
99
|
+
nav,
|
|
100
|
+
noscript,
|
|
101
|
+
object_,
|
|
102
|
+
ol,
|
|
103
|
+
optgroup,
|
|
104
|
+
option,
|
|
105
|
+
output,
|
|
106
|
+
p,
|
|
107
|
+
picture,
|
|
108
|
+
pre,
|
|
109
|
+
progress,
|
|
110
|
+
q,
|
|
111
|
+
rp,
|
|
112
|
+
rt,
|
|
113
|
+
ruby,
|
|
114
|
+
s,
|
|
115
|
+
samp,
|
|
116
|
+
script,
|
|
117
|
+
section,
|
|
118
|
+
select,
|
|
119
|
+
small,
|
|
120
|
+
span,
|
|
121
|
+
strong,
|
|
122
|
+
style,
|
|
123
|
+
sub,
|
|
124
|
+
summary,
|
|
125
|
+
sup,
|
|
126
|
+
table,
|
|
127
|
+
tbody,
|
|
128
|
+
td,
|
|
129
|
+
template,
|
|
130
|
+
textarea,
|
|
131
|
+
tfoot,
|
|
132
|
+
th,
|
|
133
|
+
thead,
|
|
134
|
+
time,
|
|
135
|
+
title,
|
|
136
|
+
tr,
|
|
137
|
+
u,
|
|
138
|
+
ul,
|
|
139
|
+
var,
|
|
140
|
+
video,
|
|
141
|
+
# Self-closing tags
|
|
142
|
+
area,
|
|
143
|
+
base,
|
|
144
|
+
br,
|
|
145
|
+
col,
|
|
146
|
+
embed,
|
|
147
|
+
hr,
|
|
148
|
+
img,
|
|
149
|
+
input,
|
|
150
|
+
link,
|
|
151
|
+
meta,
|
|
152
|
+
param,
|
|
153
|
+
source,
|
|
154
|
+
track,
|
|
155
|
+
wbr,
|
|
156
|
+
# React fragment
|
|
157
|
+
fragment,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
from .codegen import CodegenConfig
|
|
161
|
+
from .components import (
|
|
162
|
+
Link,
|
|
163
|
+
Outlet,
|
|
164
|
+
)
|
|
165
|
+
from .react_component import (
|
|
166
|
+
ComponentRegistry,
|
|
167
|
+
COMPONENT_REGISTRY,
|
|
168
|
+
ReactComponent,
|
|
169
|
+
react_component,
|
|
170
|
+
registered_react_components,
|
|
171
|
+
Prop,
|
|
172
|
+
prop,
|
|
173
|
+
DEFAULT,
|
|
174
|
+
)
|
|
175
|
+
from .helpers import EventHandler, For
|
pulse/app.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pulse UI App class - similar to FastAPI's App.
|
|
3
|
+
|
|
4
|
+
This module provides the main App class that users instantiate in their main.py
|
|
5
|
+
to define routes and configure their Pulse application.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from enum import IntEnum
|
|
12
|
+
from typing import Optional, Sequence, TypedDict, TypeVar, Unpack
|
|
13
|
+
from uuid import uuid4
|
|
14
|
+
|
|
15
|
+
import socketio
|
|
16
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
17
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
18
|
+
|
|
19
|
+
from pulse.codegen import Codegen, CodegenConfig
|
|
20
|
+
from pulse.react_component import ReactComponent, registered_react_components
|
|
21
|
+
from pulse.messages import ClientMessage, RouteInfo, ServerMessage
|
|
22
|
+
from pulse import flatted
|
|
23
|
+
from pulse.middleware import (
|
|
24
|
+
Deny,
|
|
25
|
+
MiddlewareStack,
|
|
26
|
+
NotFound,
|
|
27
|
+
Ok,
|
|
28
|
+
PulseMiddleware,
|
|
29
|
+
Redirect,
|
|
30
|
+
)
|
|
31
|
+
from pulse.reactive import (
|
|
32
|
+
REACTIVE_CONTEXT,
|
|
33
|
+
Epoch,
|
|
34
|
+
GlobalBatch,
|
|
35
|
+
ReactiveContext,
|
|
36
|
+
Scope,
|
|
37
|
+
)
|
|
38
|
+
from pulse.request import PulseRequest
|
|
39
|
+
from pulse.routing import Layout, Route, RouteTree
|
|
40
|
+
from pulse.session import Session
|
|
41
|
+
from pulse.vdom import VDOM
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
T = TypeVar("T")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AppStatus(IntEnum):
|
|
49
|
+
created = 0
|
|
50
|
+
initialized = 1
|
|
51
|
+
running = 2
|
|
52
|
+
stopped = 3
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AppConfig(TypedDict, total=False):
|
|
56
|
+
server_address: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class App:
|
|
60
|
+
"""
|
|
61
|
+
Pulse UI Application - the main entry point for defining your app.
|
|
62
|
+
|
|
63
|
+
Similar to FastAPI, users create an App instance and define their routes.
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
```python
|
|
67
|
+
import pulse as ps
|
|
68
|
+
|
|
69
|
+
app = ps.App()
|
|
70
|
+
|
|
71
|
+
@app.route("/")
|
|
72
|
+
def home():
|
|
73
|
+
return ps.div("Hello World!")
|
|
74
|
+
```
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
routes: Optional[Sequence[Route | Layout]] = None,
|
|
80
|
+
codegen: Optional[CodegenConfig] = None,
|
|
81
|
+
middleware: Optional[PulseMiddleware | Sequence[PulseMiddleware]] = None,
|
|
82
|
+
**config: Unpack[AppConfig],
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Initialize a new Pulse App.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
routes: Optional list of Route objects to register.
|
|
89
|
+
codegen: Optional codegen configuration.
|
|
90
|
+
"""
|
|
91
|
+
self.config = config
|
|
92
|
+
|
|
93
|
+
routes = routes or []
|
|
94
|
+
# Auto-add React components to all routes
|
|
95
|
+
add_react_components(routes, registered_react_components())
|
|
96
|
+
self.routes = RouteTree(routes)
|
|
97
|
+
self.sessions: dict[str, Session] = {}
|
|
98
|
+
|
|
99
|
+
self.codegen = Codegen(
|
|
100
|
+
self.routes,
|
|
101
|
+
config=codegen or CodegenConfig(),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
self.fastapi = FastAPI(title="Pulse UI Server")
|
|
105
|
+
self.sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
|
|
106
|
+
self.asgi = socketio.ASGIApp(self.sio, self.fastapi)
|
|
107
|
+
self.status = AppStatus.created
|
|
108
|
+
# Allow single middleware or sequence; compose into a stack when needed
|
|
109
|
+
if middleware is None:
|
|
110
|
+
self._middleware: PulseMiddleware | None = None
|
|
111
|
+
elif isinstance(middleware, PulseMiddleware):
|
|
112
|
+
self._middleware = middleware
|
|
113
|
+
else:
|
|
114
|
+
self._middleware = MiddlewareStack(middleware)
|
|
115
|
+
|
|
116
|
+
def setup(self):
|
|
117
|
+
if self.status >= AppStatus.initialized:
|
|
118
|
+
logger.warning("Called App.setup() on an already initialized application")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
# Add CORS middleware
|
|
122
|
+
REACTIVE_CONTEXT.set(AppReactiveContext())
|
|
123
|
+
self.fastapi.add_middleware(
|
|
124
|
+
CORSMiddleware,
|
|
125
|
+
allow_origin_regex=".*",
|
|
126
|
+
allow_credentials=True,
|
|
127
|
+
allow_methods=["*"],
|
|
128
|
+
allow_headers=["*"],
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@self.fastapi.get("/health")
|
|
132
|
+
def healthcheck():
|
|
133
|
+
return {"health": "ok", "message": "Pulse server is running"}
|
|
134
|
+
|
|
135
|
+
# RouteInfo is the request body
|
|
136
|
+
@self.fastapi.post("/prerender/{path:path}")
|
|
137
|
+
def prerender(path: str, route_info: RouteInfo, request: Request) -> VDOM:
|
|
138
|
+
# Provide a working reactive context (and not the global AppReactiveContext which errors)
|
|
139
|
+
if not path.startswith("/"):
|
|
140
|
+
path = "/" + path
|
|
141
|
+
session = Session(uuid4().hex, self.routes)
|
|
142
|
+
|
|
143
|
+
def _render() -> VDOM:
|
|
144
|
+
return session.render(path, route_info, prerendering=True)
|
|
145
|
+
|
|
146
|
+
if not self._middleware:
|
|
147
|
+
return _render()
|
|
148
|
+
try:
|
|
149
|
+
|
|
150
|
+
def _next():
|
|
151
|
+
return Ok(_render())
|
|
152
|
+
|
|
153
|
+
with session.reactive_context:
|
|
154
|
+
res = self._middleware.prerender(
|
|
155
|
+
path=path,
|
|
156
|
+
route_info=route_info,
|
|
157
|
+
request=PulseRequest.from_fastapi(request),
|
|
158
|
+
context=session.context,
|
|
159
|
+
next=_next,
|
|
160
|
+
)
|
|
161
|
+
except Exception:
|
|
162
|
+
logger.exception("Error in prerender middleware")
|
|
163
|
+
res = Ok(_render())
|
|
164
|
+
if isinstance(res, Redirect):
|
|
165
|
+
raise HTTPException(
|
|
166
|
+
status_code=302, headers={"Location": res.path or "/"}
|
|
167
|
+
)
|
|
168
|
+
elif isinstance(res, NotFound):
|
|
169
|
+
raise HTTPException(status_code=404)
|
|
170
|
+
elif isinstance(res, Ok):
|
|
171
|
+
return res.payload
|
|
172
|
+
# Fallback to default render
|
|
173
|
+
else:
|
|
174
|
+
raise NotImplementedError(f"Unexpected middleware return: {res}")
|
|
175
|
+
|
|
176
|
+
@self.sio.event
|
|
177
|
+
async def connect(sid: str, environ, auth=None):
|
|
178
|
+
# Create session first to instantiate reactive and session contexts
|
|
179
|
+
session = self.create_session(sid)
|
|
180
|
+
if self._middleware:
|
|
181
|
+
try:
|
|
182
|
+
|
|
183
|
+
def _next():
|
|
184
|
+
return Ok(None)
|
|
185
|
+
|
|
186
|
+
# Ensure middleware executes within the session's reactive context
|
|
187
|
+
with session.reactive_context:
|
|
188
|
+
res = self._middleware.connect(
|
|
189
|
+
request=PulseRequest.from_socketio_environ(environ, auth),
|
|
190
|
+
ctx=session.context,
|
|
191
|
+
next=_next,
|
|
192
|
+
)
|
|
193
|
+
except Exception:
|
|
194
|
+
logger.exception("Error in connect middleware")
|
|
195
|
+
res = Ok(None)
|
|
196
|
+
if isinstance(res, Deny):
|
|
197
|
+
# Tear down the created session if denied
|
|
198
|
+
try:
|
|
199
|
+
self.close_session(sid)
|
|
200
|
+
finally:
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
def on_message(message: ServerMessage):
|
|
204
|
+
message = flatted.stringify(message)
|
|
205
|
+
asyncio.create_task(self.sio.emit("message", message, to=sid))
|
|
206
|
+
|
|
207
|
+
session.connect(on_message)
|
|
208
|
+
|
|
209
|
+
@self.sio.event
|
|
210
|
+
def disconnect(sid: str):
|
|
211
|
+
self.close_session(sid)
|
|
212
|
+
|
|
213
|
+
@self.sio.event
|
|
214
|
+
def message(sid: str, data: ClientMessage):
|
|
215
|
+
try:
|
|
216
|
+
# Deserialize the message using flatted
|
|
217
|
+
data = flatted.parse(data)
|
|
218
|
+
session = self.sessions[sid]
|
|
219
|
+
|
|
220
|
+
def _handler(sess: Session) -> None:
|
|
221
|
+
# Per-message middleware guard
|
|
222
|
+
if self._middleware:
|
|
223
|
+
try:
|
|
224
|
+
# Run middleware within the session's reactive context
|
|
225
|
+
with sess.reactive_context:
|
|
226
|
+
res = self._middleware.message(
|
|
227
|
+
ctx=sess.context,
|
|
228
|
+
data=data,
|
|
229
|
+
next=lambda: Ok(None),
|
|
230
|
+
)
|
|
231
|
+
if isinstance(res, Deny):
|
|
232
|
+
# Report as server error for this path
|
|
233
|
+
path = data.get("path")
|
|
234
|
+
sess.report_error(
|
|
235
|
+
path or "api_response",
|
|
236
|
+
"server",
|
|
237
|
+
Exception("Request denied by server"),
|
|
238
|
+
{"kind": "deny"},
|
|
239
|
+
)
|
|
240
|
+
return
|
|
241
|
+
except Exception:
|
|
242
|
+
logger.exception("Error in message middleware")
|
|
243
|
+
if data["type"] == "mount":
|
|
244
|
+
sess.mount(data["path"], data["routeInfo"])
|
|
245
|
+
elif data["type"] == "navigate":
|
|
246
|
+
sess.navigate(data["path"], data["routeInfo"])
|
|
247
|
+
elif data["type"] == "callback":
|
|
248
|
+
sess.execute_callback(
|
|
249
|
+
data["path"], data["callback"], data["args"]
|
|
250
|
+
)
|
|
251
|
+
elif data["type"] == "unmount":
|
|
252
|
+
sess.unmount(data["path"])
|
|
253
|
+
elif data["type"] == "api_result":
|
|
254
|
+
# type: ignore[union-attr]
|
|
255
|
+
sess.handle_api_result(data) # type: ignore[arg-type]
|
|
256
|
+
else:
|
|
257
|
+
logger.warning(f"Unknown message type received: {data}")
|
|
258
|
+
|
|
259
|
+
_handler(session)
|
|
260
|
+
except Exception as e:
|
|
261
|
+
try:
|
|
262
|
+
# Best effort: report error for this path if available
|
|
263
|
+
path = data.get("path", "") if isinstance(data, dict) else ""
|
|
264
|
+
session = self.sessions.get(sid)
|
|
265
|
+
if session:
|
|
266
|
+
session.report_error(path, "server", e)
|
|
267
|
+
else:
|
|
268
|
+
logger.exception("Error handling client message: %s", data)
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logger.exception("Error while reporting server error: %s", e)
|
|
271
|
+
|
|
272
|
+
def run_codegen(self, address: Optional[str] = None):
|
|
273
|
+
address = address or self.config.get("server_address")
|
|
274
|
+
if not address:
|
|
275
|
+
raise RuntimeError(
|
|
276
|
+
"Please provide a server address to the App constructor or the Pulse CLI."
|
|
277
|
+
)
|
|
278
|
+
self.codegen.generate_all(address)
|
|
279
|
+
|
|
280
|
+
def asgi_factory(self):
|
|
281
|
+
"""
|
|
282
|
+
ASGI factory for uvicorn. This is called on every reload.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
host = os.environ.get("PULSE_HOST", "127.0.0.1")
|
|
286
|
+
port = int(os.environ.get("PULSE_PORT", 8000))
|
|
287
|
+
protocol = "http" if host in ("127.0.0.1", "localhost") else "https"
|
|
288
|
+
|
|
289
|
+
self.run_codegen(f"{protocol}://{host}:{port}")
|
|
290
|
+
self.setup()
|
|
291
|
+
return self.asgi
|
|
292
|
+
|
|
293
|
+
def get_route(self, path: str):
|
|
294
|
+
self.routes.find(path)
|
|
295
|
+
|
|
296
|
+
def create_session(self, id: str):
|
|
297
|
+
if id in self.sessions:
|
|
298
|
+
raise ValueError(f"Session {id} already exists")
|
|
299
|
+
# print(f"--> Creating session {id}")
|
|
300
|
+
self.sessions[id] = Session(id, self.routes)
|
|
301
|
+
return self.sessions[id]
|
|
302
|
+
|
|
303
|
+
def close_session(self, id: str):
|
|
304
|
+
if id not in self.sessions:
|
|
305
|
+
raise KeyError(f"Session {id} does not exist")
|
|
306
|
+
self.sessions[id].close()
|
|
307
|
+
del self.sessions[id]
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def add_react_components(
|
|
311
|
+
routes: Sequence[Route | Layout], components: list[ReactComponent]
|
|
312
|
+
):
|
|
313
|
+
for route in routes:
|
|
314
|
+
if route.components is None:
|
|
315
|
+
route.components = components
|
|
316
|
+
if route.children:
|
|
317
|
+
add_react_components(route.children, components)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class AppReactiveContext(ReactiveContext):
|
|
321
|
+
def __init__(self, allow_usage=False) -> None:
|
|
322
|
+
self._epoch = Epoch()
|
|
323
|
+
self._batch = GlobalBatch()
|
|
324
|
+
self._scope = Scope()
|
|
325
|
+
self.allow_usage = allow_usage
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def epoch(self):
|
|
329
|
+
if self.allow_usage:
|
|
330
|
+
return self._epoch
|
|
331
|
+
raise RuntimeError(
|
|
332
|
+
"App reactive context should not be used, all reactive context should be scoped to sessions."
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def batch(self):
|
|
337
|
+
if self.allow_usage:
|
|
338
|
+
return self._batch
|
|
339
|
+
raise RuntimeError(
|
|
340
|
+
"App reactive context should not be used, all reactive context should be scoped to sessions."
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def scope(self):
|
|
345
|
+
if self.allow_usage:
|
|
346
|
+
return self._scope
|
|
347
|
+
raise RuntimeError(
|
|
348
|
+
"App reactive context should not be used, all reactive context should be scoped to sessions."
|
|
349
|
+
)
|