nova-trame 0.13.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,262 @@
1
+ """Implementation of ThemedApp."""
2
+
3
+ import json
4
+ import logging
5
+ from asyncio import create_task
6
+ from functools import partial
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ import sass
11
+ from mergedeep import Strategy, merge
12
+ from trame.app import get_server
13
+ from trame.assets.local import LocalFileManager
14
+ from trame.ui.vuetify3 import VAppLayout
15
+ from trame.widgets import client
16
+ from trame.widgets import vuetify3 as vuetify
17
+ from trame_client.widgets import html
18
+ from trame_server.core import Server
19
+ from trame_server.state import State
20
+
21
+ from nova.mvvm.pydantic_utils import validate_pydantic_parameter
22
+ from nova.trame.view.utilities.local_storage import LocalStorageManager
23
+
24
+ THEME_PATH = Path(__file__).parent
25
+
26
+ logger = logging.getLogger(__name__)
27
+ logger.setLevel(logging.INFO)
28
+
29
+
30
+ class ThemedApp:
31
+ """Automatically injects theming into your Trame application.
32
+
33
+ You should always inherit from this class when you define your Trame application.
34
+
35
+ Currently, it supports two themes:
36
+
37
+ 1. ModernTheme - The recommended theme for most applications. Leverages ORNL brand colors and a typical Vuetify \
38
+ appearance.
39
+ 2. CompactTheme - Similar to ModernTheme but with a smaller global font size and reduced margins and paddings on \
40
+ all components.
41
+ """
42
+
43
+ def __init__(
44
+ self, layout: str = "default", server: Server = None, vuetify_config_overrides: Optional[dict] = None
45
+ ) -> None:
46
+ """Constructor for the ThemedApp class.
47
+
48
+ Parameters
49
+ ----------
50
+ layout : str
51
+ The layout to use. Current options are: :code:`default` and :code:`two-column`
52
+ server : `trame_server.core.Server \
53
+ <https://trame.readthedocs.io/en/latest/core.server.html#trame_server.core.Server>`_, optional
54
+ The Trame server to use. If not provided, a new server will be created.
55
+ vuetify_config_overrides : dict, optional
56
+ `Vuetify Configuration <https://vuetifyjs.com/en/features/global-configuration/>`__
57
+ that will override anything set in our default configuration. You should only use this if you don't want to
58
+ use one of our predefined themes. If you just want to set your color palette without providing a full
59
+ Vuetify configuration, then you can set use the following to only set the color palette used by our
60
+ :code:`ModernTheme`:
61
+
62
+ .. code-block:: json
63
+
64
+ {
65
+ "primary": "#f00",
66
+ "secondary": "#0f0",
67
+ "accent": "#00f",
68
+ }
69
+
70
+ Returns
71
+ -------
72
+ None
73
+ """
74
+ self.server = get_server(server, client_type="vue3")
75
+ self.local_storage: Optional[LocalStorageManager] = None
76
+ if vuetify_config_overrides is None:
77
+ vuetify_config_overrides = {}
78
+
79
+ self.css = None
80
+ try:
81
+ with open(THEME_PATH / "assets" / "core_style.scss", "r") as scss_file:
82
+ self.css = sass.compile(string=scss_file.read())
83
+ except Exception as e:
84
+ logger.warning("Could not load base scss stylesheet.")
85
+ logger.error(e)
86
+
87
+ theme_path = THEME_PATH / "assets" / "vuetify_config.json"
88
+ try:
89
+ with open(theme_path, "r") as vuetify_config:
90
+ self.vuetify_config = json.load(vuetify_config)
91
+
92
+ merge(
93
+ self.vuetify_config,
94
+ vuetify_config_overrides,
95
+ strategy=Strategy.REPLACE,
96
+ )
97
+ except Exception as e:
98
+ logger.warning(f"Could not load vuetify config from {theme_path}.")
99
+ logger.error(e)
100
+ for shortcut in ["primary", "secondary", "accent"]:
101
+ if shortcut in self.vuetify_config:
102
+ self.vuetify_config["theme"]["themes"]["ModernTheme"]["colors"][shortcut] = self.vuetify_config[
103
+ shortcut
104
+ ]
105
+
106
+ # Since this is only intended for theming Trame apps, I don't think we need to invoke the MVVM framework here,
107
+ # and working directly with the Trame state makes this easier for me to manage.
108
+ self.state.nova__menu = False
109
+ self.state.nova__defaults = self.vuetify_config["theme"]["themes"]["ModernTheme"].get("defaults", {})
110
+ self.state.nova__theme = "ModernTheme"
111
+ self.state.trame__favicon = LocalFileManager(__file__).url("favicon", "./assets/favicon.png")
112
+
113
+ @property
114
+ def state(self) -> State:
115
+ return self.server.state
116
+
117
+ def init_mantid(self) -> None:
118
+ """Initializes MantidManager.
119
+
120
+ This doesn't happen by default because Mantid is a large dependency.
121
+ """
122
+ pass
123
+
124
+ async def _init_theme(self) -> None:
125
+ if self.local_storage:
126
+ theme = await self.local_storage.get("nova__theme")
127
+ if theme and theme in self.vuetify_config["theme"]["themes"]:
128
+ self.set_theme(theme, False)
129
+
130
+ async def init_theme(self) -> None:
131
+ create_task(self._init_theme())
132
+
133
+ def set_theme(self, theme: Optional[str], force: bool = True) -> None:
134
+ """Sets the theme of the application.
135
+
136
+ Parameters
137
+ ----------
138
+ theme : str, optional
139
+ The new theme to use. If the theme is not found, the default theme will be used.
140
+ force : bool, optional
141
+ If True, the theme will be set even if the theme selection menu is disabled.
142
+
143
+ Returns
144
+ -------
145
+ None
146
+ """
147
+ if theme not in self.vuetify_config["theme"]["themes"]:
148
+ raise ValueError(
149
+ f"Theme '{theme}' not found in the Vuetify configuration. "
150
+ "For a list of available themes, please visit "
151
+ "https://nova-application-development.readthedocs.io/en/stable/api.html#nova.trame.ThemedApp."
152
+ )
153
+
154
+ # I set force to True by default as I want the user to be able to say self.set_theme('MyTheme')
155
+ # while still blocking theme.py calls to set_theme if the selection menu is disabled.
156
+ if self.state.nova__menu or force:
157
+ with self.state:
158
+ self.state.nova__defaults = self.vuetify_config["theme"]["themes"].get(theme, {}).get("defaults", {})
159
+ self.state.nova__theme = theme
160
+
161
+ # We only want to sync to localStorage if the user is selecting and we want to preserve the selection.
162
+ if self.state.nova__menu and self.local_storage:
163
+ self.local_storage.set("nova__theme", theme)
164
+
165
+ def create_ui(self) -> VAppLayout:
166
+ """Creates the base UI into which you will inject your content.
167
+
168
+ You should always call this method from your application class that inherits from :code:`ThemedApp`.
169
+
170
+ Returns
171
+ -------
172
+ `trame_client.ui.core.AbstractLayout <https://trame.readthedocs.io/en/latest/core.ui.html#trame_client.ui.core.AbstractLayout>`_
173
+ """
174
+ with VAppLayout(self.server, vuetify_config=self.vuetify_config) as layout:
175
+ self.local_storage = LocalStorageManager(self.server.controller)
176
+
177
+ client.ClientTriggers(mounted=self.init_theme)
178
+ client.Style(self.css)
179
+
180
+ with vuetify.VDefaultsProvider(defaults=("nova__defaults",)) as defaults:
181
+ layout.defaults = defaults
182
+
183
+ with vuetify.VThemeProvider(theme=("nova__theme",)) as theme:
184
+ layout.theme = theme
185
+
186
+ with vuetify.VAppBar() as toolbar:
187
+ layout.toolbar = toolbar
188
+
189
+ with vuetify.VAppBarTitle() as toolbar_title:
190
+ layout.toolbar_title = toolbar_title
191
+ vuetify.VSpacer()
192
+ with html.Div(classes="mr-2") as actions:
193
+ layout.actions = actions
194
+
195
+ with vuetify.VMenu(
196
+ v_if="nova__menu",
197
+ close_delay=10000,
198
+ open_on_hover=True,
199
+ ) as theme_menu:
200
+ layout.theme_menu = theme_menu
201
+
202
+ with vuetify.Template(v_slot_activator="{ props }"):
203
+ vuetify.VBtn(
204
+ v_bind="props",
205
+ classes="mr-2",
206
+ icon="mdi-brush-variant",
207
+ )
208
+
209
+ with vuetify.VList(width=200):
210
+ vuetify.VListSubheader("Select Theme")
211
+ vuetify.VDivider()
212
+
213
+ for theme in self.vuetify_config.get("theme", {}).get("themes", {}).values():
214
+ with vuetify.VListItem(click=partial(self.set_theme, theme["value"])):
215
+ vuetify.VListItemTitle(theme["title"])
216
+ vuetify.VListItemSubtitle(
217
+ "Selected",
218
+ v_if=f"nova__theme === '{theme['value']}'",
219
+ )
220
+
221
+ with vuetify.VMain(classes="align-stretch d-flex flex-column h-screen"):
222
+ # [slot override example]
223
+ layout.pre_content = vuetify.VSheet(classes="bg-background ")
224
+ # [slot override example complete]
225
+ with vuetify.VContainer(classes="flex-1-1 overflow-hidden pt-0 pb-0", fluid=True):
226
+ layout.content = html.Div(classes="h-100 overflow-y-auto pb-1 ")
227
+ layout.post_content = vuetify.VSheet(classes="bg-background ")
228
+
229
+ with vuetify.VFooter(
230
+ app=True,
231
+ classes="my-0 px-1 py-0 text-center justify-center",
232
+ border=True,
233
+ ) as footer:
234
+ layout.footer = footer
235
+
236
+ vuetify.VProgressCircular(
237
+ classes="mr-1",
238
+ color="primary",
239
+ indeterminate=("!!galaxy_running",),
240
+ size=16,
241
+ width=3,
242
+ )
243
+ html.A(
244
+ "Powered by Calvera",
245
+ classes="text-grey-lighten-1 text-caption text-decoration-none",
246
+ href=("galaxy_url",),
247
+ target="_blank",
248
+ )
249
+ vuetify.VSpacer()
250
+ footer.add_child(
251
+ '<a href="https://www.ornl.gov/" '
252
+ 'class="text-grey-lighten-1 text-caption text-decoration-none" '
253
+ 'target="_blank">© 2024 ORNL</a>'
254
+ )
255
+
256
+ @self.server.controller.trigger("validate_pydantic_field")
257
+ def validate_pydantic_field(name: str, value: str, index: int) -> bool:
258
+ if "[index]" in name:
259
+ name = name.replace("[index]", f"[{str(index)}]")
260
+ return validate_pydantic_parameter(name, value)
261
+
262
+ return layout
@@ -0,0 +1,102 @@
1
+ """Implementation of LocalStorageManager."""
2
+
3
+ from asyncio import sleep
4
+ from typing import Any, Optional
5
+
6
+ from trame.widgets import client
7
+ from trame_server.core import Controller
8
+
9
+
10
+ class LocalStorageManager:
11
+ """Allows manipulation of window.localStorage from your Python code.
12
+
13
+ LocalStorageManager requires a Trame layout to exist in order to work properly. Because of this, it's strongly
14
+ recommended that you don't use this class directly. Instead, ThemedApp automatically creates an instance of this
15
+ class and stores it in ThemedApp.local_storage, through which it can safely be used.
16
+ """
17
+
18
+ def __init__(self, ctrl: Controller) -> None:
19
+ """Constructor for the LocalStorageManager class.
20
+
21
+ Parameters
22
+ ----------
23
+ ctrl : `trame_server.core.Controller <https://trame.readthedocs.io/en/latest/core.controller.html#trame_server.controller.Controller>`_
24
+ The Trame controller.
25
+
26
+ Returns
27
+ -------
28
+ None
29
+ """
30
+ self.js_get = client.JSEval(
31
+ exec=(
32
+ "window.trame.trigger("
33
+ " 'nova__local_storage_trigger', "
34
+ " [$event.key, window.localStorage.getItem($event.key)]"
35
+ ");"
36
+ )
37
+ ).exec
38
+ self.js_remove = client.JSEval(exec="window.localStorage.removeItem($event.key);").exec
39
+ self.js_set = client.JSEval(exec="window.localStorage.setItem($event.key, $event.value);").exec
40
+
41
+ self._ready: dict[str, bool] = {}
42
+ self._values: dict[str, str] = {}
43
+
44
+ @ctrl.trigger("nova__local_storage_trigger")
45
+ def _(key: str, value: str) -> None:
46
+ self._ready[key] = True
47
+ self._values[key] = value
48
+
49
+ async def get(self, key: str) -> Optional[str]:
50
+ """Gets the value of a key from window.localStorage.
51
+
52
+ You cannot call this from the main Trame coroutine because this waits on a response from the browser that must
53
+ be processed by the main coroutine. Instead, you should call this from another thread or coroutine, typically
54
+ with :code:`asyncio.create_task`.
55
+
56
+ Parameters
57
+ ----------
58
+ key : str
59
+ The key to get the value of.
60
+
61
+ Returns
62
+ -------
63
+ Optional[str]
64
+ The value of the key from window.localStorage.
65
+ """
66
+ self._ready[key] = False
67
+ self.js_get({"key": key})
68
+
69
+ while not self._ready[key]:
70
+ await sleep(0.1)
71
+
72
+ return self._values[key]
73
+
74
+ def remove(self, key: str) -> None:
75
+ """Removes a key from window.localStorage.
76
+
77
+ Parameters
78
+ ----------
79
+ key : str
80
+ The key to remove.
81
+
82
+ Returns
83
+ -------
84
+ None
85
+ """
86
+ self.js_remove({"key": key})
87
+
88
+ def set(self, key: str, value: Any) -> None:
89
+ """Sets the value of a key in window.localStorage.
90
+
91
+ Parameters
92
+ ----------
93
+ key : str
94
+ The key to set the value of.
95
+ value : typing.Any
96
+ The value to set. This value will be coerced to a string before being stored.
97
+
98
+ Returns
99
+ -------
100
+ None
101
+ """
102
+ self.js_set({"key": key, "value": value})
@@ -0,0 +1,93 @@
1
+ """View model for RemoteFileInput."""
2
+
3
+ from typing import Any, Union
4
+
5
+ from nova.mvvm.interface import BindingInterface
6
+ from nova.trame.model.remote_file_input import RemoteFileInputModel
7
+
8
+
9
+ class RemoteFileInputViewModel:
10
+ """Manages the view state of RemoteFileInput."""
11
+
12
+ counter = 0
13
+
14
+ def __init__(self, model: RemoteFileInputModel, binding: BindingInterface) -> None:
15
+ """Creates a new RemoteFileInputViewModel."""
16
+ self.model = model
17
+
18
+ # Needed to keep state variables separated if this class is instantiated multiple times.
19
+ self.id = RemoteFileInputViewModel.counter
20
+ RemoteFileInputViewModel.counter += 1
21
+
22
+ self.showing_all_files = False
23
+ self.showing_base_paths = True
24
+ self.previous_value = ""
25
+ self.value = ""
26
+ self.dialog_bind = binding.new_bind()
27
+ self.file_list_bind = binding.new_bind()
28
+ self.filter_bind = binding.new_bind()
29
+ self.showing_all_bind = binding.new_bind()
30
+ self.valid_selection_bind = binding.new_bind()
31
+ self.on_close_bind = binding.new_bind()
32
+ self.on_update_bind = binding.new_bind()
33
+
34
+ def open_dialog(self) -> None:
35
+ self.previous_value = self.value
36
+ self.populate_file_list()
37
+
38
+ def close_dialog(self, cancel: bool = False) -> None:
39
+ if not cancel:
40
+ self.on_update_bind.update_in_view(self.value)
41
+ else:
42
+ self.value = self.previous_value
43
+
44
+ self.filter_bind.update_in_view(self.value)
45
+ self.on_close_bind.update_in_view(None)
46
+
47
+ def filter_paths(self, filter: str) -> None:
48
+ self.populate_file_list(filter)
49
+
50
+ def get_dialog_state_name(self) -> str:
51
+ return f"nova__dialog_{self.id}"
52
+
53
+ def get_file_list_state_name(self) -> str:
54
+ return f"nova__file_list_{self.id}"
55
+
56
+ def get_filter_state_name(self) -> str:
57
+ return f"nova__filter_{self.id}"
58
+
59
+ def get_showing_all_state_name(self) -> str:
60
+ return f"nova__showing_all_{self.id}"
61
+
62
+ def get_valid_selection_state_name(self) -> str:
63
+ return f"nova__valid_selection_{self.id}"
64
+
65
+ def init_view(self) -> None:
66
+ self.dialog_bind.update_in_view(False)
67
+ self.valid_selection_bind.update_in_view(False)
68
+ self.showing_all_bind.update_in_view(self.showing_all_files)
69
+
70
+ def set_value(self, value: str) -> None:
71
+ self.value = value
72
+
73
+ def toggle_showing_all_files(self) -> None:
74
+ self.showing_all_files = not self.showing_all_files
75
+ self.showing_all_bind.update_in_view(self.showing_all_files)
76
+ self.populate_file_list()
77
+
78
+ def populate_file_list(self, filter: str = "") -> None:
79
+ files = self.scan_current_path(filter)
80
+ self.file_list_bind.update_in_view(files)
81
+
82
+ def scan_current_path(self, filter: str) -> list[dict[str, Any]]:
83
+ files, self.showing_base_paths = self.model.scan_current_path(self.value, self.showing_all_files, filter)
84
+
85
+ return files
86
+
87
+ def select_file(self, file: Union[dict[str, str], str]) -> None:
88
+ new_path = self.model.select_file(file, self.value, self.showing_base_paths)
89
+ self.set_value(new_path)
90
+ self.filter_bind.update_in_view(self.value)
91
+
92
+ self.valid_selection_bind.update_in_view(self.model.valid_selection(new_path))
93
+ self.populate_file_list()
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 ONRL
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.3
2
+ Name: nova-trame
3
+ Version: 0.13.1
4
+ Summary: A Python Package for injecting curated themes and custom components into Trame applications
5
+ License: MIT
6
+ Keywords: NDIP,Python,Trame,Vuetify
7
+ Author: Duggan, John
8
+ Author-email: dugganjw@ornl.gov
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Dist: libsass
17
+ Requires-Dist: mergedeep
18
+ Requires-Dist: nova-mvvm
19
+ Requires-Dist: pydantic
20
+ Requires-Dist: tomli
21
+ Requires-Dist: trame
22
+ Requires-Dist: trame-vega
23
+ Requires-Dist: trame-vuetify
24
+ Description-Content-Type: text/markdown
25
+
26
+ nova-trame
27
+ ==========
28
+
29
+ `nova-trame` is a Python package for streamlining development of Trame applications used in the NOVA project.
30
+
31
+ You can install this package directly with
32
+
33
+ ```commandline
34
+ pip install nova-trame
35
+ ```
36
+
37
+ A user guide, examples, and a full API for this package can be found at https://nova-application-development.readthedocs.io/en/stable/.
38
+
39
+ Developers: please read [this document](DEVELOPMENT.md)
40
+
@@ -0,0 +1,24 @@
1
+ nova/__init__.py,sha256=ED6jHcYiuYpr_0vjGz0zx2lrrmJT9sDJCzIljoDfmlM,65
2
+ nova/trame/__init__.py,sha256=gFrAg1qva5PIqR5TjvPzAxLx103IKipJLqp3XXvrQL8,59
3
+ nova/trame/model/remote_file_input.py,sha256=9KAf31ZHzpsh_aXUrNcF81Q5jvUZDWCzW1QATKls-Jk,3675
4
+ nova/trame/view/components/__init__.py,sha256=fopr6mVqcpDcVYK9ue7SLUHyswgvRPcFESTq86mu1R8,128
5
+ nova/trame/view/components/input_field.py,sha256=YyDJJKHBsAdLN-GFOhXEpwNs5aJHb0BBbstcQ6nbAQE,12680
6
+ nova/trame/view/components/remote_file_input.py,sha256=k2yrwkell_g0sGnWR9XLL1LxkmFLr8-AGhduo8E-N4A,8669
7
+ nova/trame/view/components/visualization/__init__.py,sha256=kDX1fkbtAgXSGlqhlMNhYYoYrq-hfS636smjgLsh6gg,84
8
+ nova/trame/view/components/visualization/interactive_2d_plot.py,sha256=foZCMoqbuahT5dtqIQvm8C4ZJcY9P211eJEcpQJltmM,3421
9
+ nova/trame/view/layouts/__init__.py,sha256=cMrlB5YMUoK8EGB83b34UU0kPTVrH8AxsYvKRtpUNEc,141
10
+ nova/trame/view/layouts/grid.py,sha256=k-QHuH31XeAVDuMKUMoAMVnAM-Yavq7kdLYOC1ZrGTQ,5021
11
+ nova/trame/view/layouts/hbox.py,sha256=r5irhFX6YWTWN4V4NwNQx6mheyM8p6PVcJbrbhvOAwo,2625
12
+ nova/trame/view/layouts/vbox.py,sha256=Q4EvrtGJORyNF6AnCLGXToy8XU6yofiO5_kt7hK-AYs,2626
13
+ nova/trame/view/theme/__init__.py,sha256=70_marDlTigIcPEOGiJb2JTs-8b2sGM5SlY7XBPtBDM,54
14
+ nova/trame/view/theme/assets/core_style.scss,sha256=AktysiiCYLeiTzCTtYwkksiUVmqb4S23RlDcW8L1ebI,518
15
+ nova/trame/view/theme/assets/favicon.png,sha256=Xbp1nUmhcBDeObjsebEbEAraPDZ_M163M_ZLtm5AbQc,1927
16
+ nova/trame/view/theme/assets/vuetify_config.json,sha256=7WGV6rO7hv2sapGsX9yy1d-dINshYFXRNX99D9I3dKQ,4780
17
+ nova/trame/view/theme/theme.py,sha256=-5lq8u34eQ9j1bMeB5b63BiAdpi92WI2o-fJKcrwl3o,11401
18
+ nova/trame/view/utilities/local_storage.py,sha256=vD8f2VZIpxhIKjZwEaD7siiPCTZO4cw9AfhwdawwYLY,3218
19
+ nova/trame/view_model/remote_file_input.py,sha256=WHWCQkZBGeKLe1aTPbtVNI8tn-PDt64mi1-561uuBpQ,3320
20
+ nova_trame-0.13.1.dist-info/LICENSE,sha256=MOqZ8tPMKy8ZETJ2-HEvFTZ7dYNlg3gXmBkV-Y9i8bw,1061
21
+ nova_trame-0.13.1.dist-info/METADATA,sha256=ivedQKUq-ZJ2jk1e0aXkeHjbvtxXWLyOBGNl-lijv5E,1240
22
+ nova_trame-0.13.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
23
+ nova_trame-0.13.1.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
24
+ nova_trame-0.13.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.0.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ app=tests.gallery:main
3
+