Coverage for src/lib/address_book.py: 82%
198 statements
« prev ^ index » next coverage.py v7.2.7, created at 2025-03-09 17:37 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2025-03-09 17:37 +0000
2import datetime as dt
3from os import path, remove
4from logging import getLogger, Logger
5from sty import fg
6from lib.client import Client
8from lib.overlay import Node, Distance
9from lib.helper import read_json_file, write_json_file
10from lib.contact import Contact
13class AddressBook():
14 _path: str
15 _config: dict
16 _ab_config: dict
17 _clients_by_uuid: dict[str, Client]
18 _clients_by_id: dict[str, Client]
19 _changes: bool
20 _clients_ttl: dt.timedelta
21 _logger: Logger
23 def __init__(self, path: str, config: dict = None):
24 self._path = path
25 self._config = config
26 self._ab_config = self._config['address_book']
27 self._clients_by_uuid = dict()
28 self._clients_by_id = dict()
29 self._changes = False
31 self._logger = getLogger('app.address_book')
32 self._logger.info('init()')
34 if self._ab_config is None:
35 self._clients_ttl = dt.timedelta(hours=1)
36 else:
37 self._clients_ttl = dt.timedelta(hours=self._ab_config['client_retention_time'])
39 self._logger.info('clients_ttl %s', self._clients_ttl)
41 def load(self):
42 self._logger.debug('load()')
44 _data = read_json_file(self._path, {})
45 for client_uuid, row in _data.items():
46 client = Client()
47 client.uuid = client_uuid
48 client.from_dict(row)
50 self._logger.debug('load client: %s', client)
52 self._clients_by_uuid[client_uuid] = client
53 if client.id is not None:
54 if client.id in self._clients_by_id:
55 self._logger.warning('Client ID already exists: %s', client.id)
57 self._clients_by_id[client.id] = client
59 key_file_path = path.join(self._config['keys_dir'], client.id + '.pem')
60 if path.isfile(key_file_path):
61 client.load_public_key_from_pem_file(key_file_path)
63 def save(self) -> bool:
64 self._logger.debug('save() changes=%s', self._changes)
66 if not self._changes:
67 return False
69 _data = dict()
70 for client_uuid, client in self._clients_by_uuid.items():
71 self._logger.debug('save client: %s', client)
73 _data[client_uuid] = client.as_dict()
75 if client.id is not None:
76 key_file_path = path.join(self._config['keys_dir'], client.id + '.pem')
77 if not path.isfile(key_file_path):
78 client.write_public_key_to_pem_file(key_file_path)
80 write_json_file(self._path, _data)
81 self._changes = False
83 return True
85 def get_clients(self) -> dict[str, Client]:
86 return self._clients_by_uuid
88 def get_clients_len(self) -> int:
89 return len(self._clients_by_uuid)
91 def get_bootstrap_clients(self) -> list:
92 def ffunc(_client):
93 return _client[1].is_bootstrap
94 bootstrap_clients = list(filter(ffunc, self._clients_by_uuid.items()))
95 return bootstrap_clients
97 def get_bootstrap_clients_len(self) -> int:
98 def ffunc(_client):
99 return _client[1].is_bootstrap
100 bootstrap_clients = list(filter(ffunc, self._clients_by_uuid.items()))
101 return len(bootstrap_clients)
103 def get_client_by_id(self, id: str) -> Client:
104 self._logger.debug('get_client_by_id(%s)', id)
106 if id in self._clients_by_id:
107 return self._clients_by_id[id]
109 return None
111 def get_client_by_uuid(self, uuid: str) -> Client:
112 self._logger.debug('get_client_by_uuid(%s)', uuid)
114 if uuid in self._clients_by_uuid:
115 return self._clients_by_uuid[uuid]
117 return None
119 def get_client_by_addr_port(self, addr: str, port: int):
120 def ffunc(_client):
121 return _client[1].address == addr and _client[1].port == port
122 _clients = list(filter(ffunc, self._clients_by_uuid.items()))
124 if len(_clients) > 0:
125 return _clients[0][1]
127 return None
129 def add_client(self, id: str = None, addr: str = None, port: int = None) -> Client:
130 self._logger.debug('add_client(%s, %s, %s)', id, addr, port)
132 client = Client()
134 if id is not None:
135 client.set_id(id)
137 if addr is not None:
138 client.address = addr
140 if port is not None:
141 client.port = port
143 self._clients_by_uuid[client.uuid] = client
144 if client.id is not None:
145 self._clients_by_id[client.id] = client
147 self.changed()
149 return client
151 def append_client(self, client: Client):
152 self._logger.debug('append_client(%s)', client)
154 self._clients_by_uuid[client.uuid] = client
155 if client.id is not None:
156 self._clients_by_id[client.id] = client
158 self.changed()
160 def remove_client(self, client: Client, force: bool = False) -> bool:
161 self._logger.debug('remove_client(%s, %s)', client, force)
163 if not force and client.is_trusted:
164 return False
166 key_file_path = path.join(self._config['keys_dir'], client.id + '.pem')
167 if path.isfile(key_file_path):
168 remove(key_file_path)
170 del self._clients_by_uuid[client.uuid]
171 if client.id is not None:
172 del self._clients_by_id[client.id]
174 self.changed()
176 return True
178 def add_bootstrap(self, file_path: str):
179 self._logger.debug('add_bootstrap(%s)', file_path)
180 _data = read_json_file(file_path, [])
181 for row in _data:
182 contact = Contact.parse(row)
184 _client = self.get_client_by_addr_port(contact.addr, contact.port)
185 if _client is not None:
186 self._logger.debug('bootstrap client already exists: %s', _client)
187 continue
189 client = Client()
190 client.address = contact.addr
191 client.port = contact.port
192 client.is_bootstrap = True
193 client.debug_add = 'bootstrap'
195 self._clients_by_uuid[client.uuid] = client
196 if client.id is not None:
197 self._clients_by_id[client.id] = client
199 self.changed()
201 write_json_file(file_path, [])
203 def changed(self):
204 self._changes = True
206 def hard_clean_up(self, local_id: str = None):
207 self._logger.debug('hard_clean_up(%s)', local_id)
209 # remove local_id
210 if local_id is not None and local_id in self._clients_by_id:
211 del self._clients_by_id[local_id]
213 _clients = list(self._clients_by_uuid.values())
214 _clients_len = len(_clients)
216 if _clients_len <= self._ab_config['max_clients']:
217 return
219 # remove bootstrap clients with no meetings
220 _clients = list(filter(lambda _client: _client.is_bootstrap and _client.meetings == 0, _clients))
221 for client in _clients:
222 if self.remove_client(client):
223 _clients_len -= 1
224 if _clients_len <= self._ab_config['max_clients']:
225 return
227 # remove out-of-date clients (invalid client_retention_time)
228 _clients = list(self._clients_by_uuid.values())
229 _clients = list(filter(lambda _client: dt.datetime.now(dt.UTC) - _client.used_at > self._clients_ttl, _clients))
230 _clients.sort(key=lambda _client: _client.used_at)
231 for client in _clients:
232 if self.remove_client(client):
233 _clients_len -= 1
234 if _clients_len <= self._ab_config['max_clients']:
235 return
237 # remove clients, sorted by meetings
238 _clients = list(self._clients_by_uuid.values())
239 _clients.sort(key=lambda _client: _client.meetings)
241 for client in _clients:
242 if self.remove_client(client):
243 _clients_len -= 1
244 if _clients_len <= self._ab_config['max_clients']:
245 return
247 def soft_clean_up(self, local_id: str = None):
248 self._logger.debug('soft_clean_up(%s)', local_id)
250 # remove local_id
251 if local_id is not None and local_id in self._clients_by_id:
252 del self._clients_by_id[local_id]
254 _clients = list(self._clients_by_uuid.values())
255 _clients_len = len(_clients)
257 # remove out-of-date clients (invalid client_retention_time)
258 _clients = list(self._clients_by_uuid.values())
259 _clients = list(filter(lambda _client: dt.datetime.now(dt.UTC) - _client.used_at > self._clients_ttl and _client.meetings == 0, _clients))
260 _clients.sort(key=lambda _client: _client.used_at)
261 for client in _clients:
262 if self.remove_client(client):
263 _clients_len -= 1
264 if _clients_len <= self._ab_config['max_clients']:
265 return
267 def get_nearest_to(self, node: Node, limit: int = 20, with_contact_infos: bool = None) -> list:
268 def sort_key(_client: Client) -> Distance:
269 print(f'-> sort_key client: {_client}')
270 print(f'-> sort_key node: {_client.node}')
271 return _client.node.distance(node)
273 _clients = list(self._clients_by_uuid.values())
274 _clients.sort(key=sort_key)
276 if with_contact_infos:
277 _clients = list(filter(lambda _client: with_contact_infos == _client.has_contact(), _clients))
279 return _clients[:limit]