nova-trame 0.15.0__py3-none-any.whl → 0.16.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.
- nova/trame/view/components/visualization/__init__.py +2 -1
- nova/trame/view/components/visualization/matplotlib_figure.py +307 -0
- nova/trame/view/theme/assets/core_style.scss +39 -4
- nova/trame/view/theme/assets/vuetify_config.json +7 -6
- {nova_trame-0.15.0.dist-info → nova_trame-0.16.0.dist-info}/METADATA +5 -1
- {nova_trame-0.15.0.dist-info → nova_trame-0.16.0.dist-info}/RECORD +9 -8
- {nova_trame-0.15.0.dist-info → nova_trame-0.16.0.dist-info}/WHEEL +1 -1
- {nova_trame-0.15.0.dist-info → nova_trame-0.16.0.dist-info}/LICENSE +0 -0
- {nova_trame-0.15.0.dist-info → nova_trame-0.16.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,307 @@
|
|
1
|
+
"""View implementation for MatplotlibFigure."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
import socketserver
|
6
|
+
from asyncio import FIRST_COMPLETED, new_event_loop, set_event_loop, wait
|
7
|
+
from io import BytesIO
|
8
|
+
from mimetypes import types_map
|
9
|
+
from pathlib import Path
|
10
|
+
from threading import Thread
|
11
|
+
from typing import Any, Optional
|
12
|
+
|
13
|
+
import tornado
|
14
|
+
from aiohttp import ClientSession, WSMsgType, web
|
15
|
+
from matplotlib import get_data_path
|
16
|
+
from matplotlib.backends.backend_webagg import FigureManagerWebAgg, new_figure_manager_given_figure # type: ignore
|
17
|
+
from matplotlib.figure import Figure
|
18
|
+
from trame.app import get_server
|
19
|
+
from trame.widgets import client, html, matplotlib
|
20
|
+
from wslink.backends.aiohttp import WebAppServer
|
21
|
+
|
22
|
+
|
23
|
+
class _MPLApplication(tornado.web.Application):
|
24
|
+
"""Tornado application compatible with Matplotlib's WebAgg backend."""
|
25
|
+
|
26
|
+
class WebSocket(tornado.websocket.WebSocketHandler):
|
27
|
+
"""Implements the WebSocket manager for WebAgg."""
|
28
|
+
|
29
|
+
supports_binary = True
|
30
|
+
|
31
|
+
def check_origin(self, origin: Any) -> bool:
|
32
|
+
return True
|
33
|
+
|
34
|
+
def open(self, *args: Any, **kwargs: Any) -> None:
|
35
|
+
# Register the websocket with the FigureManager.
|
36
|
+
manager = self.application.manager # type: ignore
|
37
|
+
manager.add_web_socket(self)
|
38
|
+
if hasattr(self, "set_nodelay"):
|
39
|
+
self.set_nodelay(True)
|
40
|
+
|
41
|
+
def on_close(self) -> None:
|
42
|
+
# When the socket is closed, deregister the websocket with
|
43
|
+
# the FigureManager.
|
44
|
+
manager = self.application.manager # type: ignore
|
45
|
+
manager.remove_web_socket(self)
|
46
|
+
|
47
|
+
def on_message(self, message: Any) -> None:
|
48
|
+
# The 'supports_binary' message is relevant to the
|
49
|
+
# websocket itself. The other messages get passed along
|
50
|
+
# to matplotlib as-is.
|
51
|
+
|
52
|
+
# Every message has a "type" and a "figure_id".
|
53
|
+
message = json.loads(message)
|
54
|
+
if message["type"] == "supports_binary":
|
55
|
+
self.supports_binary = message["value"]
|
56
|
+
else:
|
57
|
+
manager = self.application.manager # type: ignore
|
58
|
+
manager.handle_json(message)
|
59
|
+
|
60
|
+
def send_json(self, content: Any) -> None:
|
61
|
+
set_event_loop(self.application.loop) # type: ignore
|
62
|
+
self.write_message(json.dumps(content))
|
63
|
+
|
64
|
+
def send_binary(self, blob: Any) -> None:
|
65
|
+
set_event_loop(self.application.loop) # type: ignore
|
66
|
+
if self.supports_binary:
|
67
|
+
self.write_message(blob, binary=True)
|
68
|
+
else:
|
69
|
+
data_uri = "data:image/png;base64," + blob.encode("base64").replace("\n", "")
|
70
|
+
self.write_message(data_uri)
|
71
|
+
|
72
|
+
def __init__(self, figure: Figure) -> None:
|
73
|
+
self.figure = figure
|
74
|
+
self.manager = new_figure_manager_given_figure(id(figure), figure)
|
75
|
+
self.loop = new_event_loop()
|
76
|
+
|
77
|
+
super().__init__([("/ws", self.WebSocket)])
|
78
|
+
|
79
|
+
|
80
|
+
class MatplotlibFigure(matplotlib.Figure):
|
81
|
+
"""Creates an interactive Matplotlib Figure using the WebAgg backend.
|
82
|
+
|
83
|
+
By default, this will leverage the built-in Trame widget for Matplotlib support. This built-in widget can display
|
84
|
+
poor performance for detailed plots due to it being locked into using SVG rendering.
|
85
|
+
|
86
|
+
If you experience this, then you can use the `webagg` parameter to enable the WebAgg backend for Matplotlib. This
|
87
|
+
will switch to server-side rendering leveraging the Anti-Grain Geometry engine.
|
88
|
+
|
89
|
+
.. code-block:: python
|
90
|
+
|
91
|
+
my_figure = matplotlib.figure.Figure()
|
92
|
+
MatplotlibFigure(my_figure) # Display SVG-based plot in Trame
|
93
|
+
MatplotlibFigure(my_figure, webagg=True) # Display WebAgg-based plot in Trame
|
94
|
+
"""
|
95
|
+
|
96
|
+
mpl_initialized = False
|
97
|
+
mpl_instances: dict[int, "MatplotlibFigure"] = {}
|
98
|
+
|
99
|
+
@classmethod
|
100
|
+
def _get_free_port(cls) -> int:
|
101
|
+
with socketserver.TCPServer(("localhost", 0), None) as s: # type: ignore
|
102
|
+
return s.server_address[1]
|
103
|
+
|
104
|
+
@classmethod
|
105
|
+
def _setup_mpl(cls) -> None:
|
106
|
+
server = get_server(None, client_type="vue3")
|
107
|
+
|
108
|
+
@server.controller.add("on_server_bind")
|
109
|
+
def _add_routes(wslink_server: WebAppServer) -> None:
|
110
|
+
# matplotlib WebAgg serves JS that will reference the base URL of the application, so we need to add
|
111
|
+
# endpoints to the main server to handle these requests.
|
112
|
+
wslink_server.app.add_routes(
|
113
|
+
[
|
114
|
+
web.get("/_images/{image}", cls._mpl_image_endpoint),
|
115
|
+
web.get("/download/{port}/{format}", cls._mpl_download_endpoint),
|
116
|
+
web.get("/mpl/{port}", cls._mpl_ws_proxy_endpoint),
|
117
|
+
]
|
118
|
+
)
|
119
|
+
|
120
|
+
# The CSS and JS files, on the other hand, can be preloaded into the page which is simpler.
|
121
|
+
css_path = Path(FigureManagerWebAgg.get_static_file_path(), "css")
|
122
|
+
for fname in os.listdir(css_path):
|
123
|
+
with open(Path(css_path, fname)) as css_file:
|
124
|
+
content = css_file.read()
|
125
|
+
client.Style(content)
|
126
|
+
client.Script(FigureManagerWebAgg.get_javascript())
|
127
|
+
|
128
|
+
MatplotlibFigure.mpl_initialized = True
|
129
|
+
|
130
|
+
@classmethod
|
131
|
+
async def _mpl_download_endpoint(cls, request: web.Request) -> web.Response:
|
132
|
+
# We use the websocket port to differentiate between plots if there are multiple WebAgg figures.
|
133
|
+
port = request.match_info.get("port", "")
|
134
|
+
format = request.match_info.get("format", "png")
|
135
|
+
|
136
|
+
if not port or int(port) not in MatplotlibFigure.mpl_instances:
|
137
|
+
raise web.HTTPNotFound
|
138
|
+
|
139
|
+
instance = MatplotlibFigure.mpl_instances[int(port)]
|
140
|
+
|
141
|
+
buff = BytesIO()
|
142
|
+
if instance._figure:
|
143
|
+
instance._figure.savefig(buff, format=format)
|
144
|
+
buff.seek(0)
|
145
|
+
|
146
|
+
return web.Response(body=buff.read(), headers={"Content-Type": types_map.get(format, "binary")})
|
147
|
+
|
148
|
+
@classmethod
|
149
|
+
async def _mpl_image_endpoint(cls, request: web.Request) -> web.Response:
|
150
|
+
image_name = request.match_info.get("image", None)
|
151
|
+
|
152
|
+
if image_name:
|
153
|
+
try:
|
154
|
+
with open(Path(get_data_path(), "images", image_name), mode="rb") as image_file:
|
155
|
+
image_data = image_file.read()
|
156
|
+
return web.Response(body=image_data)
|
157
|
+
except OSError as err:
|
158
|
+
raise web.HTTPNotFound from err
|
159
|
+
|
160
|
+
raise web.HTTPNotFound
|
161
|
+
|
162
|
+
@classmethod
|
163
|
+
async def _mpl_ws_proxy_endpoint(cls, request: web.Request) -> web.WebSocketResponse:
|
164
|
+
# The WebAgg backend assumes a tornado-based WebSocket handler, so we need to proxy it to work with Trame's
|
165
|
+
# aiohttp setup.
|
166
|
+
port = request.match_info.get("port", "")
|
167
|
+
|
168
|
+
# Initialize the proxy
|
169
|
+
ws_server = web.WebSocketResponse()
|
170
|
+
await ws_server.prepare(request)
|
171
|
+
|
172
|
+
# Connect to the tornado WebSocket handler
|
173
|
+
client_session = ClientSession(cookies=request.cookies)
|
174
|
+
async with client_session.ws_connect(
|
175
|
+
f"http://localhost:{port}/ws",
|
176
|
+
) as ws_client:
|
177
|
+
|
178
|
+
async def ws_forward(ws_from: Any, ws_to: Any) -> None:
|
179
|
+
# The browser will send text messages for rendering requests and the server will send bytes to transmit
|
180
|
+
# rendered image data.
|
181
|
+
async for msg in ws_from:
|
182
|
+
if msg.type == WSMsgType.TEXT:
|
183
|
+
await ws_to.send_str(msg.data)
|
184
|
+
elif msg.type == WSMsgType.BINARY:
|
185
|
+
await ws_to.send_bytes(msg.data)
|
186
|
+
else:
|
187
|
+
raise ValueError("unexpected message type: %s", print(msg))
|
188
|
+
|
189
|
+
# Forward websocket data in both directions
|
190
|
+
await wait(
|
191
|
+
[ws_forward(ws_server, ws_client), ws_forward(ws_client, ws_server)], return_when=FIRST_COMPLETED
|
192
|
+
)
|
193
|
+
await client_session.close() # Ensure the connection is cleaned up when the Trame client disconnects.
|
194
|
+
|
195
|
+
return ws_server
|
196
|
+
|
197
|
+
def __init__(self, figure: Optional[Figure] = None, webagg: bool = False, **kwargs: Any) -> None:
|
198
|
+
"""Creates a Matplotlib figure in the Trame UI.
|
199
|
+
|
200
|
+
Parameters
|
201
|
+
----------
|
202
|
+
figure : `altair.Chart <https://altair-viz.github.io/user_guide/generated/toplevel/altair.Chart.html#altair.Chart>`_
|
203
|
+
Altair chart object
|
204
|
+
webagg : bool
|
205
|
+
If true, then the WebAgg backend for Matplotlib is used. If not, then the default Trame matplotlib plugin
|
206
|
+
is used.
|
207
|
+
kwargs
|
208
|
+
Arguments to be passed to `AbstractElement <https://trame.readthedocs.io/en/latest/core.widget.html#trame_client.widgets.core.AbstractElement>`_
|
209
|
+
|
210
|
+
Returns
|
211
|
+
-------
|
212
|
+
None
|
213
|
+
"""
|
214
|
+
self._webagg = webagg
|
215
|
+
if webagg:
|
216
|
+
self._port = MatplotlibFigure._get_free_port()
|
217
|
+
if "classes" in kwargs:
|
218
|
+
kwargs["classes"] += " nova-mpl"
|
219
|
+
else:
|
220
|
+
kwargs["classes"] = "nova-mpl"
|
221
|
+
|
222
|
+
html.Div(id=f"nova_mpl_{self._port}", **kwargs)
|
223
|
+
|
224
|
+
self._server = get_server(None, client_type="vue3")
|
225
|
+
|
226
|
+
self._figure = figure
|
227
|
+
self._initialized = False
|
228
|
+
|
229
|
+
if not MatplotlibFigure.mpl_initialized:
|
230
|
+
MatplotlibFigure._setup_mpl()
|
231
|
+
MatplotlibFigure.mpl_instances[self._port] = self
|
232
|
+
|
233
|
+
self.update()
|
234
|
+
else:
|
235
|
+
super().__init__(figure, **kwargs)
|
236
|
+
|
237
|
+
def update(self, figure: Optional[Figure] = None) -> None:
|
238
|
+
if self._webagg:
|
239
|
+
if figure:
|
240
|
+
self._figure = figure
|
241
|
+
|
242
|
+
if self._figure is not None and not self._initialized:
|
243
|
+
self._setup_figure_websocket()
|
244
|
+
self._setup_figure_javascript()
|
245
|
+
|
246
|
+
self._initialized = True
|
247
|
+
|
248
|
+
# Re-render the figure in the UI
|
249
|
+
if self._figure is not None:
|
250
|
+
self._figure.canvas.draw_idle()
|
251
|
+
self._figure.canvas.flush_events()
|
252
|
+
else:
|
253
|
+
super().update(figure)
|
254
|
+
|
255
|
+
def _setup_figure_websocket(self) -> None:
|
256
|
+
thread = Thread(target=self._mpl_run_ws_server, daemon=True)
|
257
|
+
thread.start()
|
258
|
+
|
259
|
+
def _setup_figure_javascript(self) -> None:
|
260
|
+
figure_js = """
|
261
|
+
function ondownload_%(port)d(figure, format) {
|
262
|
+
window.open('download/%(port)d/' + format, '_blank');
|
263
|
+
};
|
264
|
+
|
265
|
+
function ready_%(port)d(fn) {
|
266
|
+
if (document.getElementById("nova_mpl_%(port)d") === null) {
|
267
|
+
setTimeout(() => {
|
268
|
+
ready_%(port)d(fn);
|
269
|
+
}, 100);
|
270
|
+
} else {
|
271
|
+
fn();
|
272
|
+
}
|
273
|
+
}
|
274
|
+
|
275
|
+
ready_%(port)d(
|
276
|
+
function() {
|
277
|
+
var websocket_type = mpl.get_websocket_type();
|
278
|
+
var websocket = new websocket_type(`ws://${window.location.host}/mpl/%(port)d`);
|
279
|
+
|
280
|
+
var fig = new mpl.figure(
|
281
|
+
// A unique numeric identifier for the figure
|
282
|
+
%(port)d,
|
283
|
+
// A websocket object (or something that behaves like one)
|
284
|
+
websocket,
|
285
|
+
// A function called when a file type is selected for download
|
286
|
+
ondownload_%(port)d,
|
287
|
+
// The HTML element in which to place the figure
|
288
|
+
document.getElementById("nova_mpl_%(port)d")
|
289
|
+
);
|
290
|
+
}
|
291
|
+
);
|
292
|
+
"""
|
293
|
+
|
294
|
+
client.Script(figure_js % {"port": self._port}) # TODO
|
295
|
+
|
296
|
+
def _mpl_run_ws_server(self) -> None:
|
297
|
+
if not self._figure:
|
298
|
+
return
|
299
|
+
|
300
|
+
application = _MPLApplication(self._figure)
|
301
|
+
|
302
|
+
http_server = tornado.httpserver.HTTPServer(application)
|
303
|
+
sockets = tornado.netutil.bind_sockets(self._port, "")
|
304
|
+
http_server.add_sockets(sockets)
|
305
|
+
|
306
|
+
ioloop = tornado.ioloop.IOLoop.instance()
|
307
|
+
ioloop.start()
|
@@ -4,10 +4,22 @@ html {
|
|
4
4
|
|
5
5
|
.d-grid {
|
6
6
|
display: grid;
|
7
|
-
gap: 0.
|
7
|
+
gap: 0.8em;
|
8
8
|
grid-auto-rows: auto;
|
9
9
|
}
|
10
10
|
|
11
|
+
.v-toolbar {
|
12
|
+
box-shadow: none !important;
|
13
|
+
}
|
14
|
+
|
15
|
+
.mpl-message, .ui-dialog-titlebar {
|
16
|
+
display: none !important;
|
17
|
+
}
|
18
|
+
|
19
|
+
.nova-mpl * {
|
20
|
+
resize: none !important;
|
21
|
+
}
|
22
|
+
|
11
23
|
@media only screen and (max-width: 959px) {
|
12
24
|
.d-grid {
|
13
25
|
grid-template-columns: repeat(1, 1fr) !important;
|
@@ -20,7 +32,15 @@ html {
|
|
20
32
|
|
21
33
|
/* Global font sizes can't be set through the Vuetify configuration. */
|
22
34
|
.v-theme--CompactTheme {
|
23
|
-
font-size: 0.
|
35
|
+
font-size: 0.75rem;
|
36
|
+
|
37
|
+
&.v-btn--variant-elevated.bg-primary {
|
38
|
+
box-shadow: none !important;
|
39
|
+
background-color: transparent !important;
|
40
|
+
color: green !important;
|
41
|
+
border: solid 1px lightgrey;
|
42
|
+
border-radius: 4px;
|
43
|
+
}
|
24
44
|
|
25
45
|
.v-card-title,
|
26
46
|
.v-list-item-title,
|
@@ -29,7 +49,22 @@ html {
|
|
29
49
|
}
|
30
50
|
|
31
51
|
.v-btn {
|
32
|
-
min-width: 0px;
|
33
|
-
padding:
|
52
|
+
min-width: 0px !important;
|
53
|
+
padding: 5px 5px !important;
|
54
|
+
box-shadow: none !important;
|
55
|
+
}
|
56
|
+
|
57
|
+
.v-btn__content {
|
58
|
+
text-transform: none;
|
59
|
+
}
|
60
|
+
|
61
|
+
.v-label {
|
62
|
+
font-size: 0.75rem;
|
63
|
+
}
|
64
|
+
|
65
|
+
.v-tab {
|
66
|
+
height: 30px !important;
|
67
|
+
min-width: fit-content !important;
|
68
|
+
padding: 10px !important;
|
34
69
|
}
|
35
70
|
}
|
@@ -152,14 +152,11 @@
|
|
152
152
|
"secondary": "#f48e5c"
|
153
153
|
},
|
154
154
|
"defaults": {
|
155
|
-
"global": {
|
156
|
-
"density": "compact"
|
157
|
-
},
|
158
155
|
"VBadge": {
|
159
156
|
"dot": true
|
160
157
|
},
|
161
158
|
"VBtn": {
|
162
|
-
"
|
159
|
+
"size": "small"
|
163
160
|
},
|
164
161
|
"VTextField": {
|
165
162
|
"VBtn": {
|
@@ -172,9 +169,13 @@
|
|
172
169
|
}
|
173
170
|
},
|
174
171
|
"variations": {
|
175
|
-
"colors": [
|
172
|
+
"colors": [
|
173
|
+
"primary",
|
174
|
+
"secondary",
|
175
|
+
"accent"
|
176
|
+
],
|
176
177
|
"darken": 5,
|
177
178
|
"lighten": 5
|
178
179
|
}
|
179
180
|
}
|
180
|
-
}
|
181
|
+
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: nova-trame
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.16.0
|
4
4
|
Summary: A Python Package for injecting curated themes and custom components into Trame applications
|
5
5
|
License: MIT
|
6
6
|
Keywords: NDIP,Python,Trame,Vuetify
|
@@ -13,12 +13,16 @@ Classifier: Programming Language :: Python :: 3.10
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
16
|
+
Requires-Dist: altair
|
16
17
|
Requires-Dist: libsass
|
17
18
|
Requires-Dist: mergedeep
|
18
19
|
Requires-Dist: nova-mvvm
|
19
20
|
Requires-Dist: pydantic
|
20
21
|
Requires-Dist: tomli
|
22
|
+
Requires-Dist: tornado
|
21
23
|
Requires-Dist: trame
|
24
|
+
Requires-Dist: trame-matplotlib
|
25
|
+
Requires-Dist: trame-plotly
|
22
26
|
Requires-Dist: trame-vega
|
23
27
|
Requires-Dist: trame-vuetify
|
24
28
|
Description-Content-Type: text/markdown
|
@@ -4,24 +4,25 @@ nova/trame/model/remote_file_input.py,sha256=9KAf31ZHzpsh_aXUrNcF81Q5jvUZDWCzW1Q
|
|
4
4
|
nova/trame/view/components/__init__.py,sha256=fopr6mVqcpDcVYK9ue7SLUHyswgvRPcFESTq86mu1R8,128
|
5
5
|
nova/trame/view/components/input_field.py,sha256=8q18MyegDl0ni2Wpb6TwvTJ4UYoKSNINq9TilE5EnbE,14956
|
6
6
|
nova/trame/view/components/remote_file_input.py,sha256=k2yrwkell_g0sGnWR9XLL1LxkmFLr8-AGhduo8E-N4A,8669
|
7
|
-
nova/trame/view/components/visualization/__init__.py,sha256=
|
7
|
+
nova/trame/view/components/visualization/__init__.py,sha256=reqkkbhD5uSksHHlhVMy1qNUCwSekS5HlXk6wCREYxU,152
|
8
8
|
nova/trame/view/components/visualization/interactive_2d_plot.py,sha256=foZCMoqbuahT5dtqIQvm8C4ZJcY9P211eJEcpQJltmM,3421
|
9
|
+
nova/trame/view/components/visualization/matplotlib_figure.py,sha256=yop7Kd_MylUiCwEial2jOYESbvchrYhrpSmRowUhePY,12003
|
9
10
|
nova/trame/view/layouts/__init__.py,sha256=cMrlB5YMUoK8EGB83b34UU0kPTVrH8AxsYvKRtpUNEc,141
|
10
11
|
nova/trame/view/layouts/grid.py,sha256=k-QHuH31XeAVDuMKUMoAMVnAM-Yavq7kdLYOC1ZrGTQ,5021
|
11
12
|
nova/trame/view/layouts/hbox.py,sha256=r5irhFX6YWTWN4V4NwNQx6mheyM8p6PVcJbrbhvOAwo,2625
|
12
13
|
nova/trame/view/layouts/vbox.py,sha256=Q4EvrtGJORyNF6AnCLGXToy8XU6yofiO5_kt7hK-AYs,2626
|
13
14
|
nova/trame/view/theme/__init__.py,sha256=70_marDlTigIcPEOGiJb2JTs-8b2sGM5SlY7XBPtBDM,54
|
14
|
-
nova/trame/view/theme/assets/core_style.scss,sha256=
|
15
|
+
nova/trame/view/theme/assets/core_style.scss,sha256=_RUO_WnxsteRXftfLQiql0TLh_9Vx93bxLeUBu8Xp3M,1284
|
15
16
|
nova/trame/view/theme/assets/favicon.png,sha256=Xbp1nUmhcBDeObjsebEbEAraPDZ_M163M_ZLtm5AbQc,1927
|
16
17
|
nova/trame/view/theme/assets/js/delay_manager.js,sha256=vmb34DZ5YCQIlRW9Tf2M_uvJW6HFCmtlKZ5e_TPR8yg,536
|
17
18
|
nova/trame/view/theme/assets/js/lodash.debounce.min.js,sha256=GLzlQH04WDUNYN7i39ttHHejSdu-CpAvfWgDgKDn-OY,4448
|
18
19
|
nova/trame/view/theme/assets/js/lodash.throttle.min.js,sha256=9csqjX-M-LVGJnF3z4ha1R_36O5AfkFE8rPHkxmt3tE,4677
|
19
|
-
nova/trame/view/theme/assets/vuetify_config.json,sha256=
|
20
|
+
nova/trame/view/theme/assets/vuetify_config.json,sha256=SQOPcwuAYTgYfkhyC1eB1TA8VtnMWZe7-P2ItJkOrX0,4736
|
20
21
|
nova/trame/view/theme/theme.py,sha256=18tHChrB1_qosjIiiLxMwWYRDTdXfgxEP4-4yJ8E3Cw,11893
|
21
22
|
nova/trame/view/utilities/local_storage.py,sha256=vD8f2VZIpxhIKjZwEaD7siiPCTZO4cw9AfhwdawwYLY,3218
|
22
23
|
nova/trame/view_model/remote_file_input.py,sha256=WHWCQkZBGeKLe1aTPbtVNI8tn-PDt64mi1-561uuBpQ,3320
|
23
|
-
nova_trame-0.
|
24
|
-
nova_trame-0.
|
25
|
-
nova_trame-0.
|
26
|
-
nova_trame-0.
|
27
|
-
nova_trame-0.
|
24
|
+
nova_trame-0.16.0.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
|
25
|
+
nova_trame-0.16.0.dist-info/METADATA,sha256=-lnKJubLE96B49GZ567TOGxbxeNwqYX_R9KLaBo0FuY,1345
|
26
|
+
nova_trame-0.16.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
27
|
+
nova_trame-0.16.0.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
|
28
|
+
nova_trame-0.16.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|