Skip to content

Commit 6b9a4f6

Browse files
committed
feat: add circle archiving wizard GUI
1 parent 6996cb3 commit 6b9a4f6

21 files changed

Lines changed: 671 additions & 94 deletions

CHANGELOG

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
1.1.0
22
- feat: add circle archiving in the BagIt (RFC 8493) format
3+
- feat: add circle archiving wizard GUI
34
- fix: handle resource downloads whose target directory got deleted
45
- fix: check for existence of persistent job list in upload
56
- enh: allow to programmatically abort download jobs

dcoraid/bagit/archive.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,14 @@ def bag_circle(api: CKANAPI,
6060
# and if yes, compute the MD5 hash and compare it. If the comparison
6161
# fails, then the user has to choose a different `target_path`, because
6262
# we cannot guarantee data integrity.
63+
target_path.mkdir(parents=True, exist_ok=True)
6364
circle_jsonlines_path = target_path / "circle.jsonlines"
6465
if circle_jsonlines_path.exists():
6566
lines = circle_jsonlines_path.read_text().split("\n")
6667
hasher2 = hashlib.sha256()
6768
for line in lines:
68-
hasher2.update(json.loads(line)["id"].encode(encoding="utf-8"))
69+
if line.strip():
70+
hasher2.update(json.loads(line)["id"].encode(encoding="utf-8"))
6971
sha256_hash2 = hasher2.hexdigest()
7072
if sha256_hash != sha256_hash2:
7173
raise ValueError(
@@ -87,8 +89,8 @@ def bag_circle(api: CKANAPI,
8789
if callback:
8890
callback(ii / num_datasets)
8991

90-
dataset_index = ii+1
91-
prefix = str(dataset_index).zfill(max_digits)
92+
dataset_index = ii + 1
93+
prefix = str(dataset_index).zfill(max_digits+1)
9294
bag_path = target_path / f"{prefix}_{ds_dict['name']}"
9395

9496
if not manifest.is_bagged(bag_path):
@@ -148,14 +150,24 @@ def bag_dataset(api: CKANAPI,
148150
download_resource(api=api,
149151
bag_path=bag_path,
150152
res_dict=res_dict,
151-
condensed=False)
153+
condensed=False,
154+
abort_event=abort_event,
155+
)
156+
157+
if abort_event is not None and abort_event.is_set():
158+
return
152159

153160
# condensed resource
154161
if res_dict["name"].endswith(".rtdc"):
155162
download_resource(api=api,
156163
bag_path=bag_path,
157164
res_dict=res_dict,
158-
condensed=True)
165+
condensed=True,
166+
abort_event=abort_event,
167+
)
168+
169+
if abort_event is not None and abort_event.is_set():
170+
return
159171

160172
# create BagIt files
161173
info.write_bag_info(bag_path=bag_path,
@@ -171,7 +183,9 @@ def bag_dataset(api: CKANAPI,
171183
def download_resource(api: CKANAPI,
172184
bag_path: pathlib.Path,
173185
res_dict: dict,
174-
condensed: bool):
186+
condensed: bool,
187+
abort_event: threading.Event = None,
188+
):
175189
"""Download and verify a resource from DCOR
176190
177191
Parameters
@@ -183,6 +197,8 @@ def download_resource(api: CKANAPI,
183197
CKAN resource dictionary
184198
condensed
185199
Whether to download the condensed resource (or the original resource)
200+
abort_event
201+
For stopping the download process prematurely
186202
"""
187203
data_path = bag_path / "data"
188204
data_path.mkdir(parents=True, exist_ok=True)
@@ -192,7 +208,14 @@ def download_resource(api: CKANAPI,
192208
dj = DownloadJob(api=api,
193209
resource_id=res_dict["id"],
194210
download_path=dl_path,
195-
condensed=condensed
211+
condensed=condensed,
196212
)
197-
dj.task_download_resource()
213+
if abort_event is not None and abort_event.is_set():
214+
return
215+
216+
dj.task_download_resource(abort_event=abort_event)
217+
218+
if abort_event is not None and abort_event.is_set():
219+
return
220+
198221
dj.task_verify_resource()

dcoraid/gui/main.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
from .preferences import PreferencesDialog
2626
from .status_widget import StatusWidget
2727
from . import updater
28-
from .wizard_init import SetupWizard
28+
from .wizard_bagit import BagItWizard
29+
from .wizard_init import InitWizard
2930

3031

3132
file_manager = ExitStack()
@@ -100,7 +101,8 @@ def __init__(self, *args, **kwargs):
100101
self.menubar.setNativeMenuBar(False)
101102
# File menu
102103
self.actionPreferences.triggered.connect(self.dlg_pref.show)
103-
self.actionSetupWizard.triggered.connect(self.on_wizard)
104+
self.actionInitWizard.triggered.connect(self.on_wizard_init)
105+
self.actionBagItWizard.triggered.connect(self.on_wizard_bagit)
104106
# Help menu
105107
self.actionSoftware.triggered.connect(self.on_action_software)
106108
self.actionAbout.triggered.connect(self.on_action_about)
@@ -361,8 +363,13 @@ def on_progress_db_update(self, data):
361363
self.logger.error("Progress dialog not defined")
362364

363365
@QtCore.pyqtSlot()
364-
def on_wizard(self):
365-
self.wizard_init = SetupWizard(self)
366+
def on_wizard_bagit(self):
367+
self.wizard_bagit = BagItWizard(self)
368+
self.wizard_bagit.exec()
369+
370+
@QtCore.pyqtSlot()
371+
def on_wizard_init(self):
372+
self.wizard_init = InitWizard(self)
366373
self.wizard_init.exec()
367374

368375

dcoraid/gui/main.ui

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@
181181
<property name="title">
182182
<string>File</string>
183183
</property>
184-
<addaction name="actionSetupWizard"/>
184+
<addaction name="actionInitWizard"/>
185+
<addaction name="actionBagItWizard"/>
185186
<addaction name="separator"/>
186187
<addaction name="actionPreferences"/>
187188
<addaction name="separator"/>
@@ -207,8 +208,7 @@
207208
</action>
208209
<action name="actionQuit">
209210
<property name="icon">
210-
<iconset theme="times-circle">
211-
<normaloff>../../../../.designer/backup</normaloff>../../../../.designer/backup</iconset>
211+
<iconset theme="times-circle"/>
212212
</property>
213213
<property name="text">
214214
<string>Quit</string>
@@ -221,8 +221,7 @@
221221
</action>
222222
<action name="actionPreferences">
223223
<property name="icon">
224-
<iconset theme="cogs">
225-
<normaloff>../../../../.designer/backup</normaloff>../../../../.designer/backup</iconset>
224+
<iconset theme="cogs"/>
226225
</property>
227226
<property name="text">
228227
<string>Preferences...</string>
@@ -237,15 +236,22 @@
237236
<string>About</string>
238237
</property>
239238
</action>
240-
<action name="actionSetupWizard">
239+
<action name="actionInitWizard">
241240
<property name="icon">
242-
<iconset theme="hat-wizard">
243-
<normaloff>../../../../.designer/backup</normaloff>../../../../.designer/backup</iconset>
241+
<iconset theme="hat-wizard"/>
244242
</property>
245243
<property name="text">
246244
<string>Setup wizard...</string>
247245
</property>
248246
</action>
247+
<action name="actionBagItWizard">
248+
<property name="icon">
249+
<iconset theme="folder-tree"/>
250+
</property>
251+
<property name="text">
252+
<string>Archive to BagIt format...</string>
253+
</property>
254+
</action>
249255
</widget>
250256
<customwidgets>
251257
<customwidget>

dcoraid/gui/settings.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import pathlib
2+
from PyQt6 import QtCore
3+
4+
5+
def get_dir(topic: str,
6+
settings: QtCore.QSettings
7+
) -> str:
8+
"""Given a QSettings instance, return a directory
9+
10+
If writable is `True`, return a writable directory
11+
"""
12+
location = settings.value(f"paths/{topic}", "")
13+
14+
if not location:
15+
# use default value
16+
location = QtCore.QStandardPaths.standardLocations(
17+
QtCore.QStandardPaths.StandardLocation.HomeLocation)[0]
18+
else:
19+
# check whether the location exists
20+
path = pathlib.Path(location)
21+
if not path.is_dir():
22+
for pp in path.parents:
23+
if pp.is_dir():
24+
location = str(pp)
25+
break
26+
else:
27+
location = "."
28+
29+
return location or "."
30+
31+
32+
def set_dir(topic: str,
33+
path: pathlib.Path | str,
34+
settings: QtCore.QSettings,
35+
):
36+
"""Given a QSettings instance, save a directory topic"""
37+
path = pathlib.Path(path)
38+
if path.exists():
39+
if not path.is_dir():
40+
path = path.parent
41+
settings.setValue(f"paths/{topic}", str(path))

0 commit comments

Comments
 (0)