09e86bfa6549230d20c41600a57c3a7c37bb9397
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

1) # SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info>
2) #
3) # SPDX-License-Identifier: MIT
4) 
5) """Command-line interface for derivepassphrase.
6) 
7) """
8) 
9) from __future__ import annotations
10) 
11) import inspect
12) import json
13) import pathlib
14) from typing import Any, TextIO
15) 
16) import click
17) import derivepassphrase as dpp
18) from derivepassphrase import types as dpp_types
19) 
20) __author__ = dpp.__author__
21) __version__ = dpp.__version__
22) 
23) __all__ = ('derivepassphrase',)
24) 
25) prog_name = 'derivepassphrase'
26) 
27) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

28) def _config_filename() -> str | bytes | pathlib.Path:
29)     """Return the filename of the configuration file.
30) 
31)     The file is currently named `settings.json`, located within the
32)     configuration directory as determined by the `DERIVEPASSPHRASE_PATH`
33)     environment variable, or by [`click.get_app_dir`][] in POSIX
34)     mode.
35) 
36)     """
37)     path: str | bytes | pathlib.Path
38)     path = (os.getenv(prog_name.upper() + '_PATH')
39)             or click.get_app_dir(prog_name, force_posix=True))
40)     return os.path.join(path, 'settings.json')
41) 
42) 
43) def _load_config() -> dpp_types.VaultConfig:
44)     """Load a vault(1)-compatible config from the application directory.
45) 
46)     The filename is obtained via
47)     [`derivepassphrase.cli._config_filename`][].  This must be an
48)     unencrypted JSON file.
49) 
50)     Returns:
51)         The vault settings.  See
52)         [`derivepassphrase.types.VaultConfig`][] for details.
53) 
54)     Raises:
55)         OSError:
56)             There was an OS error accessing the file.
57)         ValueError:
58)             The data loaded from the file is not a vault(1)-compatible
59)             config.
60) 
61)     """
62)     filename = _config_filename()
63)     with open(filename, 'rb') as fileobj:
64)         data = json.load(fileobj)
65)     if not dpp_types.is_vault_config(data):
66)         raise ValueError('Invalid vault config')
67)     return data
68) 
69) 
70) def _save_config(config: dpp_types.VaultConfig, /) -> None:
71)     """Save a vault(1)-compatbile config to the application directory.
72) 
73)     The filename is obtained via
74)     [`derivepassphrase.cli._config_filename`][].  The config will be
75)     stored as an unencrypted JSON file.
76) 
77)     Args:
78)         config:
79)             vault configuration to save.
80) 
81)     Raises:
82)         OSError:
83)             There was an OS error accessing or writing the file.
84)         ValueError:
85)             The data cannot be stored as a vault(1)-compatible config.
86) 
87)     """
88)     if not dpp_types.is_vault_config(config):
89)         raise ValueError('Invalid vault config')
90)     filename = _config_filename()
91)     with open(filename, 'wt', encoding='UTF-8') as fileobj:
92)         json.dump(config, fileobj)
93) 
94) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

95) class OptionGroupOption(click.Option):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

96)     """A [`click.Option`][] with an associated group name and group epilog.
97) 
98)     Used by [`derivepassphrase.cli.CommandWithHelpGroups`][] to print
99)     help sections.  Each subclass contains its own group name and
100)     epilog.
101) 
102)     Attributes:
103)         option_group_name:
104)             The name of the option group.  Used as a heading on the help
105)             text for options in this section.
106)         epilog:
107)             An epilog to print after listing the options in this
108)             section.
109) 
110)     """
111)     option_group_name: str = ''
112)     epilog: str = ''
113) 
114)     def __init__(self, *args, **kwargs):  # type: ignore
115)         if self.__class__ == __class__:
116)             raise NotImplementedError()
117)         return super().__init__(*args, **kwargs)
118) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

119) 
120) class CommandWithHelpGroups(click.Command):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

121)     """A [`click.Command`][] with support for help/option groups.
122) 
123)     Inspired by [a comment on `pallets/click#373`][CLICK_ISSUE], and
124)     further modified to support group epilogs.
125) 
126)     [CLICK_ISSUE]: https://github.com/pallets/click/issues/373#issuecomment-515293746
127) 
128)     """
129) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

