Coverage for src/derivepassphrase/types.py: 95.455%

1# SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info>

2#

3# SPDX-License-Identifier: MIT

4

5"""Common typing declarations for the parent module.

6

7"""

8

9 from __future__ import annotations

10

11 from typing_extensions import (

12 Any, NotRequired, Required, TypedDict, TypeGuard,

13)

14

15 import derivepassphrase

16

17 __author__ = derivepassphrase.__author__

18 __version__ = derivepassphrase.__version__

19

20 class VaultConfigGlobalSettings(TypedDict, total=False):

21 r"""Configuration for vault: global settings.

22

23 Attributes:

24 key:

25 The base64-encoded ssh public key to use, overriding the

26 master passphrase. Optional.

27 phrase:

28 The master passphrase. Optional.

29

30 """

31 key: NotRequired[str]

32 phrase: NotRequired[str]

33

34

35 class VaultConfigServicesSettings(VaultConfigGlobalSettings, total=False):

36 r"""Configuration for vault: services settings.

37

38 Attributes:

39 notes:

40 Optional notes for this service, to display to the user when

41 generating the passphrase.

42 length:

43 Desired passphrase length.

44 repeat:

45 The maximum number of immediate character repetitions

46 allowed in the passphrase. Disabled if set to 0.

47 lower:

48 Optional constraint on ASCII lowercase characters. If

49 positive, include this many lowercase characters

50 somewhere in the passphrase. If 0, avoid lowercase

51 characters altogether.

52 upper:

53 Same as `lower`, but for ASCII uppercase characters.

54 number:

55 Same as `lower`, but for ASCII digits.

56 space:

57 Same as `lower`, but for the space character.

58 dash:

59 Same as `lower`, but for the hyphen-minus and underscore

60 characters.

61 symbol:

62 Same as `lower`, but for all other hitherto unlisted

63 ASCII printable characters (except backquote).

64

65 """

66 notes: NotRequired[str]

67 length: NotRequired[int]

68 repeat: NotRequired[int]

69 lower: NotRequired[int]

70 upper: NotRequired[int]

71 number: NotRequired[int]

72 space: NotRequired[int]

73 dash: NotRequired[int]

74 symbol: NotRequired[int]

75

76

77 _VaultConfig = TypedDict('_VaultConfig',

78 {'global': NotRequired[VaultConfigGlobalSettings]},

79 total=False)

80 class VaultConfig(TypedDict, _VaultConfig, total=False):

81 r"""Configuration for vault.

82

83 Usually stored as JSON.

84

85 Attributes:

86 global (NotRequired[VaultConfigGlobalSettings]):

87 Global settings.

88 services (Required[dict[str, VaultConfigServicesSettings]]):

89 Service-specific settings.

90

91 """

92 services: Required[dict[str, VaultConfigServicesSettings]]

93

94 def is_vault_config(obj: Any) -> TypeGuard[VaultConfig]:

95 """Check if `obj` is a valid vault config, according to typing.

96

97 Args:

98 obj: The object to test.

99

100 Returns:

101 True if this is a vault config, false otherwise.

102

103 """

104 if not isinstance(obj, dict):

105 return False

106 if 'global' in obj:

107 o_global = obj['global']

108 if not isinstance(o_global, dict):

109 return False

110 for key in ('key', 'phrase'):

111 if key in o_global and not isinstance(o_global[key], str):

112 return False

113 if 'key' in o_global and 'phrase' in o_global:

114 return False

115 if not isinstance(obj.get('services'), dict):

116 return False

117 for sv_name, service in obj['services'].items():

118 if not isinstance(sv_name, str):

119 return False

120 if not isinstance(service, dict):

121 return False

122 for key, value in service.items():

123 match key:

124 case 'notes' | 'phrase' | 'key': 124 ↛ 127line 124 didn't jump to line 127 because the pattern on line 124 always matched

125 if not isinstance(value, str): 125 ↛ 122line 125 didn't jump to line 122 because the condition on line 125 was always true

126 return False

127 case 'length':

128 if not isinstance(value, int) or value < 1: 128 ↛ 122line 128 didn't jump to line 122 because the condition on line 128 was always true

129 return False

130 case _:

131 if not isinstance(value, int) or value < 0: 131 ↛ 122line 131 didn't jump to line 122 because the condition on line 131 was always true

132 return False

133 if 'key' in service and 'phrase' in service:

134 return False

135 return True