Skip to content

Commit a71151c

Browse files
committed
Merge branch 'develop'
2 parents 299c589 + cc042a9 commit a71151c

File tree

9 files changed

+428
-113
lines changed

9 files changed

+428
-113
lines changed

README.md

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,49 @@ moving the conda environment.
162162

163163
No registry edits, no GPO, no per-user configuration needed.
164164

165+
### Site-wide default configuration (all-users installs)
166+
167+
When picasso-workflow is installed for all users, individual users may not
168+
have their own `config.yaml` yet. An administrator can place a shared
169+
default at:
170+
171+
| Platform | Site config path |
172+
|---|---|
173+
| Windows | `C:\ProgramData\picasso_workflow\config.yaml` |
174+
| macOS / Linux | `/etc/picasso_workflow/config.yaml` |
175+
176+
Config files are **deep-merged** in this priority order (highest wins):
177+
178+
1. Per-user — `~/.config/picasso_workflow/config.yaml`
179+
2. Site-wide — path above
180+
3. Bundled package default
181+
182+
Each file only needs to contain the keys it wants to override. For
183+
example, a site config that sets shared cluster and Confluence defaults
184+
while leaving everything else to the package default:
185+
186+
```yaml
187+
Confluence:
188+
URL: "https://confluence.example.com"
189+
Space: "PAINT"
190+
SlurmLoginNodes:
191+
hpccluster: hpcl8001
192+
ClusterEnvironment:
193+
anaconda_module: "anaconda/3/2023.03"
194+
conda_env: "picasso-workflow"
195+
```
196+
197+
Users then only need their own config if they want to override something
198+
specific (e.g. their personal Confluence page or a different template
199+
path). Keys they do not specify are inherited from the site config.
200+
201+
To create the directory and drop in the config on Windows (elevated prompt):
202+
203+
```powershell
204+
New-Item -ItemType Directory -Force "C:\ProgramData\picasso_workflow"
205+
# then copy or create config.yaml there
206+
```
207+
165208
### macOS deployment — single-user app bundle
166209

167210
On macOS the standard way to make a Python GUI launchable from Finder (or
@@ -197,8 +240,8 @@ easily accessible:
197240
to `~/Desktop` while holding `Cmd+Alt`
198241

199242
**Icon** — the script converts `picasso_workflow/picasso-workflow.ico`
200-
to the macOS `.icns` format automatically using `sips` and `iconutil`
201-
(both are built into macOS). No extra tools needed.
243+
to the macOS `.icns` format automatically using Pillow (installed with
244+
the package) and `iconutil` (built into macOS). No extra tools needed.
202245

203246
Re-run the script after upgrading the package or moving the conda
204247
environment.

picasso_workflow/__init__.py

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -66,26 +66,70 @@ def config_logger():
6666
)
6767

6868