130)     def format_options(
131)         self, ctx: click.Context, formatter: click.HelpFormatter,
132)     ) -> None:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

133)         r"""Format options on the help listing, grouped into sections.
134) 
135)         As part of the `--help` listing, list all options, but grouped
136)         into sections according to the concrete [`click.Option`][]
137)         subclass being used.  If the option is an instance of some
138)         subclass `X` of [`derivepassphrase.cli.OptionGroupOption`][],
139)         then the section heading and the epilog is taken from
140)         `X.option_group_name` and `X.epilog`; otherwise, the section
141)         heading is "Options" (or "Other options" if there are other
142)         option groups) and the epilog is empty.
143) 
144)         Args:
145)             ctx:
146)                 The click context.
147)             formatter:
148)                 The formatter for the `--help` listing.
149) 
150)         """
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

151)         help_records: dict[str, list[tuple[str, str]]] = {}
152)         epilogs: dict[str, str] = {}
153)         params = self.params[:]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

154)         if (  # pragma: no branch
155)             (help_opt := self.get_help_option(ctx)) is not None
156)             and help_opt not in params
157)         ):
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

158)             params.append(help_opt)
159)         for param in params:
160)             rec = param.get_help_record(ctx)
161)             if rec is not None:
162)                 if isinstance(param, OptionGroupOption):
163)                     group_name = param.option_group_name
164)                     epilogs.setdefault(group_name, param.epilog)
165)                 else:
166)                     group_name = ''
167)                 help_records.setdefault(group_name, []).append(rec)
168)         default_group = help_records.pop('')
169)         default_group_name = ('Other Options' if len(default_group) > 1
170)                               else 'Options')
171)         help_records[default_group_name] = default_group
172)         for group_name, records in help_records.items():
173)             with formatter.section(group_name):
174)                 formatter.write_dl(records)
175)             epilog = inspect.cleandoc(epilogs.get(group_name, ''))
176)             if epilog:
177)                 formatter.write_paragraph()
178)                 with formatter.indentation():
179)                     formatter.write_text(epilog)
180) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

181) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

182) # Concrete option groups used by this command-line interface.
183) class PasswordGenerationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

184)     """Password generation options for the CLI."""
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

185)     option_group_name = 'Password generation'
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

186)     epilog = '''
187)         Use NUMBER=0, e.g. "--symbol 0", to exclude a character type
188)         from the output.
189)     '''
190) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

191) 
192) class ConfigurationOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

193)     """Configuration options for the CLI."""
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

194)     option_group_name = 'Configuration'
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

195)     epilog = '''
196)         Use $VISUAL or $EDITOR to configure the spawned editor.
197)     '''
198) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

199) 
200) class StorageManagementOption(OptionGroupOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

201)     """Storage management options for the CLI."""
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

202)     option_group_name = 'Storage management'
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

203)     epilog = '''
204)         Using "-" as PATH for standard input/standard output is
205)         supported.
206)     '''
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

207) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

208) def _validate_occurrence_constraint(
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

209)     ctx: click.Context, param: click.Parameter, value: Any,
210) ) -> int | None:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

211)     """Check that the occurrence constraint is valid (int, 0 or larger)."""
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

212)     if value is None:
213)         return value
214)     if isinstance(value, int):
215)         int_value = value
216)     else:
217)         try:
218)             int_value = int(value, 10)
219)         except ValueError as e:
220)             raise click.BadParameter('not an integer') from e
221)     if int_value < 0:
222)         raise click.BadParameter('not a non-negative integer')
223)     return int_value
224) 
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

225) 
226) def _validate_length(
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

227)     ctx: click.Context, param: click.Parameter, value: Any,
228) ) -> int | None:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

229)     """Check that the length is valid (int, 1 or larger)."""
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

230)     if value is None:
231)         return value
232)     if isinstance(value, int):
233)         int_value = value
234)     else:
235)         try:
236)             int_value = int(value, 10)
237)         except ValueError as e:
238)             raise click.BadParameter('not an integer') from e
239)     if int_value < 1:
240)         raise click.BadParameter('not a positive integer')
241)     return int_value
242) 
243) @click.command(
244)     context_settings={"help_option_names": ["-h", "--help"]},
245)     cls=CommandWithHelpGroups,
246)     epilog='''
247)         WARNING: There is NO WAY to retrieve the generated passphrases
248)         if the master passphrase, the SSH key, or the exact passphrase
249)         settings are lost, short of trying out all possible
250)         combinations.  You are STRONGLY advised to keep independent
251)         backups of the settings and the SSH key, if any.
252)     ''',
253) )
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

