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

54 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2024-07-14 11:39 +0200

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 

9from __future__ import annotations 

10 

11from typing_extensions import ( 

12 Any, NotRequired, Required, TypedDict, TypeGuard, 

13) 

14 

15import derivepassphrase 

16 

17__author__ = derivepassphrase.__author__ 

18__version__ = derivepassphrase.__version__ 

19 

20class 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 

35class 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) 

80class 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 

94def 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