artemis_framework 0.2.3__tar.gz → 0.3.1__tar.gz
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.
- {artemis_framework-0.2.3 → artemis_framework-0.3.1}/PKG-INFO +1 -1
- artemis_framework-0.3.1/artemis_framework/asphodel/tui/app.py +303 -0
- artemis_framework-0.3.1/artemis_framework/asphodel/tui/screens/__init__.py +0 -0
- artemis_framework-0.3.1/artemis_framework/asphodel/tui/settings.py +129 -0
- artemis_framework-0.3.1/artemis_framework/asphodel/tui/widgets/__init__.py +0 -0
- artemis_framework-0.3.1/artemis_framework/elysium/__ini +0 -0
- artemis_framework-0.3.1/artemis_framework/elysium/__init__.py +0 -0
- artemis_framework-0.3.1/artemis_framework/tartarus/__init__.py +0 -0
- {artemis_framework-0.2.3 → artemis_framework-0.3.1}/pyproject.toml +1 -1
- {artemis_framework-0.2.3 → artemis_framework-0.3.1}/LICENSE +0 -0
- {artemis_framework-0.2.3 → artemis_framework-0.3.1}/README.md +0 -0
- {artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/__init__.py +0 -0
- {artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/__main__.py +0 -0
- {artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/asphodel/__init__.py +0 -0
- /artemis_framework-0.2.3/artemis_framework/elysium/__ini → /artemis_framework-0.3.1/artemis_framework/asphodel/__main__.py +0 -0
- {artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/asphodel/core/__init__.py +0 -0
- {artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/asphodel/core/_gpg_binary.py +0 -0
- {artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/asphodel/core/armor_binary.py +0 -0
- {artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/asphodel/core/encrypt_decrypt.py +0 -0
- {artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/asphodel/core/gpg_context.py +0 -0
- {artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/asphodel/core/key_management.py +0 -0
- {artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/asphodel/core/macos_shim.py +0 -0
- {artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/asphodel/core/sign_verify.py +0 -0
- {artemis_framework-0.2.3/artemis_framework/elysium → artemis_framework-0.3.1/artemis_framework/asphodel/tui}/__init__.py +0 -0
- {artemis_framework-0.2.3/artemis_framework/tartarus → artemis_framework-0.3.1/artemis_framework/asphodel/tui/modals}/__init__.py +0 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from textual.app import App, ComposeResult
|
|
8
|
+
from textual.binding import Binding
|
|
9
|
+
from textual.screen import Screen
|
|
10
|
+
from textual.widgets import Footer, Header, Label
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from artemis_framework.asphodel.core import build_gpg
|
|
14
|
+
except ImportError as exc:
|
|
15
|
+
print(
|
|
16
|
+
f"[FATAL] Cannot import '.asphodel.core': {exc}\n"
|
|
17
|
+
"Ensure the artemis_framework.asphodel package is installed and available on your PATH."
|
|
18
|
+
)
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
from artemis_framework.asphodel.tui.settings import AppSettings, load_settings
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _import_key_management_screen():
|
|
25
|
+
from artemis_framework.asphodel.tui.screens.key_management import KeyManagementScreen
|
|
26
|
+
return KeyManagementScreen
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _import_encrypt_decrypt_screen():
|
|
30
|
+
from artemis_framework.asphodel.tui.screens.encrypt_decrypt import EncryptDecryptScreen
|
|
31
|
+
return EncryptDecryptScreen
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _import_config_screen():
|
|
35
|
+
from artemis_framework.asphodel.tui.screens.app_config import AppConfigScreen
|
|
36
|
+
return AppConfigScreen
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PlaceholderScreen(Screen):
|
|
40
|
+
|
|
41
|
+
BINDINGS = [Binding("escape,q", "app.pop_screen", "Back", priority=True)]
|
|
42
|
+
|
|
43
|
+
def __init__(self, title: str = "Coming soon", **kwargs) -> None:
|
|
44
|
+
super().__init__(**kwargs)
|
|
45
|
+
self._title = title
|
|
46
|
+
|
|
47
|
+
def compose(self) -> ComposeResult:
|
|
48
|
+
yield Header()
|
|
49
|
+
yield Label(
|
|
50
|
+
f"\n [ {self._title} ]\n\n This screen is not yet implemented.\n Press [b]Q[/b] or [b]Escape[/b] to [i][u]go back[/u][/i].",
|
|
51
|
+
markup=True,
|
|
52
|
+
id="placeholder=label"
|
|
53
|
+
)
|
|
54
|
+
yield Footer()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AsphodelApp(App):
|
|
58
|
+
TITLE = "Asphodel PGP Terminal Interface"
|
|
59
|
+
SUB_TITLE = "GnuPG key management & crypto operations"
|
|
60
|
+
|
|
61
|
+
CSS = """
|
|
62
|
+
Screen {
|
|
63
|
+
background: $background;
|
|
64
|
+
}
|
|
65
|
+
Header {
|
|
66
|
+
background: $primary;
|
|
67
|
+
color: $text;
|
|
68
|
+
height: 1;
|
|
69
|
+
dock: top;
|
|
70
|
+
}
|
|
71
|
+
Footer {
|
|
72
|
+
background: $primary-darken-2;
|
|
73
|
+
color: $text-muted;
|
|
74
|
+
height: 1;
|
|
75
|
+
dock: bottom;
|
|
76
|
+
}
|
|
77
|
+
#welcome-label {
|
|
78
|
+
padding: 1 4;
|
|
79
|
+
color: $text;
|
|
80
|
+
content-align: left top;
|
|
81
|
+
}
|
|
82
|
+
#placeholder-label {
|
|
83
|
+
padding: 2 4;
|
|
84
|
+
color: $text-muted;
|
|
85
|
+
content-align: left top;
|
|
86
|
+
}
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
BINDINGS = [
|
|
90
|
+
Binding("k", "switch_screen('keys')", "Keys"),
|
|
91
|
+
Binding("e", "switch_screen('encrypt')", "Encrypt/Decrypt"),
|
|
92
|
+
Binding("c", "switch_screen('config')", "Configuration", priority=True),
|
|
93
|
+
Binding("escape,q", "quit", "Quit"),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
def __init__(self, gpg_instance, settings: AppSettings, **kwargs) -> None:
|
|
97
|
+
super().__init__(**kwargs)
|
|
98
|
+
self.gpg = gpg_instance
|
|
99
|
+
self.settings = settings
|
|
100
|
+
self._passphrase_cache: dict[str, str] = {}
|
|
101
|
+
|
|
102
|
+
def cache_passphrase(self, fingerprint: str, passphrase: str | None) -> None:
|
|
103
|
+
if not self.settings.passphrase_cache_enabled:
|
|
104
|
+
return
|
|
105
|
+
if passphrase:
|
|
106
|
+
self._passphrase_cache[fingerprint] = passphrase
|
|
107
|
+
|
|
108
|
+
def get_cached_passphrase(self, fingerprint: str) -> str | None:
|
|
109
|
+
if not self.settings.passphrase_cache_enabled:
|
|
110
|
+
return
|
|
111
|
+
return self._passphrase_cache.get(fingerprint)
|
|
112
|
+
|
|
113
|
+
def evict_passphrase(self, fingerprint: str) -> None:
|
|
114
|
+
self._passphrase_cache.pop(fingerprint, None)
|
|
115
|
+
|
|
116
|
+
def clear_passphrase_cache(self) -> None:
|
|
117
|
+
self._passphrase_cache.clear()
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def passphrase_cache_size(self) -> int:
|
|
121
|
+
return len(self._passphrase_cache)
|
|
122
|
+
|
|
123
|
+
def on_mount(self) -> None:
|
|
124
|
+
self.install_screen(self._make_key_screen, name="keys")
|
|
125
|
+
self.install_screen(self._make_encrypt_screen, name="encrypt")
|
|
126
|
+
self.install_screen(self._make_config_screen, name="config")
|
|
127
|
+
|
|
128
|
+
def _make_key_screen(self):
|
|
129
|
+
try:
|
|
130
|
+
KeyManagementScreen = _import_key_management_screen()
|
|
131
|
+
return KeyManagementScreen(self.gpg)
|
|
132
|
+
except (ImportError, ModuleNotFoundError):
|
|
133
|
+
return PlaceholderScreen("Key Management (not yet implemented)")
|
|
134
|
+
|
|
135
|
+
def _make_encrypt_screen(self):
|
|
136
|
+
try:
|
|
137
|
+
EncryptDecryptScreen = _import_encrypt_decrypt_screen()
|
|
138
|
+
return EncryptDecryptScreen(self.gpg)
|
|
139
|
+
except (ImportError, ModuleNotFoundError):
|
|
140
|
+
return PlaceholderScreen("Encrypt / Decrypt (not yet implemented)")
|
|
141
|
+
|
|
142
|
+
def _make_config_screen(self):
|
|
143
|
+
try:
|
|
144
|
+
AppConfigScreen = _import_config_screen()
|
|
145
|
+
return AppConfigScreen(self.settings)
|
|
146
|
+
except (ImportError, ModuleNotFoundError):
|
|
147
|
+
return PlaceholderScreen("App Configuration (not yet implemented)")
|
|
148
|
+
|
|
149
|
+
def compose(self) -> ComposeResult:
|
|
150
|
+
yield Header()
|
|
151
|
+
yield Label(_WELCOME_TEXT, id="welcome-label", markup=True)
|
|
152
|
+
yield Footer()
|
|
153
|
+
|
|
154
|
+
_TOP_LEVEL_SCREENS = frozenset({
|
|
155
|
+
"KeyManagementScreen",
|
|
156
|
+
"EncryptDecryptScreen",
|
|
157
|
+
"AppConfigScreen",
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
def action_switch_screen(self, screen_name: str) -> None:
|
|
161
|
+
if self.screen.__class__.__name__ in self._TOP_LEVEL_SCREENS:
|
|
162
|
+
return
|
|
163
|
+
self.push_screen(screen_name)
|
|
164
|
+
|
|
165
|
+
def on_config_screen_saved(
|
|
166
|
+
self,
|
|
167
|
+
message: "AppConfigScreen.Saved" # type: ignore[name-defined]
|
|
168
|
+
) -> None:
|
|
169
|
+
new_cfg = message.settings
|
|
170
|
+
cache_was_on = self.settings.passphrase_cache_enabled
|
|
171
|
+
self.settings = new_cfg
|
|
172
|
+
|
|
173
|
+
if cache_was_on and not new_cfg.passphrase_cache_enabled:
|
|
174
|
+
self.clear_passphrase_cache()
|
|
175
|
+
self.notify(
|
|
176
|
+
"Passphrase cache cleared.",
|
|
177
|
+
title="Cache disabled",
|
|
178
|
+
severity="information",
|
|
179
|
+
timeout=3
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
new_gpg = new_cfg.build_gpg()
|
|
184
|
+
except RuntimeError as exc:
|
|
185
|
+
self.notify(
|
|
186
|
+
f"Settings saved, but GPG could not be reloaded:\n{exc}\n\n"
|
|
187
|
+
"The previuos keyring is still active.",
|
|
188
|
+
title="GPG reload failed",
|
|
189
|
+
severity="warning",
|
|
190
|
+
timeout=10
|
|
191
|
+
)
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
self.gpg = new_gpg
|
|
195
|
+
self.SUB_TITLE = f"keyring: {new_gpg.gnupghome}"
|
|
196
|
+
|
|
197
|
+
self._propagate_gpg_to_screens(new_gpg)
|
|
198
|
+
|
|
199
|
+
self.notify(
|
|
200
|
+
f"Keyring: {new_gpg.gnupghome}",
|
|
201
|
+
title="Settings saved",
|
|
202
|
+
severity="information",
|
|
203
|
+
timeout=4
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def _propagate_gpg_to_screens(self, gpg) -> None:
|
|
207
|
+
for name in ("keys", "encrypt"):
|
|
208
|
+
try:
|
|
209
|
+
screen = self._installed_screens.get(name)
|
|
210
|
+
if screen is None or callable(screen):
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
if hasattr(screen, "gpg"):
|
|
214
|
+
screen.gpg = gpg
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
from artemis_framework.asphodel.tui.widgets.key_list import KeyListWidget
|
|
218
|
+
for widget in screen.query(KeyListWidget):
|
|
219
|
+
widget.refresh_keys(gpg)
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
WELCOME_TEXT = """\
|
|
228
|
+
|
|
229
|
+
██████╗ ██████╗ ██████╗
|
|
230
|
+
██╔════╝ ██╔══██╗██╔════╝
|
|
231
|
+
██║ ███╗██████╔╝██║ ███╗
|
|
232
|
+
██║ ██║██╔═══╝ ██║ ██║
|
|
233
|
+
╚██████╔╝██║ ╚██████╔╝
|
|
234
|
+
╚═════╝ ╚═╝ ╚═════╝ TUI
|
|
235
|
+
|
|
236
|
+
[i]GnuPG key management & cryptographic operations.[/i]
|
|
237
|
+
|
|
238
|
+
[b][u]Navigation[/u][/b]
|
|
239
|
+
|
|
240
|
+
[b]K[/b] → Key Management [i](generate, import, export, delete keys)[/i]
|
|
241
|
+
[b]E[/b] → Encrypt / Decrypt [i](encrypt, decrypt, sign, verify)[/i]
|
|
242
|
+
[b]S[/b] → Settings [i](keyring path, GPG binary, key defaults)[/i]
|
|
243
|
+
[b]Esc[/b] → Quit
|
|
244
|
+
|
|
245
|
+
The keyring path is shown in the header subtitle.
|
|
246
|
+
[i]Use [b]`--gnupghome`[/b] on the command line to point at a different keyring.[/i]
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def parse_args() -> argparse.Namespace:
|
|
251
|
+
parser = argparse.ArgumentParser(
|
|
252
|
+
description="GnuPG key management & crypto operations",
|
|
253
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
254
|
+
epilog=__doc__
|
|
255
|
+
)
|
|
256
|
+
parser.add_argument(
|
|
257
|
+
"--gnupghome",
|
|
258
|
+
type=Path,
|
|
259
|
+
default=None,
|
|
260
|
+
metavar="PATH",
|
|
261
|
+
help=(
|
|
262
|
+
"Path to the GNUPGHOME keyring directory. "
|
|
263
|
+
"Overrides the stored setting for this session. "
|
|
264
|
+
"Defaults to the value saved in settings, or the built-in test keyring."
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
parser.add_argument(
|
|
268
|
+
"--gpg-binary",
|
|
269
|
+
type=str,
|
|
270
|
+
default=None,
|
|
271
|
+
metavar="BINARY",
|
|
272
|
+
help=(
|
|
273
|
+
"Name or full path of the GPG binary. "
|
|
274
|
+
"Overrides the stored setting for this session."
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
return parser.parse_args()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def asphodel_launch() -> None:
|
|
281
|
+
args = parse_args()
|
|
282
|
+
|
|
283
|
+
cfg = load_settings()
|
|
284
|
+
|
|
285
|
+
if args.gnupghome is not None:
|
|
286
|
+
cfg.gnupghom = str(args.gnupghome)
|
|
287
|
+
if args.gpg_binary is not None:
|
|
288
|
+
cfg.gpg_binary = str(args.gpg_binary)
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
gpg = cfg.build_gpg()
|
|
292
|
+
except RuntimeError as exc:
|
|
293
|
+
print(f"[FATAL] Failed to initialize GPG: {exc}")
|
|
294
|
+
sys.exit(1)
|
|
295
|
+
|
|
296
|
+
app = AsphodelApp(gpg_instance=gpg, settings=cfg)
|
|
297
|
+
app.SUB_TITLE = f"keyring: {gpg.gnupghome}"
|
|
298
|
+
|
|
299
|
+
app.run()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
asphodel_launch()
|
|
File without changes
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from dataclasses import asdict, dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _config_dir() -> Path:
|
|
11
|
+
if os.name == "nt":
|
|
12
|
+
base = Path(os.environ.get("APPDATA", Path.home()))
|
|
13
|
+
else:
|
|
14
|
+
base = Path(os.environ.get("XDG_CONFIG_HOME", Path.hom() / ".config"))
|
|
15
|
+
return base / "artemis-framework" / "asphodel"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
CONFIG_FILE = _config_dir() / "settings.json"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_DEFAULT_GNUPGHOME: str = ""
|
|
22
|
+
_DEFAULT_GPG_BINARY: str = "gpg"
|
|
23
|
+
_DEFAULT_ALGORITHM: str = "rsa"
|
|
24
|
+
_DEFAULT_EXPIRE: str = "2y"
|
|
25
|
+
_DEFAULT_KEYSERVER_URL: str = "https://keys.openpgp.org"
|
|
26
|
+
_DEFAULT_PASSPHRASE_CACHE: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class AppSettings:
|
|
31
|
+
gnupghome: str = field(default=_DEFAULT_GNUPGHOME)
|
|
32
|
+
gpg_binary: str = field(default=_DEFAULT_GPG_BINARY)
|
|
33
|
+
algorithm: str = field(default=_DEFAULT_ALGORITHM)
|
|
34
|
+
expire: str = field(default=_DEFAULT_EXPIRE)
|
|
35
|
+
keyserver_url: str = field(default=_DEFAULT_KEYSERVER_URL)
|
|
36
|
+
passphrase_cache_enabled: bool = field(default=_DEFAULT_PASSPHRASE_CACHE)
|
|
37
|
+
|
|
38
|
+
def gnupghome_path(self) -> Path | None:
|
|
39
|
+
if not self.gnupghome.strip():
|
|
40
|
+
return None
|
|
41
|
+
return Path(self.gnupghome).expanduser().resolve()
|
|
42
|
+
|
|
43
|
+
def gpg_binary_resolved(self) -> str:
|
|
44
|
+
return self.gpg_binary.strip() or _DEFAULT_GPG_BINARY
|
|
45
|
+
|
|
46
|
+
def keyserver_url_resolved(self) -> str:
|
|
47
|
+
url = self.keyserver_url.strip()
|
|
48
|
+
return url if url else _DEFAULT_KEYSERVER_URL
|
|
49
|
+
|
|
50
|
+
def build_gpg(self):
|
|
51
|
+
from artemis_framework.asphodel.core import build_gpg as _build_gpg
|
|
52
|
+
return _build_gpg(
|
|
53
|
+
gnupghome=self.gnupghome_path(),
|
|
54
|
+
binary=self.gpg_binary_resolved()
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def validate_gnupghome(self) -> tuple[bool, str]:
|
|
58
|
+
if not self.gnupghome.strip():
|
|
59
|
+
return True, "Use asphodel-pgp default (test keyring)"
|
|
60
|
+
p = Path(self.gnupghome).expanduser()
|
|
61
|
+
if p.exists():
|
|
62
|
+
if p.is_dir():
|
|
63
|
+
return True, str(p.resolve())
|
|
64
|
+
return False, f"Path exists but is not a directory: {p}"
|
|
65
|
+
return True, f"Will be created: {p.resolve()}"
|
|
66
|
+
|
|
67
|
+
def validate_gpg_binary(self) -> tuple[bool, str]:
|
|
68
|
+
import shutil
|
|
69
|
+
binary = self.gpg_binary_resolved()
|
|
70
|
+
found = shutil.which(binary)
|
|
71
|
+
if found:
|
|
72
|
+
return True, found
|
|
73
|
+
p = Path(binary)
|
|
74
|
+
if p.exists() and os.access(p, os.X_OK):
|
|
75
|
+
return True, str(p.resolve())
|
|
76
|
+
|
|
77
|
+
return False, f"'{binary}' not found on PATH"
|
|
78
|
+
|
|
79
|
+
def validate_keyserver_url(self) -> tuple[bool, str]:
|
|
80
|
+
url = self.keyserver_url_resolved()
|
|
81
|
+
if url.startswith("https://") or url.startswith("http://"):
|
|
82
|
+
return True, url
|
|
83
|
+
return False, f"URL must start with https:// -or- http:// (got: '{url}')"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def load_settings() -> AppSettings:
|
|
87
|
+
if not CONFIG_FILE.exists():
|
|
88
|
+
return AppSettings()
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
raw = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
92
|
+
except (json.JSONDecodeError, OSError):
|
|
93
|
+
return AppSettings()
|
|
94
|
+
|
|
95
|
+
return AppSettings(
|
|
96
|
+
gnupghome=raw.get("gnupghome", _DEFAULT_GNUPGHOME),
|
|
97
|
+
gpg_binary=raw.get("gpg_binary", _DEFAULT_GPG_BINARY),
|
|
98
|
+
algorithm=raw.get("algorithm", _DEFAULT_ALGORITHM),
|
|
99
|
+
expire=raw.get("expire", _DEFAULT_EXPIRE),
|
|
100
|
+
keyserver_url=raw.get("keyserver_url", _DEFAULT_KEYSERVER_URL),
|
|
101
|
+
passphrase_cache_enabled=bool(
|
|
102
|
+
raw.get("passphrase_cache_enabled", _DEFAULT_PASSPHRASE_CACHE)
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def save_settings(cfg: AppSettings) -> bool:
|
|
108
|
+
try:
|
|
109
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
payload = json.dumps(asdict(cfg), indent=2, ensure_ascii=False)
|
|
111
|
+
|
|
112
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
113
|
+
dir=CONFIG_FILE.parent,
|
|
114
|
+
prefix=".settings_tmp_",
|
|
115
|
+
suffix=".json"
|
|
116
|
+
)
|
|
117
|
+
try:
|
|
118
|
+
with os.fdopen(fd, "w", encoding="utf-8") as file:
|
|
119
|
+
file.write(payload)
|
|
120
|
+
Path(tmp_path).replace(CONFIG_FILE)
|
|
121
|
+
except Exception:
|
|
122
|
+
try:
|
|
123
|
+
os.unlink(tmp_path)
|
|
124
|
+
except OSError:
|
|
125
|
+
pass
|
|
126
|
+
raise
|
|
127
|
+
return True
|
|
128
|
+
except OSError:
|
|
129
|
+
return False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/asphodel/core/__init__.py
RENAMED
|
File without changes
|
{artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/asphodel/core/_gpg_binary.py
RENAMED
|
File without changes
|
{artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/asphodel/core/armor_binary.py
RENAMED
|
File without changes
|
|
File without changes
|
{artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/asphodel/core/gpg_context.py
RENAMED
|
File without changes
|
|
File without changes
|
{artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/asphodel/core/macos_shim.py
RENAMED
|
File without changes
|
{artemis_framework-0.2.3 → artemis_framework-0.3.1}/artemis_framework/asphodel/core/sign_verify.py
RENAMED
|
File without changes
|
|
File without changes
|