254) @click.option('-p', '--phrase', 'use_phrase', is_flag=True,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

255)               help='prompts you for your passphrase',
256)               cls=PasswordGenerationOption)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

257) @click.option('-k', '--key', 'use_key', is_flag=True,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

258)               help='uses your SSH private key to generate passwords',
259)               cls=PasswordGenerationOption)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

260) @click.option('-l', '--length', metavar='NUMBER',
261)               callback=_validate_length,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

262)               help='emits password of length NUMBER',
263)               cls=PasswordGenerationOption)
264) @click.option('-r', '--repeat', metavar='NUMBER',
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

265)               callback=_validate_occurrence_constraint,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

266)               help='allows maximum of NUMBER repeated adjacent chars',
267)               cls=PasswordGenerationOption)
268) @click.option('--lower', metavar='NUMBER',
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

269)               callback=_validate_occurrence_constraint,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

270)               help='includes at least NUMBER lowercase letters',
271)               cls=PasswordGenerationOption)
272) @click.option('--upper', metavar='NUMBER',
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

273)               callback=_validate_occurrence_constraint,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

274)               help='includes at least NUMBER uppercase letters',
275)               cls=PasswordGenerationOption)
276) @click.option('--number', metavar='NUMBER',
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

277)               callback=_validate_occurrence_constraint,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

278)               help='includes at least NUMBER digits',
279)               cls=PasswordGenerationOption)
280) @click.option('--space', metavar='NUMBER',
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

281)               callback=_validate_occurrence_constraint,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

282)               help='includes at least NUMBER spaces',
283)               cls=PasswordGenerationOption)
284) @click.option('--dash', metavar='NUMBER',
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

285)               callback=_validate_occurrence_constraint,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

286)               help='includes at least NUMBER "-" or "_"',
287)               cls=PasswordGenerationOption)
288) @click.option('--symbol', metavar='NUMBER',
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

289)               callback=_validate_occurrence_constraint,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

290)               help='includes at least NUMBER symbol chars',
291)               cls=PasswordGenerationOption)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

292) @click.option('-n', '--notes', 'edit_notes', is_flag=True,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

293)               help='spawn an editor to edit notes for SERVICE',
294)               cls=ConfigurationOption)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

295) @click.option('-c', '--config', 'store_config_only', is_flag=True,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

296)               help='saves the given settings for SERVICE or global',
297)               cls=ConfigurationOption)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

298) @click.option('-x', '--delete', 'delete_service_settings', is_flag=True,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

299)               help='deletes settings for SERVICE',
300)               cls=ConfigurationOption)
301) @click.option('--delete-globals', is_flag=True,
302)               help='deletes the global shared settings',
303)               cls=ConfigurationOption)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

304) @click.option('-X', '--clear', 'clear_all_settings', is_flag=True,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

305)               help='deletes all settings',
306)               cls=ConfigurationOption)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

307) @click.option('-e', '--export', 'export_settings', metavar='PATH',
308)               type=click.Path(file_okay=True, allow_dash=True, exists=False),
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

309)               help='export all saved settings into file PATH',
310)               cls=StorageManagementOption)
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

311) @click.option('-i', '--import', 'import_settings', metavar='PATH',
312)               type=click.Path(file_okay=True, allow_dash=True, exists=False),
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

313)               help='import saved settings from file PATH',
314)               cls=StorageManagementOption)
315) @click.version_option(version=dpp.__version__, prog_name=prog_name)
316) @click.argument('service', required=False)
317) @click.pass_context
318) def derivepassphrase(
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

319)     ctx: click.Context, /, *,
320)     service: str | None = None,
321)     use_phrase: bool = False,
322)     use_key: bool = False,
323)     length: int | None = None,
324)     repeat: int | None = None,
325)     lower: int | None = None,
326)     upper: int | None = None,
327)     number: int | None = None,
328)     space: int | None = None,
329)     dash: int | None = None,
330)     symbol: int | None = None,
331)     edit_notes: bool = False,
332)     store_config_only: bool = False,
333)     delete_service_settings: bool = False,
334)     delete_globals: bool = False,
335)     clear_all_settings: bool = False,
336)     export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
337)     import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

