nova-trame 0.21.0__py3-none-any.whl → 0.22.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.
- nova/trame/model/data_selector.py +31 -32
- nova/trame/view/components/data_selector.py +2 -0
- nova/trame/view/theme/assets/core_style.scss +8 -0
- nova/trame/view/theme/exit_button.py +73 -0
- nova/trame/view/theme/theme.py +29 -0
- nova/trame/view_model/data_selector.py +30 -1
- {nova_trame-0.21.0.dist-info → nova_trame-0.22.1.dist-info}/METADATA +3 -3
- {nova_trame-0.21.0.dist-info → nova_trame-0.22.1.dist-info}/RECORD +11 -10
- {nova_trame-0.21.0.dist-info → nova_trame-0.22.1.dist-info}/LICENSE +0 -0
- {nova_trame-0.21.0.dist-info → nova_trame-0.22.1.dist-info}/WHEEL +0 -0
- {nova_trame-0.21.0.dist-info → nova_trame-0.22.1.dist-info}/entry_points.txt +0 -0
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
import os
|
4
4
|
from pathlib import Path
|
5
|
-
from typing import Any, List, Optional
|
5
|
+
from typing import Any, Dict, List, Optional
|
6
6
|
from warnings import warn
|
7
7
|
|
8
8
|
from natsort import natsorted
|
@@ -140,7 +140,7 @@ class DataSelectorModel:
|
|
140
140
|
|
141
141
|
return natsorted(experiments)
|
142
142
|
|
143
|
-
def sort_directories(self, directories: List[Any]) -> List[Any]:
|
143
|
+
def sort_directories(self, directories: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
144
144
|
# Sort the current level of dictionaries
|
145
145
|
sorted_dirs = natsorted(directories, key=lambda x: x["title"])
|
146
146
|
|
@@ -164,9 +164,11 @@ class DataSelectorModel:
|
|
164
164
|
|
165
165
|
return Path(self.state.custom_directory)
|
166
166
|
|
167
|
-
def get_directories(self) -> List[str]:
|
167
|
+
def get_directories(self, base_path: Optional[Path] = None) -> List[Dict[str, Any]]:
|
168
168
|
using_custom_directory = self.state.facility == CUSTOM_DIRECTORIES_LABEL
|
169
|
-
if
|
169
|
+
if base_path:
|
170
|
+
pass
|
171
|
+
elif using_custom_directory:
|
170
172
|
base_path = self.get_custom_directory_path()
|
171
173
|
else:
|
172
174
|
base_path = self.get_experiment_directory_path()
|
@@ -176,34 +178,31 @@ class DataSelectorModel:
|
|
176
178
|
|
177
179
|
directories = []
|
178
180
|
try:
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
# Add the last part (current directory) as a child
|
206
|
-
current_level.append({"path": dirpath, "title": path_parts[-1]})
|
181
|
+
for dirpath, dirs, _ in os.walk(base_path):
|
182
|
+
# Get the relative path from the start path
|
183
|
+
path_parts = os.path.relpath(dirpath, base_path).split(os.sep)
|
184
|
+
|
185
|
+
if len(path_parts) > 1:
|
186
|
+
dirs.clear()
|
187
|
+
|
188
|
+
# Only create a new entry for top-level directories
|
189
|
+
if len(path_parts) == 1 and path_parts[0] != ".": # This indicates a top-level directory
|
190
|
+
current_dir = {"path": dirpath, "title": path_parts[0]}
|
191
|
+
directories.append(current_dir)
|
192
|
+
|
193
|
+
# Add subdirectories to the corresponding parent directory
|
194
|
+
elif len(path_parts) > 1:
|
195
|
+
current_level: Any = directories
|
196
|
+
for part in path_parts[:-1]: # Parent directories
|
197
|
+
for item in current_level:
|
198
|
+
if item["title"] == part:
|
199
|
+
if "children" not in item:
|
200
|
+
item["children"] = []
|
201
|
+
current_level = item["children"]
|
202
|
+
break
|
203
|
+
|
204
|
+
# Add the last part (current directory) as a child
|
205
|
+
current_level.append({"path": dirpath, "title": path_parts[-1]})
|
207
206
|
except OSError:
|
208
207
|
pass
|
209
208
|
|
@@ -133,8 +133,10 @@ class DataSelector(datagrid.VGrid):
|
|
133
133
|
activatable=True,
|
134
134
|
active_strategy="single-independent",
|
135
135
|
classes="flex-1-0 h-0 overflow-y-auto",
|
136
|
+
fluid=True,
|
136
137
|
item_value="path",
|
137
138
|
items=(self._directories_name,),
|
139
|
+
click_open=(self._vm.expand_directory, "[$event.path]"),
|
138
140
|
update_activated=(self._vm.set_directory, "$event"),
|
139
141
|
)
|
140
142
|
vuetify.VListItem("No directories found", classes="flex-0-1 text-center", v_else=True)
|
@@ -20,6 +20,14 @@ html {
|
|
20
20
|
white-space: pre-wrap;
|
21
21
|
}
|
22
22
|
|
23
|
+
.nova-data-selector {
|
24
|
+
.v-list-group {
|
25
|
+
.v-treeview-item {
|
26
|
+
--indent-padding: 1em !important;
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
23
31
|
.nova-data-selector revo-grid {
|
24
32
|
font-family: 'Roboto', sans-serif;
|
25
33
|
font-size: 0.75rem !important;
|
@@ -0,0 +1,73 @@
|
|
1
|
+
"""Components used to control the lifecycle of a Themed Application."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from trame.app import get_server
|
7
|
+
from trame.widgets import vuetify3 as vuetify
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
logger.setLevel(logging.INFO)
|
11
|
+
|
12
|
+
|
13
|
+
class ExitButton:
|
14
|
+
"""Exit button for Trame Applications."""
|
15
|
+
|
16
|
+
def __init__(self, exit_callback: Any = None, job_status_callback: Any = None) -> None:
|
17
|
+
self.server = get_server(None, client_type="vue3")
|
18
|
+
self.server.state.nova_kill_jobs_on_exit = False
|
19
|
+
self.server.state.nova_show_exit_dialog = False
|
20
|
+
self.server.state.nova_show_stop_jobs_on_exit_checkbox = False
|
21
|
+
self.server.state.nova_running_jobs = []
|
22
|
+
self.server.state.nova_show_exit_progress = False
|
23
|
+
self.exit_application_callback = exit_callback
|
24
|
+
self.job_status_callback = job_status_callback
|
25
|
+
self.create_ui()
|
26
|
+
|
27
|
+
def create_ui(self) -> None:
|
28
|
+
with vuetify.VBtn(
|
29
|
+
"Exit",
|
30
|
+
prepend_icon="mdi-close-box",
|
31
|
+
classes="mr-4 bg-error",
|
32
|
+
id="shutdown_app_theme_button",
|
33
|
+
color="white",
|
34
|
+
click=self.open_exit_dialog,
|
35
|
+
):
|
36
|
+
with vuetify.VDialog(v_model="nova_show_exit_dialog", persistent="true"):
|
37
|
+
with vuetify.VCard(classes="pa-4 ma-auto"):
|
38
|
+
vuetify.VCardTitle("Exit Application")
|
39
|
+
with vuetify.VCardText(
|
40
|
+
"Are you sure you want to exit this application?",
|
41
|
+
variant="outlined",
|
42
|
+
):
|
43
|
+
vuetify.VCheckbox(
|
44
|
+
v_model="nova_kill_jobs_on_exit",
|
45
|
+
label="Stop All Jobs On Exit.",
|
46
|
+
v_if="nova_running_jobs.length > 0",
|
47
|
+
)
|
48
|
+
with vuetify.VList():
|
49
|
+
vuetify.VListSubheader("Running Jobs:", v_if="nova_running_jobs.length > 0")
|
50
|
+
vuetify.VListItem("{{ item }}", v_for="(item, index) in nova_running_jobs")
|
51
|
+
with vuetify.VCardActions(v_if="!nova_show_exit_progress"):
|
52
|
+
vuetify.VBtn(
|
53
|
+
"Exit App",
|
54
|
+
click=self.exit_application_callback,
|
55
|
+
color="error",
|
56
|
+
)
|
57
|
+
vuetify.VBtn(
|
58
|
+
"Stay In App",
|
59
|
+
click=self.close_exit_dialog,
|
60
|
+
)
|
61
|
+
with vuetify.VCardActions(v_else=True):
|
62
|
+
vuetify.VCardText(
|
63
|
+
"Exiting Application...",
|
64
|
+
variant="outlined",
|
65
|
+
)
|
66
|
+
vuetify.VProgressCircular(indeterminate=True)
|
67
|
+
|
68
|
+
async def open_exit_dialog(self) -> None:
|
69
|
+
self.server.state.nova_show_exit_dialog = True
|
70
|
+
await self.job_status_callback()
|
71
|
+
|
72
|
+
async def close_exit_dialog(self) -> None:
|
73
|
+
self.server.state.nova_show_exit_dialog = False
|
nova/trame/view/theme/theme.py
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
"""Implementation of ThemedApp."""
|
2
2
|
|
3
|
+
import asyncio
|
3
4
|
import json
|
4
5
|
import logging
|
6
|
+
import sys
|
5
7
|
from asyncio import create_task
|
6
8
|
from functools import partial
|
7
9
|
from pathlib import Path
|
8
10
|
from typing import Optional
|
9
11
|
|
12
|
+
import blinker
|
10
13
|
import sass
|
11
14
|
from mergedeep import Strategy, merge
|
12
15
|
from trame.app import get_server
|
@@ -18,7 +21,9 @@ from trame_client.widgets import html
|
|
18
21
|
from trame_server.core import Server
|
19
22
|
from trame_server.state import State
|
20
23
|
|
24
|
+
from nova.common.signals import Signal
|
21
25
|
from nova.mvvm.pydantic_utils import validate_pydantic_parameter
|
26
|
+
from nova.trame.view.theme.exit_button import ExitButton
|
22
27
|
from nova.trame.view.utilities.local_storage import LocalStorageManager
|
23
28
|
|
24
29
|
THEME_PATH = Path(__file__).parent
|
@@ -138,6 +143,29 @@ class ThemedApp:
|
|
138
143
|
async def init_theme(self) -> None:
|
139
144
|
create_task(self._init_theme())
|
140
145
|
|
146
|
+
async def get_jobs_callback(self) -> None:
|
147
|
+
get_tools_signal = blinker.signal(Signal.GET_ALL_TOOLS)
|
148
|
+
response = get_tools_signal.send()
|
149
|
+
if response and len(response[0]) > 1: # Make sure that the callback had a return value
|
150
|
+
try:
|
151
|
+
self.server.state.nova_running_jobs = [tool.id for tool in response[0][1]]
|
152
|
+
if len(self.server.state.nova_running_jobs) > 0:
|
153
|
+
self.server.state.nova_show_stop_jobs_on_exit_checkbox = True
|
154
|
+
self.server.state.nova_kill_jobs_on_exit = True
|
155
|
+
else:
|
156
|
+
self.server.state.nova_show_stop_jobs_on_exit_checkbox = False
|
157
|
+
except Exception as e:
|
158
|
+
logger.warning(f"Issue getting running jobs: {e}")
|
159
|
+
|
160
|
+
async def exit_callback(self) -> None:
|
161
|
+
logger.info(f"Closing App. Killing jobs: {self.server.state.nova_kill_jobs_on_exit}")
|
162
|
+
if self.server.state.nova_kill_jobs_on_exit:
|
163
|
+
self.server.state.nova_show_exit_progress = True
|
164
|
+
await asyncio.sleep(2)
|
165
|
+
stop_signal = blinker.signal(Signal.EXIT_SIGNAL)
|
166
|
+
stop_signal.send()
|
167
|
+
sys.exit(0)
|
168
|
+
|
141
169
|
def set_theme(self, theme: Optional[str], force: bool = True) -> None:
|
142
170
|
"""Sets the theme of the application.
|
143
171
|
|
@@ -227,6 +255,7 @@ class ThemedApp:
|
|
227
255
|
"Selected",
|
228
256
|
v_if=f"nova__theme === '{theme['value']}'",
|
229
257
|
)
|
258
|
+
ExitButton(self.exit_callback, self.get_jobs_callback)
|
230
259
|
|
231
260
|
with vuetify.VMain(classes="align-stretch d-flex flex-column h-screen"):
|
232
261
|
# [slot override example]
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""View model implementation for the DataSelector widget."""
|
2
2
|
|
3
3
|
import os
|
4
|
+
from pathlib import Path
|
4
5
|
from typing import Any, Dict, List, Optional
|
5
6
|
|
6
7
|
from nova.mvvm.interface import BindingInterface
|
@@ -14,6 +15,8 @@ class DataSelectorViewModel:
|
|
14
15
|
self.model = model
|
15
16
|
|
16
17
|
self.datafiles: List[Dict[str, Any]] = []
|
18
|
+
self.directories: List[Dict[str, Any]] = []
|
19
|
+
self.expanded: List[str] = []
|
17
20
|
|
18
21
|
self.state_bind = binding.new_bind(self.model.state, callback_after_update=self.on_state_updated)
|
19
22
|
self.facilities_bind = binding.new_bind()
|
@@ -23,6 +26,30 @@ class DataSelectorViewModel:
|
|
23
26
|
self.datafiles_bind = binding.new_bind()
|
24
27
|
self.reset_bind = binding.new_bind()
|
25
28
|
|
29
|
+
def expand_directory(self, paths: List[str]) -> None:
|
30
|
+
if paths[-1] in self.expanded:
|
31
|
+
return
|
32
|
+
|
33
|
+
# Query for the new subdirectories to display in the view
|
34
|
+
new_directories = self.model.get_directories(Path(paths[-1]))
|
35
|
+
|
36
|
+
# Find the entry in the existing directories that corresponds to the directory to expand
|
37
|
+
current_level: Dict[str, Any] = {}
|
38
|
+
children: List[Dict[str, Any]] = self.directories
|
39
|
+
for current_path in paths:
|
40
|
+
if current_level:
|
41
|
+
children = current_level["children"]
|
42
|
+
|
43
|
+
for entry in children:
|
44
|
+
if current_path == entry["path"]:
|
45
|
+
current_level = entry
|
46
|
+
break
|
47
|
+
current_level["children"] = new_directories
|
48
|
+
|
49
|
+
# Mark this directory as expanded and display the new content
|
50
|
+
self.expanded.append(paths[-1])
|
51
|
+
self.directories_bind.update_in_view(self.directories)
|
52
|
+
|
26
53
|
def set_directory(self, directory_path: str = "") -> None:
|
27
54
|
self.model.set_directory(directory_path)
|
28
55
|
self.update_view()
|
@@ -33,6 +60,8 @@ class DataSelectorViewModel:
|
|
33
60
|
|
34
61
|
def reset(self) -> None:
|
35
62
|
self.model.set_directory("")
|
63
|
+
self.directories = self.model.get_directories()
|
64
|
+
self.expanded = []
|
36
65
|
self.reset_bind.update_in_view(None)
|
37
66
|
|
38
67
|
def on_state_updated(self, results: Dict[str, Any]) -> None:
|
@@ -55,7 +84,7 @@ class DataSelectorViewModel:
|
|
55
84
|
self.facilities_bind.update_in_view(self.model.get_facilities())
|
56
85
|
self.instruments_bind.update_in_view(self.model.get_instruments())
|
57
86
|
self.experiments_bind.update_in_view(self.model.get_experiments())
|
58
|
-
self.directories_bind.update_in_view(self.
|
87
|
+
self.directories_bind.update_in_view(self.directories)
|
59
88
|
|
60
89
|
self.datafiles = [
|
61
90
|
{"path": datafile, "title": os.path.basename(datafile)} for datafile in self.model.get_datafiles()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: nova-trame
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.22.1
|
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
|
@@ -18,7 +18,7 @@ Requires-Dist: blinker (>=1.9.0,<2.0.0)
|
|
18
18
|
Requires-Dist: libsass
|
19
19
|
Requires-Dist: mergedeep
|
20
20
|
Requires-Dist: natsort (>=8.4.0,<9.0.0)
|
21
|
-
Requires-Dist: nova-common (>=0.2.
|
21
|
+
Requires-Dist: nova-common (>=0.2.2)
|
22
22
|
Requires-Dist: nova-mvvm
|
23
23
|
Requires-Dist: pydantic
|
24
24
|
Requires-Dist: tomli
|
@@ -43,7 +43,7 @@ You can install this package directly with
|
|
43
43
|
pip install nova-trame
|
44
44
|
```
|
45
45
|
|
46
|
-
A user guide, examples, and a full API for this package can be found at https://nova-application-development.readthedocs.io/en/stable
|
46
|
+
A user guide, examples, and a full API for this package can be found at [https://nova-application-development.readthedocs.io/en/stable/](https://nova-application-development.readthedocs.io/projects/nova-trame/en/stable/).
|
47
47
|
|
48
48
|
Developers: please read [this document](DEVELOPMENT.md)
|
49
49
|
|
@@ -1,9 +1,9 @@
|
|
1
1
|
nova/__init__.py,sha256=ED6jHcYiuYpr_0vjGz0zx2lrrmJT9sDJCzIljoDfmlM,65
|
2
2
|
nova/trame/__init__.py,sha256=gFrAg1qva5PIqR5TjvPzAxLx103IKipJLqp3XXvrQL8,59
|
3
|
-
nova/trame/model/data_selector.py,sha256=
|
3
|
+
nova/trame/model/data_selector.py,sha256=UnLBCp_jJ523QxTR3R8iun2Ogq4D0G0lxmtW9e_zwOM,8938
|
4
4
|
nova/trame/model/remote_file_input.py,sha256=9KAf31ZHzpsh_aXUrNcF81Q5jvUZDWCzW1QATKls-Jk,3675
|
5
5
|
nova/trame/view/components/__init__.py,sha256=60BeS69aOrFnkptjuD17rfPE1f4Z35iBH56TRmW5MW8,451
|
6
|
-
nova/trame/view/components/data_selector.py,sha256=
|
6
|
+
nova/trame/view/components/data_selector.py,sha256=lgZjyT_jc3eE19HNgz_Hdog5bWvXZJ3IfxxiSBkZ7hE,11222
|
7
7
|
nova/trame/view/components/execution_buttons.py,sha256=fIkrWKI3jFZqk3GHhtmYh3nK2c-HOXpD3D3zd_TUpi0,4049
|
8
8
|
nova/trame/view/components/file_upload.py,sha256=7VcpfA6zmiqMDLkwVPlb35Tf0IUTBN1xsHpoUFnSr1w,3111
|
9
9
|
nova/trame/view/components/input_field.py,sha256=q6WQ_N-BOlimUL9zgazDlsDfK28FrrKjH4he8e_HzRA,16088
|
@@ -19,21 +19,22 @@ nova/trame/view/layouts/hbox.py,sha256=qlOMp_iOropIkC9Jxa6D89b7OPv0pNvJ73tUEzddy
|
|
19
19
|
nova/trame/view/layouts/utils.py,sha256=Hg34VQWTG3yHBsgNvmfatR4J-uL3cko7UxSJpT-h3JI,376
|
20
20
|
nova/trame/view/layouts/vbox.py,sha256=hzhzPu99R2fAclMe-FwHZseJWk7iailZ31bKdGhi1hk,3514
|
21
21
|
nova/trame/view/theme/__init__.py,sha256=70_marDlTigIcPEOGiJb2JTs-8b2sGM5SlY7XBPtBDM,54
|
22
|
-
nova/trame/view/theme/assets/core_style.scss,sha256=
|
22
|
+
nova/trame/view/theme/assets/core_style.scss,sha256=lK86Fp55oAMDh1eUHA-DTeGGZi0uUYOseIyJUTj-0A0,4081
|
23
23
|
nova/trame/view/theme/assets/favicon.png,sha256=Xbp1nUmhcBDeObjsebEbEAraPDZ_M163M_ZLtm5AbQc,1927
|
24
24
|
nova/trame/view/theme/assets/js/delay_manager.js,sha256=mRV6KoO8-Bxq3tG5Bh9CQYy-CRVbkj3IYlqNb-Og7cI,526
|
25
25
|
nova/trame/view/theme/assets/js/lodash.min.js,sha256=KCyAYJ-fsqtp_HMwbjhy6IKjlA5lrVrtWt1JdMsC57k,73016
|
26
26
|
nova/trame/view/theme/assets/js/revo_grid.js,sha256=WBsmoslu9qI5DHZkHkJam2AVgdiBp6szfOSV8a9cA5Q,3579
|
27
27
|
nova/trame/view/theme/assets/vuetify_config.json,sha256=a0FSgpLYWGFlRGSMhMq61MyDFBEBwvz55G4qjkM08cs,5627
|
28
|
-
nova/trame/view/theme/
|
28
|
+
nova/trame/view/theme/exit_button.py,sha256=Kqv1GVJZGrSsj6_JFjGU3vm3iNuMolLC2T1x2IsdmV0,3094
|
29
|
+
nova/trame/view/theme/theme.py,sha256=8JqSrEbhxK1SccXE1_jUdel9Wtc2QNObVEwtbVWG_QY,13146
|
29
30
|
nova/trame/view/utilities/local_storage.py,sha256=vD8f2VZIpxhIKjZwEaD7siiPCTZO4cw9AfhwdawwYLY,3218
|
30
|
-
nova/trame/view_model/data_selector.py,sha256=
|
31
|
+
nova/trame/view_model/data_selector.py,sha256=RyMHml1K_pupH4JtXnGxAaYTYYwNoEVus7Abdpqwueo,3698
|
31
32
|
nova/trame/view_model/execution_buttons.py,sha256=MfKSp95D92EqpD48C15cBo6dLO0Yld4FeRZMJNxJf7Y,3551
|
32
33
|
nova/trame/view_model/progress_bar.py,sha256=6AUKHF3hfzbdsHqNEnmHRgDcBKY5TT8ywDx9S6ovnsc,2854
|
33
34
|
nova/trame/view_model/remote_file_input.py,sha256=ojEOJ8ZPkajpbAaZi9VLj7g-uBjhb8BMrTdMmwf_J6A,3367
|
34
35
|
nova/trame/view_model/tool_outputs.py,sha256=ev6LY7fJ0H2xAJn9f5ww28c8Kpom2SYc2FbvFcoN4zg,829
|
35
|
-
nova_trame-0.
|
36
|
-
nova_trame-0.
|
37
|
-
nova_trame-0.
|
38
|
-
nova_trame-0.
|
39
|
-
nova_trame-0.
|
36
|
+
nova_trame-0.22.1.dist-info/LICENSE,sha256=Iu5QiDbwNbREg75iYaxIJ_V-zppuv4QFuBhAW-qiAlM,1061
|
37
|
+
nova_trame-0.22.1.dist-info/METADATA,sha256=A-wOqVcFQXeV1qpuPJv253RugeWK5j2UryAUYdwgtII,1689
|
38
|
+
nova_trame-0.22.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
39
|
+
nova_trame-0.22.1.dist-info/entry_points.txt,sha256=J2AmeSwiTYZ4ZqHHp9HO6v4MaYQTTBPbNh6WtoqOT58,42
|
40
|
+
nova_trame-0.22.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|