Skip to content

Commit 8047d96

Browse files
authored
fix(gui): improvements to gui_local_slot_editing
Merge pull request #169 from OpenSemanticLab/fix/gui-local-slot-editing
2 parents ece8ccd + 4ca7ab9 commit 8047d96

9 files changed

Lines changed: 427 additions & 89 deletions

‎examples/gui_local_slot_editing.py‎

Lines changed: 188 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import json
55
import os
6+
import re
67

78
# import yaml
89
from pathlib import Path
@@ -52,6 +53,33 @@ def str_or_none(value):
5253
return str(value)
5354

5455

56+
def compute_page_storage_path(
57+
working_dir: str,
58+
domain_str: str,
59+
page_name_as_filename: bool,
60+
label_str: str = None,
61+
top_level: str = None,
62+
) -> str:
63+
"""Compute the folder that a page's slots are downloaded to / uploaded from.
64+
65+
Mirrors the folder-naming logic previously inlined in save_as_page_package: the
66+
page label is used when 'page_name_as_filename' is set and a label is known,
67+
otherwise the top-level page title is used. A domain sub folder is inserted so
68+
that pages from different OSW instances don't collide inside the same local
69+
working directory.
70+
71+
Both the '-DL-' (download) and '-UL-' (upload) event handlers call this helper
72+
so they always agree on where a given page's content is stored on disk.
73+
"""
74+
if page_name_as_filename and label_str is not None:
75+
page_dir_name = label_str
76+
else:
77+
page_dir_name = top_level
78+
# Sanitize the domain for use as a directory name (invalid on Windows: \/:*?"<>|)
79+
domain_dir = re.sub(r'[\\/:*?"<>|]', "_", domain_str)
80+
return os.path.join(working_dir, domain_dir, page_dir_name)
81+
82+
5583
def create_config_from_setting(settings_: dict) -> WtPage.PageDumpConfig:
5684
config_ = WtPage.PageDumpConfig(
5785
target_dir=settings_["local_working_directory"],
@@ -66,6 +94,7 @@ def save_as_page_package(
6694
full_page_name_str,
6795
wtsite_inst: WtSite,
6896
dump_config_inst: WtPage.PageDumpConfig,
97+
domain_str: str,
6998
label_str: str = None,
7099
top_level: str = None,
71100
sub_level: str = None, # "content"
@@ -81,10 +110,13 @@ def save_as_page_package(
81110
package_branch = "deleteme"
82111
publisher = "Open Semantic World"
83112
working_dir = dump_config_inst.target_dir
84-
if dump_config_inst.page_name_as_filename and label_str is not None:
85-
target_dir = os.path.join(working_dir, label_str)
86-
else:
87-
target_dir = os.path.join(working_dir, top_level)
113+
target_dir = compute_page_storage_path(
114+
working_dir,
115+
domain_str,
116+
dump_config_inst.page_name_as_filename,
117+
label_str,
118+
top_level,
119+
)
88120
if isinstance(author, list):
89121
author_list = author
90122
elif isinstance(author, str):
@@ -156,24 +188,46 @@ def save_as_page_package(
156188
}
157189
settings_read_from_file = False
158190

159-
domains, accounts = read_domains_from_credentials_file(settings["cred_filepath"])
160-
if "wiki-dev.open-semantic-lab.org" in domains:
161-
domain = "wiki-dev.open-semantic-lab.org"
191+
# An empty/missing accounts.pwd.yaml must not prevent the window from opening.
192+
# The user can still type/select a domain once the window is up.
193+
domains, accounts, cred_load_error = [], {}, None
194+
try:
195+
domains, accounts = read_domains_from_credentials_file(
196+
settings["cred_filepath"]
197+
)
198+
except (FileNotFoundError, ValueError) as e:
199+
cred_load_error = f"{type(e).__name__}: {e}"
200+
201+
if domains:
202+
if "wiki-dev.open-semantic-lab.org" in domains:
203+
domain = "wiki-dev.open-semantic-lab.org"
204+
else:
205+
domain = domains[0]
206+
if settings_read_from_file:
207+
settings["domain"] = domain
162208
else:
163-
domain = domains[0]
164-
if settings_read_from_file:
165-
settings["domain"] = domain
209+
domain = settings.get("domain", "")
166210

167211
cm = CredentialManager(cred_filepath=settings["cred_filepath"])
168-
osw_obj = OswExpress(domain=domain, cred_mngr=cm)
169-
wtsite_obj = osw_obj.site
212+
osw_obj = None
213+
wtsite_obj = None
214+
if domain:
215+
osw_obj = OswExpress(domain=domain, cred_mngr=cm)
216+
wtsite_obj = osw_obj.site
170217

171218
full_page_name = settings["target_page"].split("/")[-1].replace("_", " ")
172-
page = wtsite_obj.get_page(WtSite.GetPageParam(titles=[full_page_name])).pages[0]
219+
page = None
220+
if wtsite_obj is not None:
221+
page = wtsite_obj.get_page(WtSite.GetPageParam(titles=[full_page_name])).pages[
222+
0
223+
]
173224
label_set = False
174225
label = None
175226
slots_downloaded = False
176227
dump_config = create_config_from_setting(settings)
228+
label_default_text = ""
229+
if cred_load_error is not None:
230+
label_default_text = f"Could not load credentials: {cred_load_error}"
177231

178232
# ----- GUI Definition -----
179233
# Setting the theme of the GUI
@@ -253,7 +307,12 @@ def save_as_page_package(
253307
],
254308
[
255309
# A display element that will show the label of the OSW page
256-
psg.Multiline(size=(50, 1), key="-LABEL-", no_scrollbar=True)
310+
psg.Multiline(
311+
size=(50, 1),
312+
key="-LABEL-",
313+
no_scrollbar=True,
314+
default_text=label_default_text,
315+
)
257316
],
258317
]),
259318
],
@@ -354,10 +413,17 @@ def save_as_page_package(
354413
json.dump(settings, f, indent=4)
355414
elif event == "-CREDENTIALS-":
356415
settings["cred_filepath"] = values["-CREDENTIALS-"]
357-
domains, accounts = read_domains_from_credentials_file(
358-
settings["cred_filepath"]
359-
)
360-
window["-DOMAIN-"].update(values=domains)
416+
try:
417+
domains, accounts = read_domains_from_credentials_file(
418+
settings["cred_filepath"]
419+
)
420+
window["-DOMAIN-"].update(values=domains)
421+
except (FileNotFoundError, ValueError) as e:
422+
# Keep the previously loaded domains/accounts, the user can retry
423+
# with a different file.
424+
window["-LABEL-"].update(
425+
f"Could not load credentials: {type(e).__name__}: {e}"
426+
)
361427
elif event == "-LWD-":
362428
settings["local_working_directory"] = values["-LWD-"]
363429
elif event == "-DOMAIN-":
@@ -368,51 +434,82 @@ def save_as_page_package(
368434
elif event == "Load page":
369435
window["-LABEL-"].update("Loading page...")
370436
window["-DL_RES-"].update("")
371-
full_page_name = values["-ADDRESS-"].split("/")[-1].replace("_", " ")
372-
if (values["-ADDRESS-"].find("/wiki/") != -1) or (
373-
values["-ADDRESS-"].find("/w/") != -1
374-
):
375-
settings["target_page"] = values["-ADDRESS-"]
376-
else:
377-
settings["target_page"] = (
378-
"https://" + domain + "/wiki/" + full_page_name
437+
if wtsite_obj is None:
438+
window["-LABEL-"].update(
439+
"No domain selected. Load a credentials file or select a "
440+
"domain first."
379441
)
380-
if values["-ADDRESS-"].find(settings["domain"]) == -1:
381-
window["-LABEL-"].update("Page not on selected domain!")
382442
label_set = False
383443
else:
384-
# use connection
385-
page = wtsite_obj.get_page(
386-
WtSite.GetPageParam(titles=[full_page_name])
387-
).pages[0]
388-
if page.exists:
389-
jsondata = page.get_slot_content("jsondata")
390-
if jsondata is None:
391-
window["-LABEL-"].update("Slot 'jsondata' is empty!")
392-
label_set = False
393-
else:
394-
label = jsondata["label"][0]["text"]
395-
window["-LABEL-"].update(label)
396-
label_set = True
444+
full_page_name = values["-ADDRESS-"].split("/")[-1].replace("_", " ")
445+
if (values["-ADDRESS-"].find("/wiki/") != -1) or (
446+
values["-ADDRESS-"].find("/w/") != -1
447+
):
448+
settings["target_page"] = values["-ADDRESS-"]
397449
else:
398-
window["-LABEL-"].update("Page does not exist!")
450+
settings["target_page"] = (
451+
"https://" + domain + "/wiki/" + full_page_name
452+
)
453+
if values["-ADDRESS-"].find(settings["domain"]) == -1:
454+
window["-LABEL-"].update("Page not on selected domain!")
399455
label_set = False
456+
else:
457+
# use connection
458+
page = wtsite_obj.get_page(
459+
WtSite.GetPageParam(titles=[full_page_name])
460+
).pages[0]
461+
if page.exists:
462+
jsondata = page.get_slot_content("jsondata")
463+
if jsondata is None:
464+
window["-LABEL-"].update("Slot 'jsondata' is empty!")
465+
label_set = False
466+
else:
467+
label = jsondata["label"][0]["text"]
468+
window["-LABEL-"].update(label)
469+
label_set = True
470+
else:
471+
window["-LABEL-"].update("Page does not exist!")
472+
label_set = False
400473
elif event == "-EXC_EMPTY-" or event == "-INC_EMPTY-":
401474
settings["dump_empty_slots"] = values["-INC_EMPTY-"]
402475
elif event == "-DL-":
403476
window["-DL_RES-"].update("Downloading slots...")
404477
if label_set:
405-
dump_config = create_config_from_setting(settings)
406-
_ = save_as_page_package(
407-
full_page_name_str=full_page_name,
408-
wtsite_inst=wtsite_obj,
409-
dump_config_inst=dump_config,
410-
label_str=label,
411-
sub_level=SUB_LEVEL,
412-
author=accounts[domains[0]]["username"],
413-
)
414-
slots_downloaded = True
415-
window["-DL_RES-"].update("Slots downloaded!")
478+
try:
479+
dump_config = create_config_from_setting(settings)
480+
target_dir = compute_page_storage_path(
481+
settings["local_working_directory"],
482+
domain,
483+
settings["page_name_as_filename"],
484+
label,
485+
full_page_name.split(":")[-1],
486+
)
487+
window["-DL_RES-"].update(
488+
f"Clearing '{target_dir}' and downloading slots..."
489+
)
490+
# Look up the author for the selected domain, not the first
491+
# one in the list, and degrade gracefully if there is no
492+
# credential entry for it (e.g. empty accounts.pwd.yaml).
493+
author = accounts.get(domain, {}).get("username")
494+
if author is None:
495+
window["-DL_RES-"].update(
496+
f"No credentials found for domain '{domain}'; "
497+
"using default author."
498+
)
499+
author = "Open Semantic World"
500+
_ = save_as_page_package(
501+
full_page_name_str=full_page_name,
502+
wtsite_inst=wtsite_obj,
503+
dump_config_inst=dump_config,
504+
domain_str=domain,
505+
label_str=label,
506+
sub_level=SUB_LEVEL,
507+
author=author,
508+
)
509+
slots_downloaded = True
510+
window["-DL_RES-"].update("Slots downloaded!")
511+
except Exception as e:
512+
window["-DL_RES-"].update(f"{type(e).__name__}: {e}")
416513
else:
417514
window["-DL_RES-"].update("No page loaded!")
418515
elif event == "-UL_SEL-":
@@ -434,29 +531,45 @@ def save_as_page_package(
434531
elif len(slots_to_upload) == 0:
435532
window["-UL_RES-"].update("No slots selected!")
436533
else:
437-
if not slots_downloaded:
438-
window["-UL_RES-"].update("No slots downloaded!")
439-
dump_config = create_config_from_setting(settings)
440-
window["-UL_RES-"].update("Uploading slots...")
441-
if SUB_LEVEL is None:
442-
storage_path = Path(dump_config.target_dir)
443-
else:
444-
storage_path = Path(dump_config.target_dir).parent
445-
pages = wtsite_obj.read_page_package(
446-
WtSite.ReadPagePackageParam(
447-
storage_path=storage_path,
448-
packages_info_file_name=PACKAGE_INFO_FILE_NAME,
449-
selected_slots=slots_to_upload,
450-
debug=False,
534+
try:
535+
# Recompute the storage path the same way the download path
536+
# does, instead of trusting a possibly stale dump_config.
537+
# This also makes upload work against a folder written by a
538+
# previous run, even if no download happened this session.
539+
storage_path = Path(
540+
compute_page_storage_path(
541+
settings["local_working_directory"],
542+
domain,
543+
settings["page_name_as_filename"],
544+
label,
545+
full_page_name.split(":")[-1],
546+
)
451547
)
452-
).pages
453-
param = wtsite_obj.UploadPageParam(pages=pages, parallel=False)
454-
wtsite_obj.upload_page(param)
455-
# Success:
456-
window["-UL_RES-"].update("Slots uploaded!")
457-
# Report in the download section that the slots have been uploaded to
458-
# remind the user that he eventually has to re-download the slots
459-
window["-DL_RES-"].update("Slots uploaded!")
548+
if not slots_downloaded:
549+
window["-UL_RES-"].update(
550+
"No slots downloaded this session, trying folder from a "
551+
f"previous run at '{storage_path}'..."
552+
)
553+
else:
554+
window["-UL_RES-"].update("Uploading slots...")
555+
pages = wtsite_obj.read_page_package(
556+
WtSite.ReadPagePackageParam(
557+
storage_path=storage_path,
558+
packages_info_file_name=PACKAGE_INFO_FILE_NAME,
559+
selected_slots=slots_to_upload,
560+
debug=False,
561+
)
562+
).pages
563+
param = wtsite_obj.UploadPageParam(pages=pages, parallel=False)
564+
wtsite_obj.upload_page(param)
565+
# Success:
566+
window["-UL_RES-"].update("Slots uploaded!")
567+
# Report in the download section that the slots have been
568+
# uploaded to remind the user that he eventually has to
569+
# re-download the slots
570+
window["-DL_RES-"].update("Slots uploaded!")
571+
except Exception as e:
572+
window["-UL_RES-"].update(f"{type(e).__name__}: {e}")
460573

461574
# Some debugging output functionality
462575
if DEBUG:

‎pyproject.toml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ dataimport = [
6868
"deepl",
6969
"openpyxl",
7070
]
71-
UI = ["pysimplegui"]
71+
UI = [
72+
# PySimpleGUI 5 was commercial and its releases are yanked from PyPI.
73+
# The LGPLv3 line resumed at version 6, which the GUI examples target.
74+
"pysimplegui>=6",
75+
]
7276
workflow = [
7377
"prefect>=2.20.25,<3.0",
7478
# prefect 2.20.25 is the final 2.x release (no backports). Its

‎src/osw/model/page_package.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ class PagePackageConfig(BaseModel):
171171
content_path: Optional[Union[str, Path]] = ""
172172
"""The directory where the content (pages, files) is stored.
173173
Will be created automatically if not existing."""
174+
clear_content_dir: Optional[bool] = True
175+
"""If True (default), the content directory is deleted before the page
176+
package is created. Set to False to keep any existing content, e.g. when
177+
reusing a directory that already holds locally edited slot files."""
174178
titles: List[str]
175179
"""List of page titles."""
176180
ignore_titles: Optional[List[str]] = None

‎src/osw/wiki_tools.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ def read_domains_from_credentials_file(
4848
with open(cred_filepath, encoding="utf-8") as stream_:
4949
try:
5050
accounts_dict = yaml.safe_load(stream_)
51+
# An empty file is parsed as None by yaml.safe_load, which would
52+
# otherwise raise an AttributeError on the .keys() call below
53+
if accounts_dict is None:
54+
accounts_dict = {}
5155
domains_list = list(accounts_dict.keys())
5256
if len(domains_list) == 0:
5357
raise ValueError("No domain found in accounts.pwd.yaml!")

0 commit comments

Comments
 (0)