nova-trame 0.13.1__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.
@@ -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
+