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

50 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-22 17:34 +0100

1import typing 

2from typing import Optional 

3 

4from configuraptor import TypedConfig, asdict, asjson 

5from pyotp import TOTP 

6 

7AnyDict = dict[str, typing.Any] 

8 

9 

10class OtpDetails(TypedConfig): 

11 link: str 

12 tokenType: str 

13 source: str 

14 label: Optional[str] = None 

15 account: Optional[str] = None 

16 digits: Optional[int] = None 

17 period: Optional[int] = None 

18 

19 

20class OrderDetails(TypedConfig): 

21 position: int 

22 

23 

24class IconCollectionDetails(TypedConfig): 

25 id: str 

26 

27 

28class IconDetails(TypedConfig): 

29 selected: str 

30 iconCollection: IconCollectionDetails 

31 

32 

33class TwoFactorAuthDetails(TypedConfig): 

34 name: str 

35 secret: str 

36 updatedAt: int 

37 serviceTypeID: Optional[str] 

38 otp: OtpDetails 

39 order: OrderDetails 

40 icon: IconDetails 

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

42 

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

44 

45 @property 

46 def totp(self) -> TOTP: 

47 if not self._topt: 

48 self._topt = TOTP(self.secret) 

49 return self._topt 

50 

51 def generate(self) -> str: 

52 return self.totp.now() 

53 

54 def generate_int(self) -> int: 

55 # !!! usually not prefered, because this drops leading zeroes!! 

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

57 

58 def as_dict(self) -> AnyDict: 

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

60 

61 def as_json(self) -> str: 

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

63 

64 def __str__(self) -> str: 

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

66 

67 def __repr__(self) -> str: 

68 return self.as_json() 

69 

70 

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

72 

73 

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

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