338) ) -> None:
339)     """Derive a strong passphrase, deterministically, from a master secret.
340) 
341)     Using a master passphrase or a master SSH key, derive a strong
342)     passphrase for SERVICE, deterministically, subject to length,
343)     character and character repetition constraints.  The service name
344)     and constraints themselves need not be kept secret; the latter are
345)     usually stored in a world-readable file.
346) 
347)     If operating on global settings, or importing/exporting settings,
348)     then SERVICE must be omitted.  Otherwise it is required.\f
349) 
350)     This is a [`click`][CLICK]-powered command-line interface function,
351)     and not intended for programmatic use.  Call with arguments
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

352)     `['--help']` to see full documentation of the interface.  (See also
353)     [`click.testing.CliRunner`][] for controlled, programmatic
354)     invocation.)
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

355) 
356)     [CLICK]: https://click.palletsprojects.com/
357) 
358)     Parameters:
359)         ctx (click.Context):
360)             The `click` context.
361) 
362)     Other Parameters:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

363)         service:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

364)             A service name.  Required, unless operating on global
365)             settings or importing/exporting settings.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

366)         use_phrase:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

367)             Command-line argument `-p`/`--phrase`.  If given, query the
368)             user for a passphrase instead of an SSH key.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

369)         use_key:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

370)             Command-line argument `-k`/`--key`.  If given, query the
371)             user for an SSH key instead of a passphrase.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

372)         length:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

373)             Command-line argument `-l`/`--length`.  Override the default
374)             length of the generated passphrase.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

375)         repeat:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

376)             Command-line argument `-r`/`--repeat`.  Override the default
377)             repetition limit if positive, or disable the repetition
378)             limit if 0.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

379)         lower:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

380)             Command-line argument `--lower`.  Require a given amount of
381)             ASCII lowercase characters if positive, else forbid ASCII
382)             lowercase characters if 0.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

383)         upper:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

384)             Command-line argument `--upper`.  Same as `lower`, but for
385)             ASCII uppercase characters.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

386)         number:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

387)             Command-line argument `--number`.  Same as `lower`, but for
388)             ASCII digits.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

389)         space:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

390)             Command-line argument `--number`.  Same as `lower`, but for
391)             the space character.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

392)         dash:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

393)             Command-line argument `--number`.  Same as `lower`, but for
394)             the hyphen-minus and underscore characters.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

395)         symbol:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

396)             Command-line argument `--number`.  Same as `lower`, but for
397)             all other ASCII printable characters (except backquote).
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

398)         edit_notes:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

399)             Command-line argument `-n`/`--notes`.  If given, spawn an
400)             editor to edit notes for `service`.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

401)         store_config_only:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

402)             Command-line argument `-c`/`--config`.  If given, saves the
403)             other given settings (`--key`, ..., `--symbol`) to the
404)             configuration file, either specifically for `service` or as
405)             global settings.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

406)         delete_service_settings:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

407)             Command-line argument `-x`/`--delete`.  If given, removes
408)             the settings for `service` from the configuration file.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

409)         delete_globals:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

410)             Command-line argument `--delete-globals`.  If given, removes
411)             the global settings from the configuration file.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

412)         clear_all_settings:
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

413)             Command-line argument `-X`/`--clear`.  If given, removes all
414)             settings from the configuration file.
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

415)         export_settings:
416)             Command-line argument `-e`/`--export`.  If a file object,
417)             then it must be open for writing and accept `str` inputs.
418)             Otherwise, a filename to open for writing.  Using `-` for
419)             standard output is supported.
420)         import_settings:
421)             Command-line argument `-i`/`--import`.  If a file object, it
422)             must be open for reading and yield `str` values.  Otherwise,
423)             a filename to open for reading.  Using `-` for standard
424)             input is supported.
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

425) 
426)     """
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

427) 
428)     options_in_group: dict[type[click.Option], list[click.Option]] = {}
429)     params_by_str: dict[str, click.Parameter] = {}
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

