This repository has been archived by the owner on Jul 6, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.py
345 lines (251 loc) · 9.62 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
from collections import UserDict
from datetime import datetime, timedelta
from typing import Callable, Dict
class Field:
def __init__(self, value: str):
self.value = value
def __str__(self) -> str:
return str(self.value)
class Name(Field):
...
class PhoneError(Exception):
def __init__(self) -> None:
super().__init__('Give me a valid phone number, please.')
class Phone(Field):
def __init__(self, value: str):
if len(value) != 10 or not value.isdigit():
raise PhoneError
super().__init__(value)
class BirthdayError(Exception):
def __init__(self) -> None:
super().__init__('Invalid date format. Use DD.MM.YYYY')
class Birthday(Field):
FORMAT: str = '%d.%m.%Y'
def __init__(self, value: str):
try:
self.value = datetime.strptime(value, self.FORMAT)
except ValueError:
raise BirthdayError
def __str__(self) -> str:
return self.value.strftime(self.FORMAT) if self.value else 'Unknown'
class RecordError(Exception):
MESSAGE = 'Give me an existing name, please.'
def __init__(self) -> None:
super().__init__(self.MESSAGE)
class Record:
def __init__(self, name: str) -> None:
self.name = Name(name)
self.phones: list[Phone] = []
self.birthday: Birthday | None = None
def __str__(self) -> str:
phones = '; '.join(map(lambda phone: phone.value, self.phones))
return f'Contact name: {self.name.value}, phones: {phones}'
def __find(self,
phone: str,
get_instance: bool = False
) -> int | Phone | None:
for id, instance in enumerate(self.phones):
if instance.value == phone:
return instance if get_instance else id
return None
def add_phone(self, phone: str) -> None:
self.phones.append(Phone(phone))
def remove_phone(self, phone: str) -> None:
if (id := self.__find(phone)) is not None:
del self.phones[id]
def edit_phone(self, current: str, new: str) -> None:
if (id := self.__find(current)) is None:
raise ValueError(f'Phone {current} not found!')
else:
self.phones[id] = Phone(new)
def find_phone(self, phone: str) -> Phone | None:
return self.__find(phone, True)
def add_birthday(self, birthday: str) -> None:
self.birthday = Birthday(birthday)
class AddressBook(UserDict):
data: Dict[str, Record]
def add_record(self, record: Record) -> None:
self.data[record.name.value] = record
def find(self, name: str) -> Record:
record = self.data.get(name)
if record is None:
raise RecordError
return record
def delete(self, name: str) -> None:
for id, record in self.data.items():
if record.name.value == name:
del self.data[id]
break
@staticmethod
def date_to_string(date: datetime) -> str:
return date.strftime(Birthday.FORMAT)
@staticmethod
def find_next_weekday(start_date: datetime, weekday: int) -> datetime:
days_ahead = weekday - start_date.weekday()
if days_ahead <= 0:
days_ahead += 7
return start_date + timedelta(days=days_ahead)
@classmethod
def adjust_for_weekend(cls, birthday: datetime) -> datetime:
if birthday.weekday() >= 5:
return cls.find_next_weekday(birthday, 0)
return birthday
def get_upcoming_birthdays(self, days: int = 7) -> Dict[str, list[str]]:
dates: Dict[str, list[str]] = {}
today = datetime.today()
for name, record in self.data.items():
if record.birthday:
# Birthday this year.
real = record.birthday.value.replace(year=today.year)
if real < today:
real = real.replace(year=today.year + 1)
if 0 <= (real - today).days <= days:
# Congratulation date.
event = self.date_to_string(self.adjust_for_weekend(real))
if event not in dates:
dates[event] = []
dates[event].append(name)
return dates
def input_error(func: Callable) -> Callable:
def inner(*args: list, **kwargs: dict):
try:
return func(*args, **kwargs)
except Exception:
return 'Something went wrong.'
def add_contact_error(args: list[str], book: AddressBook) -> str:
try:
return func(args, book)
except PhoneError as e:
return e
except ValueError:
return 'Give me a new name and a new phone, please.'
def change_contact_error(args: list[str], book: AddressBook) -> str:
try:
return func(args, book)
except (PhoneError, RecordError) as e:
return e
except ValueError:
return 'Give me an existing name and a new phone, please.'
def show_phone_error(args: list[str], book: AddressBook) -> str:
try:
return func(args, book)
except RecordError as e:
return e
except IndexError:
return RecordError.MESSAGE
def show_all_error(book: AddressBook) -> str:
try:
return func(book)
except ValueError:
return 'Contacts list is empty.'
def add_birthday_error(args: list[str], book: AddressBook) -> str:
try:
return func(args, book)
except (BirthdayError, RecordError) as e:
return e
except ValueError:
return 'Give me an existing name and birthday date, please.'
def show_birthday_error(args: list[str], book: AddressBook) -> str:
try:
return func(args, book)
except (IndexError, RecordError):
return RecordError.MESSAGE
def birthdays_error(book: AddressBook) -> str:
try:
return func(book)
except ValueError:
return 'Birthdays list is empty.'
HANDLERS: Dict[str, Callable] = {
'add_contact': add_contact_error,
'change_contact': change_contact_error,
'show_phone': show_phone_error,
'show_all': show_all_error,
'add_birthday': add_birthday_error,
'show_birthday': show_birthday_error,
'birthdays': birthdays_error
}
return HANDLERS.get(func.__name__, inner)
@input_error
def parse_input(user_input: str) -> tuple[str]:
cmd, *args = user_input.split()
return cmd.strip().lower(), *args
@input_error
def add_contact(args: list[str], book: AddressBook) -> str:
name, phone, *_ = args
try:
book.find(name)
except RecordError:
record = Record(name)
record.add_phone(phone)
book.add_record(record)
return 'Contact added.'
else:
return 'Contact is already added.'
@input_error
def change_contact(args: list[str], book: AddressBook) -> str:
name, phone, *_ = args
record = book.find(name)
record.edit_phone(record.phones[0].value, phone)
return 'Contact updated.'
@input_error
def show_phone(args: list[str], book: AddressBook) -> str:
return str(book.find(args[0]))
def table(left_cell: str, right_cell: str, data: Dict[str, str]) -> str:
def divider(left: str, right: str, middle: str, cell: str = '═'):
return left + cell * (longest_left + 2) + middle + cell * \
(longest_right + 2) + right
def row() -> str:
return '║ {} │ {} ║'.format(
left_cell.ljust(longest_left),
right_cell.rjust(longest_right))
longest_left = max([len(left_cell) for left_cell in data.keys()])
longest_right = max([len(right_cell) for right_cell in data.values()])
if longest_left < len(left_cell):
longest_left = len(left_cell)
if longest_right < len(right_cell):
longest_right = len(right_cell)
rows = [divider('╔', '╗', '╤'), row(), divider('╟', '╢', '┼', '─')]
for left_cell, right_cell in data.items():
rows.append(row())
rows.append(divider('╚', '╝', '╧'))
return '\n'.join(rows)
@input_error
def show_all(book: AddressBook) -> str:
return table(
'Full name',
'Phone number',
{name: record.phones[0].value for name, record in book.data.items()})
@input_error
def add_birthday(args: list[str], book: AddressBook) -> str:
name, birthday, *_ = args
book.find(name).add_birthday(birthday)
return 'Birthday added.'
@input_error
def show_birthday(args: list[str], book: AddressBook) -> str:
return str(book.find(args[0]).birthday)
@input_error
def birthdays(book: AddressBook) -> str:
events = book.get_upcoming_birthdays()
return table('Date',
'Users',
{date: ', '.join(names) for date, names in events.items()})
def main() -> None:
book = AddressBook()
print('Welcome to the assistant bot!')
while True:
command, *args = parse_input(input('Enter a command: '))
match command:
case 'hello': print('How can I help you?')
case 'add': print(add_contact(args, book))
case 'change': print(change_contact(args, book))
case 'phone': print(show_phone(args, book))
case 'all': print(show_all(book))
case 'add-birthday': print(add_birthday(args, book))
case 'show-birthday': print(show_birthday(args, book))
case 'birthdays': print(birthdays(book))
case _ if command in ['close', 'exit']:
print('Good bye!')
break
case _: print('Invalid command.')
if __name__ == '__main__':
main()