Coverage for src/lib2fas/_types.py: 100%

50 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-01-29 11:26 +0100

1""" 

2This file holds reusable types. 

3""" 

4 

5import typing 

6from typing import Optional 

7 

8from configuraptor import TypedConfig, asdict, asjson 

9from pyotp import TOTP 

10 

11AnyDict = dict[str, typing.Any] 

12 

13 

14class OtpDetails(TypedConfig): 

15 """ 

16 Fields under the 'otp' key of the 2fas file. 

17 """ 

18 

19 link: str 

20 tokenType: str 

21 source: str 

22 label: Optional[str] = None 

23 account: Optional[str] = None 

24 digits: Optional[int] = None 

25 period: Optional[int] = None 

26 

27 

28class OrderDetails(TypedConfig): 

29 """ 

30 Fields under the 'order' key of the 2fas file. 

31 """ 

32 

33 position: int 

34 

35 

36class IconCollectionDetails(TypedConfig): 

37 """ 

38 Fields under the 'icon.iconCollection' key of the 2fas file. 

39 """ 

40 

41 id: str 

42 

43 

44class IconDetails(TypedConfig): 

45 """ 

46 Fields under the 'icon' key of the 2fas file. 

47 """ 

48 

49 selected: str 

50 iconCollection: IconCollectionDetails 

51 

52 

53class TwoFactorAuthDetails(TypedConfig): 

54 """ 

55 Fields of a service in a 2fas file. 

56 """ 

57 

58 name: str 

59 secret: str 

60 updatedAt: int 

61 serviceTypeID: Optional[str] 

62 otp: OtpDetails 

63 order: OrderDetails 

64 icon: IconDetails 

65 groupId: Optional[str] = None # todo: groups are currently not supported! 

66 

67 _topt: Optional[TOTP] = None # lazily loaded when calling .totp or .generate() 

68 

69 @property 

70 def totp(self) -> TOTP: 

71 """ 

72 Get a TOTP instance for this service. 

73 """ 

74 if not self._topt: 

75 self._topt = TOTP(self.secret) 

76 return self._topt 

77 

78 def generate(self) -> str: 

79 """ 

80 Generate the current TOTP code. 

81 """ 

82 return self.totp.now() 

83 

84 def generate_int(self) -> int: 

85 """ 

86 Generate the current TOTP code, as a number instead of string. 

87 

88 !!! usually not prefered, because this drops leading zeroes!! 

89 """ 

90 return int(self.totp.now()) 

91 

92 def as_dict(self) -> AnyDict: 

93 """ 

94 Dump this object as a dictionary. 

95 """ 

96 return asdict(self, with_top_level_key=False, exclude_internals=2) 

97 

98 def as_json(self) -> str: 

99 """ 

100 Dump this object as a JSON string. 

101 """ 

102 return asjson(self, with_top_level_key=False, indent=2, exclude_internals=2) 

103 

104 def __str__(self) -> str: 

105 """ 

106 Magic method for str() - simple representation. 

107 """ 

108 return f"<2fas '{self.name}'>" 

109 

110 def __repr__(self) -> str: 

111 """ 

112 Magic method for repr() - representation in JSON. 

113 """ 

114 return self.as_json() 

115 

116 

117T_TypedConfig = typing.TypeVar("T_TypedConfig", bound=TypedConfig) 

118 

119 

120def into_class(entries: list[AnyDict], klass: typing.Type[T_TypedConfig]) -> list[T_TypedConfig]: 

121 """ 

122 Helper to load a list of dicts into a list of Typed Config instances. 

123 """ 

124 return [klass.load(d) for d in entries]