hypershell 2.5.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.
- hypershell/__init__.py +114 -0
- hypershell/client.py +1256 -0
- hypershell/cluster/__init__.py +378 -0
- hypershell/cluster/local.py +209 -0
- hypershell/cluster/remote.py +720 -0
- hypershell/cluster/ssh.py +351 -0
- hypershell/config.py +452 -0
- hypershell/core/__init__.py +4 -0
- hypershell/core/config.py +357 -0
- hypershell/core/exceptions.py +120 -0
- hypershell/core/fsm.py +67 -0
- hypershell/core/heartbeat.py +70 -0
- hypershell/core/logging.py +201 -0
- hypershell/core/platform.py +121 -0
- hypershell/core/queue.py +152 -0
- hypershell/core/remote.py +192 -0
- hypershell/core/signal.py +57 -0
- hypershell/core/template.py +201 -0
- hypershell/core/thread.py +56 -0
- hypershell/core/types.py +32 -0
- hypershell/data/__init__.py +137 -0
- hypershell/data/core.py +225 -0
- hypershell/data/model.py +630 -0
- hypershell/server.py +1210 -0
- hypershell/submit.py +806 -0
- hypershell/task.py +1158 -0
- hypershell-2.5.1.dist-info/LICENSE +201 -0
- hypershell-2.5.1.dist-info/METADATA +114 -0
- hypershell-2.5.1.dist-info/RECORD +31 -0
- hypershell-2.5.1.dist-info/WHEEL +4 -0
- hypershell-2.5.1.dist-info/entry_points.txt +4 -0
hypershell/config.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Geoffrey Lentner
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Manage configuration."""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# type annotations
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
# standard libs
|
|
12
|
+
import os
|
|
13
|
+
import io
|
|
14
|
+
import sys
|
|
15
|
+
import json
|
|
16
|
+
import contextlib
|
|
17
|
+
import subprocess
|
|
18
|
+
|
|
19
|
+
# external libs
|
|
20
|
+
import toml
|
|
21
|
+
from pygments.styles import STYLE_MAP as CONSOLE_THEMES
|
|
22
|
+
from cmdkit.app import Application, ApplicationGroup
|
|
23
|
+
from cmdkit.cli import Interface, ArgumentError
|
|
24
|
+
from cmdkit.config import ConfigurationError
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
from rich.syntax import Syntax
|
|
27
|
+
|
|
28
|
+
# internal libs
|
|
29
|
+
from hypershell.core.platform import path
|
|
30
|
+
from hypershell.core.types import smart_coerce
|
|
31
|
+
from hypershell.core.logging import Logger
|
|
32
|
+
from hypershell.core.exceptions import get_shared_exception_mapping
|
|
33
|
+
from hypershell.core.config import (load_file, update, ACTIVE_CONFIG_VARS,
|
|
34
|
+
default as default_config, config as full_config)
|
|
35
|
+
|
|
36
|
+
# public interface
|
|
37
|
+
__all__ = ['ConfigApp', ]
|
|
38
|
+
|
|
39
|
+
# initialize logger
|
|
40
|
+
log = Logger.with_name(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
EDIT_PROGRAM = 'hs config edit'
|
|
44
|
+
EDIT_SYNOPSIS = f'{EDIT_PROGRAM} [-h] [--system | --user | --local]'
|
|
45
|
+
EDIT_USAGE = f"""\
|
|
46
|
+
Usage:
|
|
47
|
+
{EDIT_SYNOPSIS}
|
|
48
|
+
|
|
49
|
+
Edit configuration with default editor.
|
|
50
|
+
The EDITOR/VISUAL environment variable must be set.\
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
EDIT_HELP = f"""\
|
|
54
|
+
{EDIT_USAGE}
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
--system Edit system configuration.
|
|
58
|
+
--user Edit user configuration. (default)
|
|
59
|
+
--local Edit local configuration.
|
|
60
|
+
-h, --help Show this message and exit.\
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ConfigEditApp(Application):
|
|
65
|
+
"""Edit configuration with default editor."""
|
|
66
|
+
|
|
67
|
+
interface = Interface(EDIT_PROGRAM, EDIT_USAGE, EDIT_HELP)
|
|
68
|
+
|
|
69
|
+
site_name: str = 'user'
|
|
70
|
+
site_interface = interface.add_mutually_exclusive_group()
|
|
71
|
+
site_interface.add_argument('--system', action='store_const', const='system', dest='site_name')
|
|
72
|
+
site_interface.add_argument('--user', action='store_const', const='user', dest='site_name')
|
|
73
|
+
site_interface.add_argument('--local', action='store_const', const='local', dest='site_name')
|
|
74
|
+
|
|
75
|
+
exceptions = {
|
|
76
|
+
**get_shared_exception_mapping(__name__)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def run(self: ConfigEditApp) -> None:
|
|
80
|
+
"""Business logic for `config edit`."""
|
|
81
|
+
|
|
82
|
+
editor = os.getenv('EDITOR', os.getenv('VISUAL', None))
|
|
83
|
+
if not editor:
|
|
84
|
+
raise RuntimeError('EDITOR or VISUAL environment variable not defined')
|
|
85
|
+
|
|
86
|
+
config_path = path[self.site_name].config
|
|
87
|
+
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
|
88
|
+
|
|
89
|
+
log.debug(f'Opening {config_path}')
|
|
90
|
+
log.debug(f'Editor: {editor}')
|
|
91
|
+
subprocess.run([editor, config_path])
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
GET_PROGRAM = 'hs config get'
|
|
95
|
+
GET_SYNOPSIS = f'{GET_PROGRAM} [-h] SECTION[...].VAR [-x] [-r] [--system | --user | --local | --default]'
|
|
96
|
+
GET_USAGE = f"""\
|
|
97
|
+
Usage:
|
|
98
|
+
{GET_SYNOPSIS}
|
|
99
|
+
Get configuration option.\
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
GET_HELP = f"""\
|
|
103
|
+
{GET_USAGE}
|
|
104
|
+
|
|
105
|
+
If source is not specified, the output is the merged configuration
|
|
106
|
+
from all sources. Use `hs config which` to see where a specific
|
|
107
|
+
option originates from.
|
|
108
|
+
|
|
109
|
+
If a single value is requested, use -r/--raw to strip formatting.
|
|
110
|
+
|
|
111
|
+
Arguments:
|
|
112
|
+
SECTION[...].VAR Path to variable (default: '.').
|
|
113
|
+
|
|
114
|
+
Options:
|
|
115
|
+
--system Load from system configuration.
|
|
116
|
+
--user Load from user configuration.
|
|
117
|
+
--local Load from local configuration.
|
|
118
|
+
--default Load from default configuration.
|
|
119
|
+
-x, --expand Expand variable.
|
|
120
|
+
-r, --raw Disable formatting on single value output.
|
|
121
|
+
-h, --help Show this message and exit.\
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ConfigGetApp(Application):
|
|
126
|
+
"""Get configuration option."""
|
|
127
|
+
|
|
128
|
+
interface = Interface(GET_PROGRAM, GET_USAGE, GET_HELP)
|
|
129
|
+
|
|
130
|
+
varpath: str = None
|
|
131
|
+
interface.add_argument('varpath', nargs='?', default='.')
|
|
132
|
+
|
|
133
|
+
site_name: str = None
|
|
134
|
+
site_interface = interface.add_mutually_exclusive_group()
|
|
135
|
+
site_interface.add_argument('--system', action='store_const', const='system', dest='site_name')
|
|
136
|
+
site_interface.add_argument('--user', action='store_const', const='user', dest='site_name')
|
|
137
|
+
site_interface.add_argument('--local', action='store_const', const='local', dest='site_name')
|
|
138
|
+
site_interface.add_argument('--default', action='store_const', const='default', dest='site_name')
|
|
139
|
+
|
|
140
|
+
expand: bool = False
|
|
141
|
+
interface.add_argument('-x', '--expand', action='store_true')
|
|
142
|
+
|
|
143
|
+
raw_mode: bool = False
|
|
144
|
+
interface.add_argument('-r', '--raw', action='store_true', dest='raw_mode')
|
|
145
|
+
|
|
146
|
+
# Hidden options used as helpers for completion script
|
|
147
|
+
list_available: bool = False
|
|
148
|
+
list_console_themes: bool = False
|
|
149
|
+
completion_interface = interface.add_mutually_exclusive_group()
|
|
150
|
+
completion_interface.add_argument('--list-available', action='version', version=' '.join(ACTIVE_CONFIG_VARS))
|
|
151
|
+
completion_interface.add_argument('--list-console-themes', action='version',
|
|
152
|
+
version=' '.join(list(CONSOLE_THEMES)))
|
|
153
|
+
|
|
154
|
+
exceptions = {
|
|
155
|
+
**get_shared_exception_mapping(__name__)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
def run(self: ConfigGetApp) -> None:
|
|
159
|
+
"""Business logic for `config get`."""
|
|
160
|
+
|
|
161
|
+
if self.site_name is None:
|
|
162
|
+
config_path = 'configuration' # Note: not meaningful for merged configuration
|
|
163
|
+
config = full_config
|
|
164
|
+
elif self.site_name == 'default':
|
|
165
|
+
config_path = 'default'
|
|
166
|
+
config = default_config
|
|
167
|
+
else:
|
|
168
|
+
config_path = path[self.site_name].config
|
|
169
|
+
if os.path.exists(config_path):
|
|
170
|
+
config = load_file(config_path)
|
|
171
|
+
else:
|
|
172
|
+
raise ConfigurationError(f'{config_path} does not exist')
|
|
173
|
+
|
|
174
|
+
if self.varpath == '.':
|
|
175
|
+
self.print_output(config)
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
if '.' not in self.varpath:
|
|
179
|
+
if self.varpath in config:
|
|
180
|
+
self.print_output(config[self.varpath])
|
|
181
|
+
return
|
|
182
|
+
else:
|
|
183
|
+
raise ConfigurationError(f'"{self.varpath}" not found in {config_path}')
|
|
184
|
+
|
|
185
|
+
if self.varpath.startswith('.'):
|
|
186
|
+
raise ConfigurationError(f'Section name cannot start with "."')
|
|
187
|
+
|
|
188
|
+
section, *subsections, variable = self.varpath.split('.')
|
|
189
|
+
if section not in config:
|
|
190
|
+
raise ConfigurationError(f'"{section}" is not a section in {config_path}')
|
|
191
|
+
|
|
192
|
+
config_section = config[section]
|
|
193
|
+
if subsections:
|
|
194
|
+
subpath = f'{section}'
|
|
195
|
+
try:
|
|
196
|
+
for subsection in subsections:
|
|
197
|
+
subpath += f'.{subsection}'
|
|
198
|
+
if not isinstance(config_section[subsection], dict):
|
|
199
|
+
raise ConfigurationError(f'"{subpath}" not a section in {config_path}')
|
|
200
|
+
else:
|
|
201
|
+
config_section = config_section[subsection]
|
|
202
|
+
except KeyError as error:
|
|
203
|
+
raise ConfigurationError(f'"{subpath}" not found in {config_path}') from error
|
|
204
|
+
|
|
205
|
+
if self.expand:
|
|
206
|
+
try:
|
|
207
|
+
value = getattr(config_section, variable)
|
|
208
|
+
except AttributeError as error:
|
|
209
|
+
raise ConfigurationError('') from error
|
|
210
|
+
if value is None:
|
|
211
|
+
raise ConfigurationError(f'"{variable}" not found in {config_path}')
|
|
212
|
+
self.print_output(value)
|
|
213
|
+
return
|
|
214
|
+
elif variable in config_section:
|
|
215
|
+
self.print_output(config_section[variable])
|
|
216
|
+
else:
|
|
217
|
+
raise ConfigurationError(f'"{self.varpath}" not found in {config_path}')
|
|
218
|
+
|
|
219
|
+
def print_output(self: ConfigGetApp, value: Any) -> None:
|
|
220
|
+
"""Format and print final `value`."""
|
|
221
|
+
value = self.format_output(value)
|
|
222
|
+
if sys.stdout.isatty() and not self.raw_mode:
|
|
223
|
+
output = Syntax(value, 'toml', word_wrap=True,
|
|
224
|
+
theme = full_config.console.theme,
|
|
225
|
+
background_color = 'default')
|
|
226
|
+
Console().print(output)
|
|
227
|
+
else:
|
|
228
|
+
# NOTE: JSON formatting puts quotations - we don't want these on raw output
|
|
229
|
+
print(value.strip('"'), file=sys.stdout, flush=True)
|
|
230
|
+
|
|
231
|
+
def format_output(self: ConfigGetApp, value: Any) -> str:
|
|
232
|
+
"""Format `value` as appropriate text."""
|
|
233
|
+
if isinstance(value, dict):
|
|
234
|
+
value = self.format_section(value)
|
|
235
|
+
else:
|
|
236
|
+
value = json.dumps(value) # NOTE: close enough
|
|
237
|
+
return value
|
|
238
|
+
|
|
239
|
+
def format_section(self: ConfigGetApp, value: dict) -> str:
|
|
240
|
+
"""Format an entire section for output."""
|
|
241
|
+
if self.varpath == '.':
|
|
242
|
+
value = toml.dumps(value)
|
|
243
|
+
else:
|
|
244
|
+
value = toml.dumps({self.varpath: value})
|
|
245
|
+
# NOTE: Fix weird formatting of section headings.
|
|
246
|
+
# The `toml.dumps` output has unnecessary quoting.
|
|
247
|
+
lines = []
|
|
248
|
+
for line in value.strip().split('\n'):
|
|
249
|
+
if not line.startswith('['):
|
|
250
|
+
lines.append(line)
|
|
251
|
+
else:
|
|
252
|
+
lines.append(line.replace('"', ''))
|
|
253
|
+
value = '\n'.join(lines)
|
|
254
|
+
return value
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
SET_PROGRAM = 'hs config set'
|
|
258
|
+
SET_SYNOPSIS = f'{SET_PROGRAM} [-h] SECTION[...].VAR VALUE [--system | --user | --local]'
|
|
259
|
+
SET_USAGE = f"""\
|
|
260
|
+
Usage:
|
|
261
|
+
{SET_SYNOPSIS}
|
|
262
|
+
Set configuration option.\
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
SET_HELP = f"""\
|
|
266
|
+
{SET_USAGE}
|
|
267
|
+
|
|
268
|
+
Arguments:
|
|
269
|
+
SECTION[...].VAR Path to variable.
|
|
270
|
+
VALUE Value to be set.
|
|
271
|
+
|
|
272
|
+
Options:
|
|
273
|
+
--system Apply to system configuration.
|
|
274
|
+
--user Apply to user configuration. (default)
|
|
275
|
+
--local Apply to local configuration.
|
|
276
|
+
-h, --help Show this message and exit.\
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class ConfigSetApp(Application):
|
|
281
|
+
"""Set configuration option."""
|
|
282
|
+
|
|
283
|
+
interface = Interface(SET_PROGRAM, SET_USAGE, SET_HELP)
|
|
284
|
+
|
|
285
|
+
varpath: str = None
|
|
286
|
+
interface.add_argument('varpath', metavar='VAR')
|
|
287
|
+
|
|
288
|
+
value: str = None
|
|
289
|
+
interface.add_argument('value', type=smart_coerce)
|
|
290
|
+
|
|
291
|
+
site_name: str = 'user'
|
|
292
|
+
site_interface = interface.add_mutually_exclusive_group()
|
|
293
|
+
site_interface.add_argument('--user', action='store_const', const='user', dest='site_name', default=site_name)
|
|
294
|
+
site_interface.add_argument('--system', action='store_const', const='system', dest='site_name')
|
|
295
|
+
site_interface.add_argument('--local', action='store_const', const='local', dest='site_name')
|
|
296
|
+
|
|
297
|
+
exceptions = {
|
|
298
|
+
**get_shared_exception_mapping(__name__)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
def run(self: ConfigSetApp) -> None:
|
|
302
|
+
"""Business logic for `config set`."""
|
|
303
|
+
|
|
304
|
+
if '.' not in self.varpath:
|
|
305
|
+
raise ArgumentError('Missing section in variable path')
|
|
306
|
+
|
|
307
|
+
section, *subsections, variable = self.varpath.split('.')
|
|
308
|
+
|
|
309
|
+
config = {section: {}}
|
|
310
|
+
config_section = config[section]
|
|
311
|
+
for subsection in subsections:
|
|
312
|
+
if subsection not in config_section:
|
|
313
|
+
config_section[subsection] = dict()
|
|
314
|
+
config_section = config_section[subsection]
|
|
315
|
+
|
|
316
|
+
config_section[variable] = self.value
|
|
317
|
+
update(self.site_name, config)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
WHICH_PROGRAM = 'hs config which'
|
|
321
|
+
WHICH_SYNOPSIS = f'{WHICH_PROGRAM} [-h] SECTION[...].VAR [--site]'
|
|
322
|
+
WHICH_USAGE = f"""\
|
|
323
|
+
Usage:
|
|
324
|
+
{WHICH_SYNOPSIS}
|
|
325
|
+
Show origin of configuration option.\
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
WHICH_HELP = f"""\
|
|
329
|
+
{WHICH_USAGE}
|
|
330
|
+
|
|
331
|
+
Arguments:
|
|
332
|
+
SECTION[...].VAR Path to variable.
|
|
333
|
+
|
|
334
|
+
Options:
|
|
335
|
+
--site Output originating site name only.
|
|
336
|
+
-h, --help Show this message and exit.\
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class ConfigWhichApp(Application):
|
|
341
|
+
"""Show origin of configuration option."""
|
|
342
|
+
|
|
343
|
+
interface = Interface(WHICH_PROGRAM, WHICH_USAGE, WHICH_HELP)
|
|
344
|
+
|
|
345
|
+
varpath: str = None
|
|
346
|
+
interface.add_argument('varpath', metavar='VAR')
|
|
347
|
+
|
|
348
|
+
site_only: bool = False
|
|
349
|
+
interface.add_argument('--site', action='store_true', dest='site_only')
|
|
350
|
+
|
|
351
|
+
exceptions = {
|
|
352
|
+
**get_shared_exception_mapping(__name__)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
def run(self: ConfigWhichApp) -> None:
|
|
356
|
+
"""Business logic for `config which`."""
|
|
357
|
+
try:
|
|
358
|
+
site = full_config.which(*self.varpath.split('.'))
|
|
359
|
+
except KeyError:
|
|
360
|
+
log.critical(f'"{self.varpath}" not found')
|
|
361
|
+
return
|
|
362
|
+
if self.site_only:
|
|
363
|
+
print(site)
|
|
364
|
+
return
|
|
365
|
+
try:
|
|
366
|
+
with contextlib.redirect_stdout(io.StringIO()) as stdout:
|
|
367
|
+
with ConfigGetApp.from_cmdline([self.varpath, '--raw']) as app:
|
|
368
|
+
app.run()
|
|
369
|
+
except ConfigurationError:
|
|
370
|
+
value = 'null'
|
|
371
|
+
else:
|
|
372
|
+
value = stdout.getvalue().strip()
|
|
373
|
+
try:
|
|
374
|
+
with contextlib.redirect_stdout(io.StringIO()) as stdout:
|
|
375
|
+
with ConfigGetApp.from_cmdline([self.varpath, '--raw', '--default']) as app:
|
|
376
|
+
app.run()
|
|
377
|
+
except ConfigurationError:
|
|
378
|
+
default_value = 'null'
|
|
379
|
+
else:
|
|
380
|
+
default_value = stdout.getvalue().strip()
|
|
381
|
+
if '[' in value:
|
|
382
|
+
value = '[...]'
|
|
383
|
+
if '[' in default_value:
|
|
384
|
+
default_value = '[...]'
|
|
385
|
+
if site in ('default', 'logging', ):
|
|
386
|
+
print(f'{value} ({site})')
|
|
387
|
+
elif site == 'env':
|
|
388
|
+
env_varname = 'HYPERSHELL_' + self.varpath.upper().replace('.', '_')
|
|
389
|
+
if value == '[...]':
|
|
390
|
+
for name in full_config.namespaces.env.to_env().flatten(prefix='HYPERSHELL'):
|
|
391
|
+
if name.startswith(env_varname):
|
|
392
|
+
env_varname = name
|
|
393
|
+
print(f'{value} (env: {env_varname} | default: {default_value})')
|
|
394
|
+
else:
|
|
395
|
+
print(f'{value} ({site}: {path[site].config} | default: {default_value})')
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
if os.name == 'nt':
|
|
399
|
+
CONFIG_PATH_INFO = f"""\
|
|
400
|
+
--system %ProgramData%\\HyperShell\\Config.toml
|
|
401
|
+
--user %AppData%\\HyperShell\\Config.toml
|
|
402
|
+
--local {path.local.config}
|
|
403
|
+
"""
|
|
404
|
+
else:
|
|
405
|
+
CONFIG_PATH_INFO = f"""\
|
|
406
|
+
--system /etc/hypershell.toml
|
|
407
|
+
--user ~/.hypershell/config.toml
|
|
408
|
+
--local {path.local.config}
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
PROGRAM = 'hs config'
|
|
413
|
+
USAGE = f"""\
|
|
414
|
+
Usage:
|
|
415
|
+
{PROGRAM} [-h]
|
|
416
|
+
{GET_SYNOPSIS}
|
|
417
|
+
{SET_SYNOPSIS}
|
|
418
|
+
{EDIT_SYNOPSIS}
|
|
419
|
+
{WHICH_SYNOPSIS}
|
|
420
|
+
|
|
421
|
+
{__doc__}\
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
HELP = f"""\
|
|
425
|
+
{USAGE}
|
|
426
|
+
|
|
427
|
+
Commands:
|
|
428
|
+
get {ConfigGetApp.__doc__}
|
|
429
|
+
set {ConfigSetApp.__doc__}
|
|
430
|
+
edit {ConfigEditApp.__doc__}
|
|
431
|
+
which {ConfigWhichApp.__doc__}
|
|
432
|
+
|
|
433
|
+
Options:
|
|
434
|
+
-h, --help Show this message and exit.
|
|
435
|
+
|
|
436
|
+
Files:
|
|
437
|
+
{CONFIG_PATH_INFO}\
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class ConfigApp(ApplicationGroup):
|
|
442
|
+
"""Manage configuration."""
|
|
443
|
+
|
|
444
|
+
interface = Interface(PROGRAM, USAGE, HELP)
|
|
445
|
+
|
|
446
|
+
interface.add_argument('command')
|
|
447
|
+
|
|
448
|
+
command = None
|
|
449
|
+
commands = {'get': ConfigGetApp,
|
|
450
|
+
'set': ConfigSetApp,
|
|
451
|
+
'edit': ConfigEditApp,
|
|
452
|
+
'which': ConfigWhichApp, }
|