69+
def _deep_merge(base: dict, override: dict) -> dict:
70+
"""Return a new dict with override merged on top of base, recursively."""
71+
result = base.copy()
72+
for key, val in override.items():
73+
if (
74+
key in result
75+
and isinstance(result[key], dict)
76+
and isinstance(val, dict)
77+
):
78+
result[key] = _deep_merge(result[key], val)
79+
else:
80+
result[key] = val
81+
return result
82+
83+
84+
def _site_config_path() -> Path:
85+
"""Return the platform-appropriate site-wide config path."""
86+
if sys.platform == "win32":
87+
base = Path(os.environ.get("ProgramData", r"C:\ProgramData"))
88+
else:
89+
base = Path("/etc")
90+
return base / "picasso_workflow" / "config.yaml"
91+
92+
93+
def _load_yaml(path) -> dict:
94+
with open(path, "r") as f:
95+
return yaml.safe_load(f) or {}
96+
97+
6998
def load_config():
70-
"""Load the picasso-workflow configuration yaml file"""
71-
# 1. User config path
72-
user_config = Path.home() / ".config" / "picasso_workflow" / "config.yaml"
73-
if user_config.exists():
74-
with open(user_config, "r") as f:
75-
return yaml.safe_load(f)
76-
# 2. Fallback to package default
99+
"""Load the picasso-workflow configuration yaml file.
100+
101+
Configs are deep-merged in increasing priority order so that
102+
higher-priority files only need to specify the keys they override:
103+
104+
1. Bundled package default (config.yaml / config_template.yaml)
105+
2. Site-wide admin config (C:\\ProgramData\\picasso_workflow\\config.yaml
106+
or /etc/picasso_workflow/config.yaml)
107+
3. Per-user config (~/.config/picasso_workflow/config.yaml)
108+
"""
109+
# 1. Package default
77110
try:
78-
default_config = importlib.resources.files("picasso_workflow").joinpath(
111+
pkg_config = importlib.resources.files("picasso_workflow").joinpath(
79112
"config.yaml"
80113
)
81-
with open(default_config, "r") as f:
82-
return yaml.safe_load(f)
83-
except FileNotFoundError:
84-
template_config = importlib.resources.files("picasso_workflow").joinpath(
85-
"config_template.yaml"
86-
)
87-
with open(template_config, "r") as f:
88-
return yaml.safe_load(f)
114+
config = _load_yaml(pkg_config)
115+
except (FileNotFoundError, TypeError):
116+
# template = importlib.resources.files("picasso_workflow").joinpath(
117+
# "config_template.yaml"
118+
# )
119+
# config = _load_yaml(template)
120+
config = {}
121+
122+
# 2. Site-wide admin config (optional)
123+
site_config = _site_config_path()
124+
if site_config.exists():
125+
config = _deep_merge(config, _load_yaml(site_config))
126+
127+
# 3. Per-user config (optional)
128+
user_config = Path.home() / ".config" / "picasso_workflow" / "config.yaml"
129+
if user_config.exists():
130+
config = _deep_merge(config, _load_yaml(user_config))
131+
132+
return config
89133

90134

91135
config_logger()

picasso_workflow/_launcher.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Entry point wrapper for the picasso-workflow GUI.
2+
3+
Using pythonw / a windowless executable means that any uncaught exception
4+
before the Qt event loop starts is completely invisible — no terminal output,
5+
no dialog. This module wraps the real startup so that:
6+
7+
1. A crash log is always written to ~/picasso-workflow-crash.log.
8+
2. A Qt error dialog is shown if Qt can be imported at all.
9+
10+
The gui-scripts entry point in pyproject.toml points here instead of
11+
directly to picasso_workflow.gui:main.
12+
"""
13+
14+
15+
def _hide_console_window() -> None:
16+
"""Hide the console window on Windows so no black terminal flashes up.
17+
18+
console_scripts creates an exe backed by python.exe (always present in
19+
conda envs), whereas gui_scripts uses pythonw.exe which may be absent.
20+
We therefore use console_scripts and hide the window ourselves.
21+
"""
22+
import sys
23+
24+
if sys.platform != "win32":
25+
return
26+
try:
27+
import ctypes
28+
29+
hwnd = ctypes.windll.kernel32.GetConsoleWindow()
30+
if hwnd:
31+
ctypes.windll.user32.ShowWindow(hwnd, 0) # SW_HIDE
32+
except Exception:
33+
pass
34+
35+
36+
def main() -> None:
37+
_hide_console_window()
38+
39+
import sys
40+
from pathlib import Path
41+
42+
crash_log = Path.home() / "picasso-workflow-crash.log"
43+
44+
try:
45+
from picasso_workflow.gui import main as _gui_main
46+
47+
_gui_main()
48+
except SystemExit:
49+
raise
50+
except Exception:
51+
import traceback
52+
53+
msg = traceback.format_exc()
54+
55+
# Always write to a location that is writable regardless of cwd.
56+
try:
57+
crash_log.write_text(msg, encoding="utf-8")
58+
except Exception:
59+
pass
60+
61+
# Best-effort Qt dialog so the user sees something on screen.
62+
try:
63+
from PyQt5 import QtWidgets
64+
65+
_app = QtWidgets.QApplication.instance()
66+
if _app is None:
67+
_app = QtWidgets.QApplication(sys.argv)
68+
QtWidgets.QMessageBox.critical(
69+
None,
70+
"picasso-workflow failed to start",
71+
f"{msg}\n\nFull crash log:\n{crash_log}",
72+
)
73+
except Exception:
74+
pass
75+
76+
sys.exit(1)
77+
78+
79+
if __name__ == "__main__":
80+
main()

picasso_workflow/config_template.yaml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
# default located in picasso-workflow/picasso_workflow folder
2-
# user-specific version can be in ~/.config/picasso_workflow/config.yaml or %APPDATA%\picasso_workflow\config.yaml
1+
# Bundled package default — lowest priority.
2+
#
3+
# Config loading order (deep-merged, highest priority wins):
4+
# 1. Per-user: ~/.config/picasso_workflow/config.yaml
5+
# 2. Site-wide: C:\ProgramData\picasso_workflow\config.yaml (Windows)
6+
# /etc/picasso_workflow/config.yaml (macOS/Linux)
7+
# 3. This file: bundled package default
8+
#
9+
# Each file only needs to specify the keys it wants to override.
310
Templates:
411
TemplateName: "/local/path/to/template"
512
TestData:

0 commit comments

Comments
 (0)