Coverage for src/lib2fas/_types.py: 100%
50 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-04-10 14:44 +0200
« prev ^ index » next coverage.py v7.4.1, created at 2024-04-10 14:44 +0200
1"""
2This file holds reusable types.
3"""
5import typing
6from typing import Optional
8from configuraptor import TypedConfig, asdict, asjson
9from pyotp import TOTP
11AnyDict = dict[str, typing.Any]
14class OtpDetails(TypedConfig):
15 """
16 Fields under the 'otp' key of the 2fas file.
17 """
19 link: Optional[str] = None
20 tokenType: Optional[str] = None
21 source: Optional[str] = None
22 label: Optional[str] = None
23 account: Optional[str] = None
24 digits: Optional[int] = None
25 period: Optional[int] = None
28class OrderDetails(TypedConfig):
29 """
30 Fields under the 'order' key of the 2fas file.
31 """
33 position: int = 0
36class IconCollectionDetails(TypedConfig):
37 """
38 Fields under the 'icon.iconCollection' key of the 2fas file.
39 """
41 id: str
44class IconDetails(TypedConfig):
45 """
46 Fields under the 'icon' key of the 2fas file.
47 """
49 selected: str
50 iconCollection: IconCollectionDetails
53class TwoFactorAuthDetails(TypedConfig):
54 """
55 Fields of a service in a 2fas file.
56 """
58 name: str
59 secret: str
60 updatedAt: int
61 serviceTypeID: Optional[str]
62 otp: Optional[OtpDetails] = None
63 order: Optional[OrderDetails] = None
64 icon: Optional[IconDetails] = None
65 groupId: Optional[str] = None # todo: groups are currently not supported!
67 _topt: Optional[TOTP] = None # lazily loaded when calling .totp or .generate()
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
78 def generate(self) -> str:
79 """
80 Generate the current TOTP code.
81 """
82 return self.totp.now()
84 def generate_int(self) -> int:
85 """
86 Generate the current TOTP code, as a number instead of string.
88 !!! usually not prefered, because this drops leading zeroes!!
89 """
90 return int(self.totp.now())
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)
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)
104 def __str__(self) -> str:
105 """
106 Magic method for str() - simple representation.
107 """
108 return f"<2fas '{self.name}'>"
110 def __repr__(self) -> str:
111 """
112 Magic method for repr() - representation in JSON.
113 """
114 return self.as_json()
117T_TypedConfig = typing.TypeVar("T_TypedConfig", bound=TypedConfig)
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]