430)     for param in ctx.command.params:
431)         if isinstance(param, click.Option):
432)             group: type[click.Option]
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

433)             match param:
434)                 case PasswordGenerationOption():
435)                     group = PasswordGenerationOption
436)                 case ConfigurationOption():
437)                     group = ConfigurationOption
438)                 case StorageManagementOption():
439)                     group = StorageManagementOption
440)                 case OptionGroupOption():
441)                     raise AssertionError(
442)                         f'Unknown option group for {param!r}')
443)                 case _:
444)                     group = click.Option
445)             options_in_group.setdefault(group, []).append(param)
446)         params_by_str[param.human_readable_name] = param
447)         for name in param.opts + param.secondary_opts:
448)             params_by_str[name] = param
449) 
450)     def is_param_set(param: click.Parameter):
451)         return bool(ctx.params.get(param.human_readable_name))
452) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

453)     def check_incompatible_options(
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

454)         param: click.Parameter | str, *incompatible: click.Parameter | str,
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

455)     ) -> None:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

456)         if isinstance(param, str):
457)             param = params_by_str[param]
458)         assert isinstance(param, click.Parameter)
459)         if not is_param_set(param):
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

460)             return
461)         for other in incompatible:
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

462)             if isinstance(other, str):
463)                 other = params_by_str[other]
464)             assert isinstance(other, click.Parameter)
465)             if other != param and is_param_set(other):
466)                 opt_str = param.opts[0]
467)                 other_str = other.opts[0]
468)                 raise click.BadOptionUsage(
469)                     opt_str, f'mutually exclusive with {other_str}', ctx=ctx)
470) 
471)     def get_config() -> dpp_types.VaultConfig:
472)         try:
473)             return _load_config()
474)         except FileNotFoundError:
475)             return {'services': {}}
476)         except Exception as e:
477)             ctx.fail(f'cannot load config: {e}')
478) 
479)     configuration: dpp_types.VaultConfig
480) 
481)     check_incompatible_options('--phrase', '--key')
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

482)     for group in (ConfigurationOption, StorageManagementOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

483)         for opt in options_in_group[group]:
484)             if opt != params_by_str['--config']:
485)                 check_incompatible_options(
486)                     opt, *options_in_group[PasswordGenerationOption])
487) 
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

488)     for group in (ConfigurationOption, StorageManagementOption):
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

489)         for opt in options_in_group[group]:
490)             check_incompatible_options(
491)                 opt, *options_in_group[ConfigurationOption],
492)                 *options_in_group[StorageManagementOption])
493)     sv_options = (options_in_group[PasswordGenerationOption] +
494)                   [params_by_str['--notes'], params_by_str['--delete']])
495)     sv_options.remove(params_by_str['--key'])
496)     sv_options.remove(params_by_str['--phrase'])
497)     for param in sv_options:
498)         if is_param_set(param) and not service:
499)             opt_str = param.opts[0]
500)             raise click.UsageError(f'{opt_str} requires a SERVICE')
501)     for param in [params_by_str['--key'], params_by_str['--phrase']]:
502)         if (
503)             is_param_set(param)
504)             and not (service or is_param_set(params_by_str['--config']))
505)         ):
506)             opt_str = param.opts[0]
507)             raise click.UsageError(f'{opt_str} requires a SERVICE or --config')
508)     no_sv_options = [params_by_str['--delete-globals'],
509)                      params_by_str['--clear'],
510)                      *options_in_group[StorageManagementOption]]
511)     for param in no_sv_options:
512)         if is_param_set(param) and service:
513)             opt_str = param.opts[0]
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

514)             raise click.UsageError(
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

515)                 f'{opt_str} does not take a SERVICE argument')
Marco Ricci Add prototype command-line...

Marco Ricci authored 4 months ago

516)     #if kwargs['length'] is None:
517)     #    kwargs['length'] = dpp.Vault.__init__.__kwdefaults__['length']
518)     #if kwargs['repeat'] is None:
519)     #    kwargs['repeat'] = dpp.Vault.__init__.__kwdefaults__['repeat']
Marco Ricci Fortify the argument parsin...

Marco Ricci authored 4 months ago

520)     click.echo(repr(ctx.params))