ngapp 0.0.2.dev2236__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.
- ngapp/__init__.py +39 -0
- ngapp/_version.py +21 -0
- ngapp/api.py +64 -0
- ngapp/app.py +618 -0
- ngapp/basecomponent.py +5 -0
- ngapp/cli/__init__.py +0 -0
- ngapp/cli/build_static_website.py +121 -0
- ngapp/cli/generate_app_config.py +52 -0
- ngapp/cli/get_plugin_dirs.py +37 -0
- ngapp/cli/run.py +241 -0
- ngapp/cli/serve_app.py +118 -0
- ngapp/cli/serve_compute_env.py +69 -0
- ngapp/cli/serve_in_venv.py +243 -0
- ngapp/cli/serve_standalone.py +242 -0
- ngapp/cli/worker.py +42 -0
- ngapp/cli/zip_module.py +22 -0
- ngapp/components/__init__.py +5 -0
- ngapp/components/basecomponent.py +780 -0
- ngapp/components/helper_components.py +978 -0
- ngapp/components/material.py +148 -0
- ngapp/components/qcomponents.py +25178 -0
- ngapp/components/visualization.py +810 -0
- ngapp/create_app.py +41 -0
- ngapp/file.py +74 -0
- ngapp/frontend_testing.py +77 -0
- ngapp/generate_metadata.py +29 -0
- ngapp/material.py +5 -0
- ngapp/qcomponents.py +5 -0
- ngapp/test_utils.py +123 -0
- ngapp/utils.py +725 -0
- ngapp/visualization.py +5 -0
- ngapp-0.0.2.dev2236.dist-info/METADATA +15 -0
- ngapp-0.0.2.dev2236.dist-info/RECORD +35 -0
- ngapp-0.0.2.dev2236.dist-info/WHEEL +5 -0
- ngapp-0.0.2.dev2236.dist-info/top_level.txt +1 -0
ngapp/app.py
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
# pylint: disable=invalid-name
|
|
2
|
+
"""Base class for all applications"""
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import copy
|
|
6
|
+
import hashlib
|
|
7
|
+
import importlib.util
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import os.path
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import time
|
|
15
|
+
import types
|
|
16
|
+
from contextlib import contextmanager
|
|
17
|
+
from enum import IntEnum
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import pydantic
|
|
21
|
+
import urllib3
|
|
22
|
+
|
|
23
|
+
from . import api, utils
|
|
24
|
+
from .components.basecomponent import AppStatus, Component, reset_components
|
|
25
|
+
from .utils import (
|
|
26
|
+
ComputeEnvironment,
|
|
27
|
+
EnvironmentType,
|
|
28
|
+
get_environment,
|
|
29
|
+
is_pyodide,
|
|
30
|
+
read_file,
|
|
31
|
+
read_file_binary,
|
|
32
|
+
read_json,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def asset(filename: Path | str, binary: bool | None = None, module=None) -> str:
|
|
37
|
+
"""Load an asset from the 'assets' directory within an application"""
|
|
38
|
+
import inspect
|
|
39
|
+
|
|
40
|
+
filename = Path(filename)
|
|
41
|
+
file_format = filename.suffix[1:].lower()
|
|
42
|
+
if file_format == "svg":
|
|
43
|
+
file_format = "svg+xml"
|
|
44
|
+
if module:
|
|
45
|
+
calling_file = Path(inspect.getfile(module))
|
|
46
|
+
else:
|
|
47
|
+
calling_file = Path(inspect.stack()[1].filename)
|
|
48
|
+
filename = calling_file.parent / "assets" / filename
|
|
49
|
+
|
|
50
|
+
if not filename.exists():
|
|
51
|
+
raise FileNotFoundError(f"Asset file {filename} not found.")
|
|
52
|
+
|
|
53
|
+
if binary is None:
|
|
54
|
+
binary = filename.suffix in [".png", ".jpg", ".jpeg", ".gif", ".svg"]
|
|
55
|
+
|
|
56
|
+
if binary:
|
|
57
|
+
data = base64.b64encode(read_file_binary(filename)).decode("ascii")
|
|
58
|
+
else:
|
|
59
|
+
data = read_file(filename)
|
|
60
|
+
|
|
61
|
+
if file_format in ["png", "jpg", "jpeg", "gif", "svg+xml"]:
|
|
62
|
+
data = f"data:image/{file_format};base64,{data}"
|
|
63
|
+
|
|
64
|
+
return data
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AccessLevel(IntEnum):
|
|
68
|
+
"""Access levels enum to distinguish acess rights (HIDDEN, ADMIN, etc.)"""
|
|
69
|
+
|
|
70
|
+
HIDDEN = 0
|
|
71
|
+
VISIBLE = 5
|
|
72
|
+
TRIAL = 10
|
|
73
|
+
STANDARD = 20
|
|
74
|
+
ADMIN = 9000
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AccessLevelConfig(pydantic.BaseModel):
|
|
78
|
+
"""Defines the restrictions of access to an Application for a certain access level"""
|
|
79
|
+
|
|
80
|
+
model_config = pydantic.ConfigDict(extra="forbid")
|
|
81
|
+
|
|
82
|
+
name: str
|
|
83
|
+
access_level: AccessLevel = AccessLevel.HIDDEN
|
|
84
|
+
max_cpus: int = 0
|
|
85
|
+
max_memory: str = "0G"
|
|
86
|
+
max_saved_results: int = 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AppAccessConfig(pydantic.BaseModel):
|
|
90
|
+
"""Defines the restrictions of access to an Application for a certain access level"""
|
|
91
|
+
|
|
92
|
+
model_config = pydantic.ConfigDict(extra="forbid")
|
|
93
|
+
|
|
94
|
+
default_level: AccessLevel = AccessLevel.STANDARD
|
|
95
|
+
enable_trial: bool = False
|
|
96
|
+
auto_grant_trial: bool = False
|
|
97
|
+
trial_days: int = 21
|
|
98
|
+
levels: dict[AccessLevel, AccessLevelConfig] = {
|
|
99
|
+
AccessLevel.HIDDEN: AccessLevelConfig(
|
|
100
|
+
access_level=AccessLevel.HIDDEN,
|
|
101
|
+
name="hidden",
|
|
102
|
+
),
|
|
103
|
+
AccessLevel.VISIBLE: AccessLevelConfig(
|
|
104
|
+
access_level=AccessLevel.VISIBLE,
|
|
105
|
+
name="visible",
|
|
106
|
+
),
|
|
107
|
+
AccessLevel.TRIAL: AccessLevelConfig(
|
|
108
|
+
access_level=AccessLevel.TRIAL,
|
|
109
|
+
name="trial",
|
|
110
|
+
max_memory="14G",
|
|
111
|
+
max_cpus=4,
|
|
112
|
+
max_saved_results=20,
|
|
113
|
+
),
|
|
114
|
+
AccessLevel.STANDARD: AccessLevelConfig(
|
|
115
|
+
access_level=AccessLevel.STANDARD,
|
|
116
|
+
name="standard",
|
|
117
|
+
max_memory="14G",
|
|
118
|
+
max_cpus=4,
|
|
119
|
+
max_saved_results=20,
|
|
120
|
+
),
|
|
121
|
+
AccessLevel.ADMIN: AccessLevelConfig(
|
|
122
|
+
access_level=AccessLevel.ADMIN,
|
|
123
|
+
name="admin",
|
|
124
|
+
max_memory="60G",
|
|
125
|
+
max_cpus=20,
|
|
126
|
+
max_saved_results=999,
|
|
127
|
+
),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class PythonPackage(pydantic.BaseModel):
|
|
132
|
+
"""Python package model"""
|
|
133
|
+
|
|
134
|
+
file_name: str
|
|
135
|
+
package: bytes | None
|
|
136
|
+
|
|
137
|
+
model_config = pydantic.ConfigDict(ser_json_bytes="base64")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class AppConfig(pydantic.BaseModel):
|
|
141
|
+
"""App configuration class, contains all metadata about an app"""
|
|
142
|
+
|
|
143
|
+
model_config = pydantic.ConfigDict(extra="forbid")
|
|
144
|
+
|
|
145
|
+
name: str
|
|
146
|
+
version: str
|
|
147
|
+
|
|
148
|
+
python_class: str
|
|
149
|
+
frontend_pip_dependencies: list[str] = []
|
|
150
|
+
frontend_dependencies: list[str] = []
|
|
151
|
+
|
|
152
|
+
description: str = "No description."
|
|
153
|
+
image: str | None = None
|
|
154
|
+
logo: dict = {}
|
|
155
|
+
|
|
156
|
+
compute_environments: list[ComputeEnvironment] = []
|
|
157
|
+
access: AppAccessConfig = AppAccessConfig()
|
|
158
|
+
|
|
159
|
+
frontend_package: bytes = b""
|
|
160
|
+
python_packages: list[PythonPackage] = []
|
|
161
|
+
python_packages_hash: str = ""
|
|
162
|
+
|
|
163
|
+
def __init__(self, python_class, **kwargs):
|
|
164
|
+
if not isinstance(python_class, str):
|
|
165
|
+
python_class = python_class.__module__ + "." + python_class.__name__
|
|
166
|
+
super().__init__(python_class=python_class, **kwargs)
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def python_package_name(self):
|
|
170
|
+
return self.python_class.split(".")[0]
|
|
171
|
+
|
|
172
|
+
def create_frontend_package(self):
|
|
173
|
+
self.frontend_package = utils.zip_modules(
|
|
174
|
+
[self.python_package_name] + self.frontend_dependencies
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def create_backend_packages(self, include_dependencies=True):
|
|
178
|
+
if len(self.compute_environments) == 0:
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
origin = Path(importlib.util.find_spec(self.python_package_name).origin)
|
|
182
|
+
src_dir = None
|
|
183
|
+
for _ in range(3):
|
|
184
|
+
origin = origin.parent
|
|
185
|
+
for filename in ["pyproject.toml", "setup.py"]:
|
|
186
|
+
if (origin / filename).exists():
|
|
187
|
+
src_dir = origin
|
|
188
|
+
break
|
|
189
|
+
if src_dir is None:
|
|
190
|
+
raise RuntimeError(
|
|
191
|
+
f"Could not find source directory for package {self.python_package_name}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
with tempfile.TemporaryDirectory() as temp_dir_:
|
|
195
|
+
temp_dir = Path(temp_dir_)
|
|
196
|
+
subprocess.run(
|
|
197
|
+
[
|
|
198
|
+
sys.executable,
|
|
199
|
+
"-m",
|
|
200
|
+
"build",
|
|
201
|
+
"-n",
|
|
202
|
+
"-w",
|
|
203
|
+
"-o",
|
|
204
|
+
temp_dir,
|
|
205
|
+
src_dir,
|
|
206
|
+
],
|
|
207
|
+
check=True,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
package_hash = hashlib.sha256()
|
|
211
|
+
packages = []
|
|
212
|
+
|
|
213
|
+
dirs = [temp_dir]
|
|
214
|
+
if include_dependencies:
|
|
215
|
+
dirs.append(src_dir / "dependencies")
|
|
216
|
+
|
|
217
|
+
for d in dirs:
|
|
218
|
+
if not d.exists():
|
|
219
|
+
continue
|
|
220
|
+
for filename in sorted(os.listdir(d)):
|
|
221
|
+
if filename.endswith(".whl"):
|
|
222
|
+
data = (d / filename).read_bytes()
|
|
223
|
+
package_hash.update(data)
|
|
224
|
+
packages.append(
|
|
225
|
+
PythonPackage(
|
|
226
|
+
file_name=filename,
|
|
227
|
+
package=data,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
self.python_packages = packages
|
|
231
|
+
self.python_packages_hash = package_hash.hexdigest()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class App:
|
|
235
|
+
"""Base class for all applications"""
|
|
236
|
+
|
|
237
|
+
component: Component
|
|
238
|
+
metadata: dict
|
|
239
|
+
_default_data: dict | None
|
|
240
|
+
_status: AppStatus
|
|
241
|
+
|
|
242
|
+
def __init__(self, component: Component | None = None, name=None):
|
|
243
|
+
self.metadata = {"name": name}
|
|
244
|
+
self._default_data = None
|
|
245
|
+
self._status = AppStatus()
|
|
246
|
+
self._status.app = self
|
|
247
|
+
|
|
248
|
+
if component is not None:
|
|
249
|
+
self.component = component
|
|
250
|
+
component._namespace_id = ""
|
|
251
|
+
component._parent = self
|
|
252
|
+
component._status = self._status
|
|
253
|
+
component._recurse(Component._calc_namespace_id, True, set())
|
|
254
|
+
|
|
255
|
+
def __getitem__(self, key: str) -> Component:
|
|
256
|
+
return self._status.components_by_id[key]
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def assets_path(self) -> Path:
|
|
260
|
+
"""Get the path to the assets directory in the python package of the app"""
|
|
261
|
+
return (
|
|
262
|
+
Path(sys.modules[self.__module__.split(".")[0]].__file__).parent / "assets"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def set_colors(
|
|
266
|
+
self,
|
|
267
|
+
primary: str | None = None,
|
|
268
|
+
secondary: str | None = None,
|
|
269
|
+
accent: str | None = None,
|
|
270
|
+
dark: str | None = None,
|
|
271
|
+
positive: str | None = None,
|
|
272
|
+
negative: str | None = None,
|
|
273
|
+
info: str | None = None,
|
|
274
|
+
warning: str | None = None,
|
|
275
|
+
):
|
|
276
|
+
"""Set the colors of the app"""
|
|
277
|
+
if get_environment().type == EnvironmentType.LOCAL_APP:
|
|
278
|
+
from webgpu.platform import execute_when_init
|
|
279
|
+
|
|
280
|
+
def f(js):
|
|
281
|
+
if primary is not None:
|
|
282
|
+
js.document.body.style.setProperty("--q-primary", primary)
|
|
283
|
+
if secondary is not None:
|
|
284
|
+
js.document.body.style.setProperty("--q-secondary", secondary)
|
|
285
|
+
if accent is not None:
|
|
286
|
+
js.document.body.style.setProperty("--q-accent", accent)
|
|
287
|
+
if dark is not None:
|
|
288
|
+
js.document.body.style.setProperty("--q-dark", dark)
|
|
289
|
+
if positive is not None:
|
|
290
|
+
js.document.body.style.setProperty("--q-positive", positive)
|
|
291
|
+
if negative is not None:
|
|
292
|
+
js.document.body.style.setProperty("--q-negative", negative)
|
|
293
|
+
if info is not None:
|
|
294
|
+
js.document.body.style.setProperty("--q-info", info)
|
|
295
|
+
if warning is not None:
|
|
296
|
+
js.document.body.style.setProperty("--q-warning", warning)
|
|
297
|
+
|
|
298
|
+
execute_when_init(f)
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def name(self):
|
|
302
|
+
return self.metadata.get("name", "Untitled")
|
|
303
|
+
|
|
304
|
+
@name.setter
|
|
305
|
+
def name(self, value):
|
|
306
|
+
self.metadata["name"] = value
|
|
307
|
+
|
|
308
|
+
def upgrade(self, data):
|
|
309
|
+
"""Upgrade data to the current version"""
|
|
310
|
+
return data
|
|
311
|
+
|
|
312
|
+
def dump(
|
|
313
|
+
self, exclude_default_data=False, keep_storage=False, include_storage_data=False
|
|
314
|
+
):
|
|
315
|
+
"""Get input data for storage"""
|
|
316
|
+
if self.component is None:
|
|
317
|
+
return {}
|
|
318
|
+
if (
|
|
319
|
+
not include_storage_data
|
|
320
|
+
and keep_storage
|
|
321
|
+
and not get_environment().have_backend
|
|
322
|
+
):
|
|
323
|
+
self._save_storage_local()
|
|
324
|
+
component_data = {
|
|
325
|
+
"data": self.component._dump_recursive(),
|
|
326
|
+
"storage": self.component._dump_storage(include_storage_data),
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if exclude_default_data:
|
|
330
|
+
|
|
331
|
+
def remove_default_data(data, default_data):
|
|
332
|
+
if isinstance(data, dict):
|
|
333
|
+
for key in list(data.keys()):
|
|
334
|
+
if key not in default_data:
|
|
335
|
+
return
|
|
336
|
+
if isinstance(data[key], dict) and isinstance(
|
|
337
|
+
default_data[key], dict
|
|
338
|
+
):
|
|
339
|
+
if data[key] == default_data[key]:
|
|
340
|
+
del data[key]
|
|
341
|
+
else:
|
|
342
|
+
remove_default_data(data[key], default_data[key])
|
|
343
|
+
|
|
344
|
+
remove_default_data(component_data["data"], self._default_data["data"])
|
|
345
|
+
remove_default_data(
|
|
346
|
+
component_data["storage"], self._default_data["storage"]
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return {"component": component_data, "metadata": self.metadata}
|
|
350
|
+
|
|
351
|
+
def save(self):
|
|
352
|
+
"""Save data to backend"""
|
|
353
|
+
env = get_environment()
|
|
354
|
+
if not env.have_backend:
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
self.component._emit_recursive("before_save")
|
|
358
|
+
status = self._status
|
|
359
|
+
file_id = status.file_id
|
|
360
|
+
if file_id is None:
|
|
361
|
+
metadata = api.post(
|
|
362
|
+
"/create_model",
|
|
363
|
+
{"app_id": self.metadata["app_id"], "name": self.name or "Untitled"},
|
|
364
|
+
)
|
|
365
|
+
self.metadata |= metadata
|
|
366
|
+
status.file_id = metadata["id"]
|
|
367
|
+
|
|
368
|
+
api.put(f"/model/{status.file_id}", self.dump())
|
|
369
|
+
self.component._emit_recursive("save")
|
|
370
|
+
|
|
371
|
+
if env.type == EnvironmentType.PYODIDE:
|
|
372
|
+
env.frontend.set_query_parameter("fileId", status.file_id)
|
|
373
|
+
|
|
374
|
+
@classmethod
|
|
375
|
+
def reset(cls):
|
|
376
|
+
"""
|
|
377
|
+
Reset the app in the user interface to clean initial state. Only available in frontend and local environment. Return value is the new app instance.
|
|
378
|
+
"""
|
|
379
|
+
app = cls()
|
|
380
|
+
get_environment().frontend.reset_app(app)
|
|
381
|
+
return app
|
|
382
|
+
|
|
383
|
+
def save_local(self):
|
|
384
|
+
from webgpu import platform
|
|
385
|
+
|
|
386
|
+
dump = self.dump(include_storage_data=True)
|
|
387
|
+
name = self.name + ".sav" if self.name is not None else "untitled.sav"
|
|
388
|
+
options = {"suggestedName": name}
|
|
389
|
+
import pickle
|
|
390
|
+
|
|
391
|
+
pick = platform.js.showSaveFilePicker(options)
|
|
392
|
+
stream = pick.createWritable()
|
|
393
|
+
stream.write(pickle.dumps(dump))
|
|
394
|
+
stream.close()
|
|
395
|
+
|
|
396
|
+
def load_local(self):
|
|
397
|
+
from webgpu import platform
|
|
398
|
+
|
|
399
|
+
options = {"multiple": False, "accept": ".sav"}
|
|
400
|
+
pick = platform.js.showOpenFilePicker(options)
|
|
401
|
+
import pickle
|
|
402
|
+
|
|
403
|
+
data = pickle.loads(pick[0].getFile().arrayBuffer())
|
|
404
|
+
self.load(data)
|
|
405
|
+
|
|
406
|
+
def update(self, data: dict, load_local_storage=False, update_frontend=False):
|
|
407
|
+
"""Update app with new data"""
|
|
408
|
+
metadata = data.get("metadata", None)
|
|
409
|
+
component_data = data.get(
|
|
410
|
+
"component", data.get("data", {}).get("component", {})
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
if metadata:
|
|
414
|
+
self.metadata.update(metadata)
|
|
415
|
+
self._status.app_id = metadata.get("app_id", None)
|
|
416
|
+
self._status.file_id = metadata.get("id", None)
|
|
417
|
+
|
|
418
|
+
if "storage" in component_data:
|
|
419
|
+
self.component._load_storage(component_data["storage"])
|
|
420
|
+
|
|
421
|
+
if load_local_storage:
|
|
422
|
+
self.component._load_storage_local()
|
|
423
|
+
|
|
424
|
+
if "data" in component_data:
|
|
425
|
+
self.component._load_recursive(
|
|
426
|
+
component_data["data"], update_frontend=update_frontend
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def block_frontend_update(comp):
|
|
430
|
+
comp._block_frontend_update = True
|
|
431
|
+
|
|
432
|
+
self.component._recurse(block_frontend_update, True, set())
|
|
433
|
+
self.component._emit_recursive("load")
|
|
434
|
+
|
|
435
|
+
def unblock_frontend_update(comp):
|
|
436
|
+
comp._block_frontend_update = False
|
|
437
|
+
|
|
438
|
+
self.component._recurse(unblock_frontend_update, True, set())
|
|
439
|
+
if is_pyodide() and update_frontend:
|
|
440
|
+
import webapp_frontend
|
|
441
|
+
|
|
442
|
+
webapp_frontend.reload(
|
|
443
|
+
webapp_frontend.to_js(self.component._get_my_wrapper_props())
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
def load(self, data, load_local_storage=False, update_frontend=is_pyodide()):
|
|
447
|
+
"""Load app from stored data"""
|
|
448
|
+
self.component._namespace_id = ""
|
|
449
|
+
self.component._parent = self
|
|
450
|
+
self.component._status = self._status
|
|
451
|
+
self._namespace_id = ""
|
|
452
|
+
|
|
453
|
+
self.component._recurse(Component._calc_namespace_id, True, set())
|
|
454
|
+
|
|
455
|
+
component_data = data.get("component", {})
|
|
456
|
+
|
|
457
|
+
if self._default_data and "data" in component_data:
|
|
458
|
+
# we are hot-reloading the app, this means we skipped the default data when dumping before
|
|
459
|
+
# so we need to update the data with the default data
|
|
460
|
+
def update_props(data, default_data):
|
|
461
|
+
for key, value in default_data.items():
|
|
462
|
+
if not key in data:
|
|
463
|
+
data[key] = copy.deepcopy(value)
|
|
464
|
+
continue
|
|
465
|
+
if isinstance(value, dict):
|
|
466
|
+
update_props(value, default_data[key])
|
|
467
|
+
data[key] = copy.deepcopy(value)
|
|
468
|
+
|
|
469
|
+
self.update(
|
|
470
|
+
data,
|
|
471
|
+
load_local_storage=load_local_storage,
|
|
472
|
+
update_frontend=update_frontend,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
def report_context(self) -> tuple[dict, dict]:
|
|
476
|
+
"""
|
|
477
|
+
Returns the context and images for the report (docx, md).
|
|
478
|
+
"""
|
|
479
|
+
return ({}, {})
|
|
480
|
+
|
|
481
|
+
def load_asset(self, filename: str) -> str:
|
|
482
|
+
"""
|
|
483
|
+
Load an asset from the 'assets' directory within an application.
|
|
484
|
+
If the asset is an image, it will be returned as a data url.
|
|
485
|
+
"""
|
|
486
|
+
module = sys.modules[self.__module__.split(".")[0]]
|
|
487
|
+
return asset(filename, module=module)
|
|
488
|
+
|
|
489
|
+
def _get_file_url(self, name: str):
|
|
490
|
+
if "WEBAPP_JOB_ID" in os.environ:
|
|
491
|
+
id_ = int(os.environ["WEBAPP_JOB_ID"])
|
|
492
|
+
else:
|
|
493
|
+
id_ = self.metadata["id"]
|
|
494
|
+
url = "results" if isinstance(self, Report) else "files"
|
|
495
|
+
return os.environ["WEBAPP_API_URL"] + f"/{url}/{id_}/files/{name}"
|
|
496
|
+
|
|
497
|
+
def _load_data_file(self, name):
|
|
498
|
+
if is_pyodide():
|
|
499
|
+
http = urllib3.PoolManager()
|
|
500
|
+
response = http.request(
|
|
501
|
+
"GET",
|
|
502
|
+
self._get_file_url(name),
|
|
503
|
+
headers={"Authorization": f'{os.environ["WEBAPP_API_TOKEN"]}'},
|
|
504
|
+
)
|
|
505
|
+
return response.json()
|
|
506
|
+
return read_json(f"_data_files/{name}")
|
|
507
|
+
|
|
508
|
+
def _store_data_file(self, data, name):
|
|
509
|
+
# if is_pyodide():
|
|
510
|
+
if True:
|
|
511
|
+
id_ = self.metadata["id"]
|
|
512
|
+
api.post(
|
|
513
|
+
f"/files/{id_}/files/{name}",
|
|
514
|
+
data,
|
|
515
|
+
)
|
|
516
|
+
# else:
|
|
517
|
+
# os.makedirs("_data_files", exist_ok=True)
|
|
518
|
+
# import json
|
|
519
|
+
# write_json(json.dumps(data), f"_data_files/{name}")
|
|
520
|
+
|
|
521
|
+
def _save_storage_local(self):
|
|
522
|
+
self.component._save_storage_local()
|
|
523
|
+
|
|
524
|
+
def _load_storage_local(self):
|
|
525
|
+
self.component._load_storage_local()
|
|
526
|
+
|
|
527
|
+
def testing_data(self):
|
|
528
|
+
"""
|
|
529
|
+
Get data for testing purposes. User should implement this method in the app class.
|
|
530
|
+
Returns:
|
|
531
|
+
dict: Dictionary with filename, data and type keys
|
|
532
|
+
"""
|
|
533
|
+
app_data = self.dump()
|
|
534
|
+
app_data["component"].pop("storage", None)
|
|
535
|
+
return {
|
|
536
|
+
"filename": f"{self.name}.json",
|
|
537
|
+
"data": json.dumps(app_data),
|
|
538
|
+
"type": "application/json",
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
BaseModel = App
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def replace_app(new_app):
|
|
546
|
+
env = get_environment()
|
|
547
|
+
env.frontend.reset_app(new_app)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def reload_package(package_name):
|
|
551
|
+
"""Reload python package and all submodules (searches in modules for references to other submodules)"""
|
|
552
|
+
package = importlib.import_module(package_name)
|
|
553
|
+
assert hasattr(package, "__package__")
|
|
554
|
+
file_name = package.__file__
|
|
555
|
+
if file_name is None:
|
|
556
|
+
file_name = package.__path__[0]
|
|
557
|
+
package_dir = os.path.dirname(file_name) + os.sep
|
|
558
|
+
reloaded_modules = {file_name: package}
|
|
559
|
+
|
|
560
|
+
def reload_recursive(module):
|
|
561
|
+
t = time.time()
|
|
562
|
+
module = importlib.reload(module)
|
|
563
|
+
t = time.time() - t
|
|
564
|
+
|
|
565
|
+
if t > 0.1:
|
|
566
|
+
print(f"Reloaded module {module.__name__} in {1000*t:.0f} ms")
|
|
567
|
+
|
|
568
|
+
var_values = vars(module).values()
|
|
569
|
+
for var in var_values:
|
|
570
|
+
if isinstance(var, types.ModuleType):
|
|
571
|
+
file_name = getattr(var, "__file__", None)
|
|
572
|
+
if file_name is not None and file_name.startswith(package_dir):
|
|
573
|
+
if file_name not in reloaded_modules:
|
|
574
|
+
reloaded_modules[file_name] = reload_recursive(var)
|
|
575
|
+
|
|
576
|
+
return module
|
|
577
|
+
|
|
578
|
+
reload_recursive(package)
|
|
579
|
+
return reloaded_modules
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def loadModel(app_metadata, data, reload_python_modules=[], load_local_storage=False):
|
|
583
|
+
"""Load model from data"""
|
|
584
|
+
|
|
585
|
+
reloaded_modules = {}
|
|
586
|
+
for m in reload_python_modules:
|
|
587
|
+
# hot reloading of python module on client side
|
|
588
|
+
t = time.time()
|
|
589
|
+
reloaded_modules |= reload_package(m)
|
|
590
|
+
t = time.time() - t
|
|
591
|
+
print(f"Reloaded package {m} in {1000*t:.0f} ms")
|
|
592
|
+
|
|
593
|
+
module_name = app_metadata["python_class"].split(".")
|
|
594
|
+
module_name, class_name = ".".join(module_name[:-1]), module_name[-1]
|
|
595
|
+
if module_name in reloaded_modules:
|
|
596
|
+
module = reloaded_modules[module_name]
|
|
597
|
+
else:
|
|
598
|
+
module = importlib.import_module(module_name)
|
|
599
|
+
cls = getattr(module, class_name)
|
|
600
|
+
reset_components()
|
|
601
|
+
app = cls()
|
|
602
|
+
app._default_data = app.dump()["component"]
|
|
603
|
+
if not "metadata" in data:
|
|
604
|
+
data["metadata"] = {
|
|
605
|
+
"app_id": app_metadata["id"],
|
|
606
|
+
"python_class": app_metadata["python_class"],
|
|
607
|
+
}
|
|
608
|
+
app.load(data=data, load_local_storage=load_local_storage)
|
|
609
|
+
return app
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
_app_register = []
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def register_application(app):
|
|
616
|
+
"""Class decorator to register an application"""
|
|
617
|
+
_app_register.append(app)
|
|
618
|
+
return app
|
ngapp/basecomponent.py
ADDED
ngapp/cli/__init__.py
ADDED
|
File without changes
|