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
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-22 17:34 +0100
1import typing
2from typing import Optional
4from configuraptor import TypedConfig, asdict, asjson
5from pyotp import TOTP
7AnyDict = dict[str, typing.Any]
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
20class OrderDetails(TypedConfig):
21 position: int
24class IconCollectionDetails(TypedConfig):
25 id: str
28class IconDetails(TypedConfig):
29 selected: str
30 iconCollection: IconCollectionDetails
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!
43 _topt: Optional[TOTP] = None # lazily loaded when calling .totp or .generate()
45 @property
46 def totp(self) -> TOTP:
47 if not self._topt:
48 self._topt = TOTP(self.secret)
49 return self._topt
51 def generate(self) -> str:
52 return self.totp.now()
54 def generate_int(self) -> int:
55 # !!! usually not prefered, because this drops leading zeroes!!
56 return int(self.totp.now())
58 def as_dict(self) -> AnyDict:
59 return asdict(self, with_top_level_key=False, exclude_internals=2)
61 def as_json(self) -> str:
62 return asjson(self, with_top_level_key=False, indent=2, exclude_internals=2)
64 def __str__(self) -> str:
65 return f"<2fas '{self.name}'>"
67 def __repr__(self) -> str:
68 return self.as_json()
71T_TypedConfig = typing.TypeVar("T_TypedConfig", bound=TypedConfig)
74def into_class(entries: list[AnyDict], klass: typing.Type[T_TypedConfig]) -> list[T_TypedConfig]:
75 return [klass.load(d) for d in entries]