Coverage for /home/runner/work/gpu-prices-bot/gpu-prices-bot/src/bot/commands.py: 79%
591 statements
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-31 20:32 +0000
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-31 20:32 +0000
1import ast
2import datetime
3import json
4import colorsys
5import pendulum
6import matplotlib.pyplot as plt
7import numpy
8import requests
9import telegram
10from telegram import Update, InlineKeyboardMarkup
11from telegram.ext import CallbackContext
12from loguru import logger
13from src.bot.constants import *
14from src.bot.series_vendors import series_dict, vendors_dict, shops_dict, series_buttons_dict
15def set_datetime(record):
16 record['extra']['datetime'] = pendulum.now('Europe/Moscow').strftime('%Y-%m-%dT%H:%M:%S')
18logger.configure(patcher=set_datetime)
19logger.add(
20 sink='logs/bot_{time:%Y-%m-%d_%H-%M-%S}.log',
21 format="{extra[datetime]} | {level} | {message}",
22 enqueue=True,
23 rotation='00:00'
24)
27def start(update: Update, context: CallbackContext) -> int:
28 """Вывести сообщение и клавиатуру меню на команду '/start'"""
29 reset_context(context)
31 user = update.message.from_user.full_name.encode(encoding='utf-8').decode()
32 context.user_data[CURRENT_USER_NAME] = user
33 logger.info(f'User {user} started the conversation.')
35 update.message.reply_text(
36 text=hello_text + f'{user}!'
37 )
38 update.message.reply_text(
39 text=greetings_text
40 )
41 reply_markup_keyboard = InlineKeyboardMarkup(keyboard_MENU)
43 # Отправка сообщения с текстом и добавлением InlineKeyboard
44 with open(shops_logo_dir, 'rb') as photo:
45 update.message.reply_photo(
46 photo=photo,
47 caption=using_buttons_text,
48 reply_markup=reply_markup_keyboard
49 )
51 # Переход в состояние MENU
52 return MENU
55def start_over(update: Update, context: CallbackContext) -> int:
56 """Выдает тот же текст и клавиатуру, что и '/start', но не как новое сообщение"""
57 user = context.user_data[CURRENT_USER_NAME]
58 logger.info(f'User {user} returned to the menu.')
60 reset_context(context)
62 context.user_data[CURRENT_USER_NAME] = user
64 update_query_message_with_keyboard(
65 update=update,
66 context=context,
67 keyboard=keyboard_MENU,
68 image_path=shops_logo_dir,
69 caption_text=using_buttons_text
70 )
72 return MENU
75def reset_context(context: CallbackContext):
76 """Сбросить текущий контекст (данные пользователя) после возврата в меню"""
77 context.user_data[CURRENT_SUBMENU] = ''
78 context.user_data[CURRENT_SHOP] = ''
79 context.user_data[CURRENT_ARCH] = ''
80 context.user_data[CURRENT_VENDOR] = ''
81 context.user_data[CURRENT_GPU] = ''
82 context.user_data[CURRENT_GRAPH_LEVEL] = 0
83 context.user_data[CURRENT_GRAPH_DAYS] = 30
84 context.user_data[CURRENT_GRAPH_STATE] = 8
85 context.user_data[CURRENT_GRAPH_START] = 0
86 context.user_data[CURRENT_TEMP_DATA] = ''
87 context.user_data[CURRENT_USER_NAME] = ''
90def update_query_message_with_keyboard(update: Update, context: CallbackContext, keyboard,
91 image_path, caption_text, current_const=CURRENT_TEMP_DATA):
92 """Обновить сообщение (update) с новой клавиатурой (keyboard), изображением,
93 находящимся по пути (image_path) и текстом (caption_text), сохранив контекст (context)
94 в список по ключу (current_const)."""
95 query = update.callback_query
96 context.user_data[current_const] = query.data
97 query.answer()
98 reply_markup_keyboard = InlineKeyboardMarkup(keyboard)
100 with open(image_path, 'rb') as photo:
101 image = telegram.InputMediaPhoto(photo)
103 query.edit_message_media(
104 media=image
105 )
107 query.edit_message_caption(
108 caption=caption_text,
109 reply_markup=reply_markup_keyboard
110 )
113def stats_popularity_func(update: Update, context: CallbackContext) -> int:
114 """Общая для статистики и популярности функция вывода кнопок и сообщения.
115 Для статистики: по магазину, по производителю, по видеокарте.
116 Для популярности: по магазину и по производителю."""
117 user = context.user_data[CURRENT_USER_NAME]
118 if update.callback_query.data == str(STATS):
119 logger.info(f'User {user} pressed the statistics button.')
121 update_query_message_with_keyboard(
122 update=update,
123 context=context,
124 keyboard=keyboard_STATS,
125 image_path=shops_logo_dir,
126 caption_text=select_stats_text
127 )
129 return STATS_SUBMENU
130 elif update.callback_query.data == str(POPULARITY):
131 logger.info(f'User {user} pressed the popularity button.')
133 update_query_message_with_keyboard(
134 update=update,
135 context=context,
136 keyboard=keyboard_POPULARITY,
137 image_path=shops_logo_dir,
138 caption_text=select_popularity_text
139 )
141 return POPULARITY_SUBMENU
142 else:
143 logger.warning(f'User {user} pressed incorrect button for menu!')
145 update_query_message_with_keyboard(
146 update=update,
147 context=context,
148 keyboard=keyboard_MENU,
149 image_path=shops_logo_dir,
150 caption_text=using_buttons_text
151 )
153 return MENU
156def for_shop_vendor_stats(update: Update, context: CallbackContext) -> int:
157 """Общая для категории статистики функция вывода кнопок и сообщения.
158 Для кнопки по магазину: DNS, MVIDEO, CITILINK.
159 Для кнопки по производителю: кнопки производителей (ASUS, MSI, Palit и другие).
160 Для кнопки по видеокарте: выделена отдельная функция (см. for_gpu)."""
161 user = context.user_data[CURRENT_USER_NAME]
162 if update.callback_query.data == str(FOR_SHOP):
163 logger.info(f'User {user} pressed the shops button for stats.')
165 update_query_message_with_keyboard(
166 update=update,
167 context=context,
168 keyboard=keyboard_SHOPS,
169 image_path=shops_logo_dir,
170 caption_text=select_shop_text,
171 current_const=CURRENT_SUBMENU
172 )
174 return SHOPS_SUBMENU
175 elif update.callback_query.data == str(FOR_VENDOR):
176 logger.info(f'User {user} pressed the vendors button for stats.')
178 update_query_message_with_keyboard(
179 update=update,
180 context=context,
181 keyboard=keyboard_VENDORS,
182 image_path=shops_logo_dir,
183 caption_text=select_vendor_text,
184 current_const=CURRENT_SUBMENU
185 )
187 return VENDORS_SUBMENU
188 else:
189 logger.warning(f'User {user} pressed incorrect button for stats!')
191 update_query_message_with_keyboard(
192 update=update,
193 context=context,
194 keyboard=keyboard_MENU,
195 image_path=shops_logo_dir,
196 caption_text=using_buttons_text
197 )
199 return MENU
202def for_shop_vendor_popularity(update: Update, context: CallbackContext) -> int:
203 """Общая для категории популярности функция вывода кнопок и сообщения.
204 Для кнопки по магазину: DNS, MVIDEO, CITILINK.
205 Для кнопки по производителю: кнопки производителей (ASUS, MSI, Palit и другие)."""
206 user = context.user_data[CURRENT_USER_NAME]
207 if update.callback_query.data == str(POPULARITY_FOR_SHOP):
208 logger.info(f'User {user} pressed the shops button for popularity.')
210 update_query_message_with_keyboard(
211 update=update,
212 context=context,
213 keyboard=keyboard_SHOPS,
214 image_path=shops_logo_dir,
215 caption_text=select_shop_text
216 )
218 return POPULARITY_SHOPS_SUBMENU
219 elif update.callback_query.data == str(POPULARITY_FOR_VENDOR):
220 logger.info(f'User {user} pressed the vendors button for popularity.')
222 update_query_message_with_keyboard(
223 update=update,
224 context=context,
225 keyboard=keyboard_VENDORS,
226 image_path=shops_logo_dir,
227 caption_text=select_vendor_text
228 )
230 return POPULARITY_VENDORS_SUBMENU
231 else:
232 logger.warning(f'User {user} pressed incorrect button for stats!')
234 update_query_message_with_keyboard(
235 update=update,
236 context=context,
237 keyboard=keyboard_MENU,
238 image_path=shops_logo_dir,
239 caption_text=using_buttons_text
240 )
242 return MENU
245def popularity_shops_graph(update: Update, context: CallbackContext) -> int:
246 """Показать график популярности видеокарт по магазину"""
247 query = update.callback_query
248 shop = ''
249 if query.data == str(DNS_SHOP):
250 shop = 'DNS'
251 elif query.data == str(MVIDEO_SHOP):
252 shop = 'MVIDEO'
253 elif query.data == str(CITILINK_SHOP):
254 shop = 'CITILINK'
256 url = f'http://173.18.0.3:8080/popularity/for-shop?shopName={shop}'
257 response = requests.get(url=url)
258 graph_data = json.loads(response.text)
260 card_names, places = define_card_names_places(graph_data)
262 draw_popularity_cards_places(card_names, places, shop)
264 user = context.user_data[CURRENT_USER_NAME]
265 logger.info(f'User {user} chose shop {shop} for popularity graph.')
267 update_query_message_with_keyboard(
268 update=update,
269 context=context,
270 keyboard=keyboard_POPULARITY_GRAPH,
271 image_path='graphic.png',
272 caption_text=popularity_shop_text + shop,
273 )
275 return POPULARITY_SHOPS_GRAPH_SUBMENU
278def draw_popularity_cards_places(card_names, places, shop):
279 """Построить график популярности видеокарт (card_names) по местам (places) в магазине (shop)."""
280 graph_days = get_days_list()
281 days_mode = 30
282 plt.set_loglevel('WARNING')
283 fig = plt.figure(figsize=(23.83, 11.68), dpi=100)
284 num_days = range(len(graph_days[-days_mode:]))
285 line_styles = ['solid', 'dashed']
286 count = 0
287 for card_name in card_names:
288 lines = plt.plot(
289 num_days,
290 places[card_name][-days_mode:],
291 label=card_name,
292 color=get_random_color()
293 )
294 lines[0].set_linestyle(line_styles[count % len(line_styles)])
295 plt.gca().invert_yaxis()
296 count += 1
297 plt.legend(bbox_to_anchor=(0.5, -0.11), loc='upper center', ncols=4)
298 plt.xlim('2022-11-20', str(datetime.date.today()))
299 plt.xticks(num_days, graph_days[-days_mode:], rotation=45, ha='right')
300 plt.yticks(numpy.arange(10, 0, -1))
301 plt.grid(axis='x', linestyle='--')
302 plt.title(f'Popularity for {shop} store', fontdict={'size': 16})
303 plt.xlabel(f'Period: {days_mode} days', fontdict={'size': 14})
304 plt.ylabel(f'Places', fontdict={'size': 14})
305 plt.savefig('graphic.png', bbox_inches='tight')
306 plt.clf()
307 plt.close(fig)
310def define_card_names_places(graph_data):
311 """Распределить полученные данные из БД (graph_data) по именам видеокарт (card_names)
312 и местам (places) для построения графика."""
313 places, card_names = {}, []
314 for offer in graph_data:
315 if len(list(offer.keys())) > 0:
316 items_length = len(list(offer.values()))
317 if items_length == 10:
318 for card in offer.values():
319 card_name = card['cardName']
320 if card_name not in card_names:
321 card_names.append(card_name)
322 places[card_name] = []
323 elif items_length < 10:
324 logger.warning('not enought items')
326 for offer in graph_data:
327 if len(list(offer.keys())) > 0:
328 items_length = len(list(offer.values()))
329 if items_length == 10:
330 for name in card_names:
331 is_not_card_today = True
332 for card in offer.values():
333 card_name = card['cardName']
334 card_popularity = card['cardPopularity']
335 if card_name == name:
336 is_not_card_today = False
337 places[card_name].append(card_popularity)
338 if is_not_card_today:
339 places[name].append(numpy.NaN)
340 elif items_length < 10:
341 logger.warning('not enought items')
343 return card_names, places
346def get_random_color():
347 """Получить рандомный цвет, распределенный по трём каналам RGB."""
348 return colorsys.hsv_to_rgb(
349 numpy.random.uniform(0.0, 1),
350 numpy.random.uniform(0.2, 1),
351 numpy.random.uniform(0.9, 1)
352 )
355def popularity_vendors_graph(update: Update, context: CallbackContext) -> int:
356 """Показать график популярности видеокарт по производителю"""
357 query = update.callback_query
359 vendor = vendors_dict.get(int(query.data)) if query.data != '' else ''
361 url = f'http://173.18.0.3:8080/popularity/for-vendor?vendorName={vendor}'
362 response = requests.get(url=url)
363 graph_data = json.loads(response.text)
365 message_caption, popularity_places_shops = define_popularity_places_shops(graph_data, vendor)
367 draw_popularity_vendors_graph(popularity_places_shops, vendor)
369 user = context.user_data[CURRENT_USER_NAME]
370 logger.info(f'User {user} chose vendor {vendor} for popularity graph.')
372 update_query_message_with_keyboard(
373 update=update,
374 context=context,
375 keyboard=keyboard_POPULARITY_GRAPH,
376 image_path='graphic.png',
377 caption_text=message_caption
378 )
380 return POPULARITY_VENDORS_GRAPH_SUBMENU
383def define_popularity_places_shops(graph_data, vendor):
384 """Распределить полученные данные из БД (graph_data) по местам в магазинах
385 (popularity_places_shops), дополнительно сформировав текст к графику (message_caption),
386 где используется название производителя (vendor)."""
387 message_caption = popularity_vendor_text + vendor + ':\n'
388 popularity_places_shops = {}
389 for shop in graph_data:
390 message_caption += shops_emojis_dict.get(shop) + f' {shop}\n'
391 popularity_places_shops[shop] = []
392 for place in ['1', '2', '3']:
393 if graph_data[shop].get(place) is not None:
394 card_name = graph_data[shop][place]['cardName']
395 message_caption += f'{place}. {card_name}\n'
396 popularity_places_shops[shop].append(card_name)
397 else:
398 message_caption += f'{place}. No Data\n'
399 popularity_places_shops[shop].append('No Data')
400 popularity_places_shops[shop].reverse()
401 message_caption += '\n'
402 return message_caption, popularity_places_shops
405def draw_popularity_vendors_graph(popularity_places_shops, vendor):
406 """Построить график популярности видеокарт производителя (vendor)
407 по местам (popularity_places_shops)."""
408 fig = plt.figure(figsize=(22, 6))
409 rects1 = plt.bar(
410 [tick + 1.0 for tick in [1.0, 2.0, 3.0]],
411 [int(i) for i in ['1', '2', '3']],
412 width=0.2, label='CITILINK'
413 )
414 rects2 = plt.bar(
415 [tick + 4.0 for tick in [1.0, 2.0, 3.0]],
416 [int(i) for i in ['1', '2', '3']],
417 width=0.2, label='DNS'
418 )
419 rects3 = plt.bar(
420 [tick + 7.0 for tick in [1.0, 2.0, 3.0]],
421 [int(i) for i in ['1', '2', '3']],
422 width=0.2, label='MVIDEO'
423 )
424 plt.ylim(0, 4)
425 plt.xticks(
426 numpy.arange(1, 12, 1),
427 ['', '', 'CITILINK', '', '', 'DNS', '', '', 'MVIDEO', '', '']
428 )
429 plt.yticks(numpy.arange(1, 4, 1))
430 plt.bar_label(rects1, popularity_places_shops['CITILINK'], padding=3)
431 plt.bar_label(rects2, popularity_places_shops['DNS'], padding=3)
432 plt.bar_label(rects3, popularity_places_shops['MVIDEO'], padding=3)
433 plt.grid(axis='y', linestyle='--')
434 plt.title(f'Popularity for {vendor}')
435 plt.xlabel('Shops')
436 plt.ylabel('Places')
437 plt.legend(bbox_to_anchor=(0.5, -0.11), loc='upper center', ncols=3)
438 plt.savefig('graphic.png', bbox_inches='tight')
439 plt.clf()
440 plt.close(fig)
443def for_gpu(update: Update, context: CallbackContext) -> int:
444 """Предложить написать название видеокарты для статистики"""
445 user = context.user_data[CURRENT_USER_NAME]
446 logger.info(f'User {user} pressed the gpu button for stats.')
448 context.user_data[CURRENT_GRAPH_GPU_LEVEL] = 0
449 context.user_data[CURRENT_GRAPH_GPU_DAYS] = 30
450 context.user_data[CURRENT_GRAPH_GPU_STATE] = 16
451 context.user_data[CURRENT_GRAPH_GPU_START] = 0
453 update_query_message_with_keyboard(
454 update=update,
455 context=context,
456 keyboard=keyboard_EMPTY,
457 image_path=shops_logo_dir,
458 caption_text=select_gpu_text
459 )
461 return GPU_SUBMENU
464def arch_func(update: Update, context: CallbackContext) -> int:
465 """Показать кнопки выбора архитектуры видеокарты: NVIDIA, AMD и 'Другие'."""
466 user = context.user_data[CURRENT_USER_NAME]
467 submenu = context.user_data[CURRENT_SUBMENU]
468 if submenu == str(FOR_SHOP):
469 update_query_message_with_keyboard(
470 update=update,
471 context=context,
472 keyboard=keyboard_ARCHITECTURES,
473 image_path=shops_logo_dir,
474 caption_text=select_arch_text,
475 current_const=CURRENT_SHOP
476 )
478 shop_index = context.user_data[CURRENT_SHOP]
479 shop = shops_dict.get(int(shop_index)) if shop_index != '' else ''
480 logger.info(f'User {user} chose shop {shop} for stats.')
482 return ARCHITECTURE_SUBMENU
483 elif submenu == str(FOR_VENDOR):
484 update_query_message_with_keyboard(
485 update=update,
486 context=context,
487 keyboard=keyboard_ARCHITECTURES,
488 image_path=shops_logo_dir,
489 caption_text=select_arch_text,
490 current_const=CURRENT_VENDOR
491 )
493 vendor_index = context.user_data[CURRENT_VENDOR]
494 vendor = vendors_dict.get(int(vendor_index)) if vendor_index != '' else ''
495 logger.info(f'User {user} chose vendor {vendor} for stats.')
497 return ARCHITECTURE_SUBMENU
498 else:
499 logger.warning(f'User {user} chose incorrect button for stats!')
501 update_query_message_with_keyboard(
502 update=update,
503 context=context,
504 keyboard=keyboard_MENU,
505 image_path=shops_logo_dir,
506 caption_text=using_buttons_text
507 )
509 return MENU
512def nvidia_amd_other_func(update: Update, context: CallbackContext) -> int:
513 """Общая функция для вывода кнопок выбора серии видеокарт NVIDIA, AMD, OTHER, INTEL и MATROX."""
514 user = context.user_data[CURRENT_USER_NAME]
515 if update.callback_query.data == str(NVIDIA):
516 logger.info(f'User {user} chose NVIDIA arch for stats.')
518 update_query_message_with_keyboard(
519 update=update,
520 context=context,
521 keyboard=keyboard_NVIDIA_SERIES,
522 image_path=shops_logo_dir,
523 caption_text=select_series_text,
524 current_const=CURRENT_ARCH
525 )
527 return NVIDIA_SERIES_SUBMENU
528 elif update.callback_query.data == str(AMD):
529 logger.info(f'User {user} chose AMD arch for stats.')
531 update_query_message_with_keyboard(
532 update=update,
533 context=context,
534 keyboard=keyboard_AMD_SERIES,
535 image_path=shops_logo_dir,
536 caption_text=select_series_text,
537 current_const=CURRENT_ARCH
538 )
540 return AMD_SERIES_SUBMENU
541 elif update.callback_query.data == str(OTHER_ARCH):
542 logger.info(f'User {user} chose OTHER arch category for stats.')
544 update_query_message_with_keyboard(
545 update=update,
546 context=context,
547 keyboard=keyboard_OTHER_ARCH,
548 image_path=shops_logo_dir,
549 caption_text=select_arch_text
550 )
552 return OTHER_ARCH_SUBMENU
553 elif update.callback_query.data == str(INTEL):
554 logger.info(f'User {user} chose INTEL arch for stats.')
556 update_query_message_with_keyboard(
557 update=update,
558 context=context,
559 keyboard=keyboard_INTEL_SERIES,
560 image_path=shops_logo_dir,
561 caption_text=select_series_text,
562 current_const=CURRENT_ARCH
563 )
565 return INTEL_SERIES_SUBMENU
566 elif update.callback_query.data == str(MATROX):
567 logger.info(f'User {user} chose MATROX arch for stats.')
569 update_query_message_with_keyboard(
570 update=update,
571 context=context,
572 keyboard=keyboard_MATROX_SERIES,
573 image_path=shops_logo_dir,
574 caption_text=select_series_text,
575 current_const=CURRENT_ARCH
576 )
578 return MATROX_SERIES_SUBMENU
579 else:
580 logger.warning(f'User {user} chose incorrect arch for stats!')
582 update_query_message_with_keyboard(
583 update=update,
584 context=context,
585 keyboard=keyboard_MENU,
586 image_path=shops_logo_dir,
587 caption_text=using_buttons_text
588 )
590 return MENU
593def nvidia_series_func(update: Update, context: CallbackContext) -> int:
594 """Показать кнопки выбора серии видеокарт NVIDIA"""
595 user = context.user_data[CURRENT_USER_NAME]
596 series_button = update.callback_query.data
597 logger.info(
598 f'User {user} chose '
599 f'{series_buttons_dict.get(int(series_button))["name"] if series_button != "" else "incorrect name"} button.'
600 )
601 keyboard = series_buttons_dict.get(int(series_button))['keyboard'] if series_button != '' else keyboard_ONLY_BACK
603 update_query_message_with_keyboard(
604 update=update,
605 context=context,
606 keyboard=keyboard,
607 image_path=shops_logo_dir,
608 caption_text=select_series_text,
609 )
611 return series_buttons_dict.get(int(series_button))['returning'] if series_button != '' else MENU
614def amd_series_func(update: Update, context: CallbackContext) -> int:
615 """Показать кнопки выбора серии видеокарт AMD"""
616 user = context.user_data[CURRENT_USER_NAME]
617 series_button = update.callback_query.data
618 logger.info(
619 f'User {user} chose '
620 f'{series_buttons_dict.get(int(series_button))["name"] if series_button != "" else "incorrect name"} button.'
621 )
622 keyboard = series_buttons_dict.get(int(series_button))['keyboard'] if series_button != '' else keyboard_ONLY_BACK
624 update_query_message_with_keyboard(
625 update=update,
626 context=context,
627 keyboard=keyboard,
628 image_path=shops_logo_dir,
629 caption_text=select_series_text,
630 )
632 return series_buttons_dict.get(int(series_button))['returning'] if series_button != '' else MENU
635def graph_for_gpu_func(update: Update, context: CallbackContext) -> int:
636 """Показать график цен по видеокарте"""
638 submenu_title, shop_title, vendor = 'for_gpu', '', 20
640 query = update.callback_query
642 graph_state = query.data
643 if graph_state == str(SHOW_30_DAYS_GPU):
644 context.user_data[CURRENT_GRAPH_GPU_DAYS] = 30
645 elif graph_state == str(SHOW_60_DAYS_GPU):
646 context.user_data[CURRENT_GRAPH_GPU_DAYS] = 60
647 elif graph_state == str(SHOW_90_DAYS_GPU):
648 context.user_data[CURRENT_GRAPH_GPU_DAYS] = 90
650 graph_days = context.user_data[CURRENT_GRAPH_GPU_DAYS]
652 card_name = context.user_data[CURRENT_GPU]
654 url = f'http://173.18.0.3:8080/price?cardName={card_name}'
655 response = requests.get(url=url)
656 graph_data = json.loads(response.text)
658 offers, prices = {}, []
659 days = get_days_list()
661 for offer in graph_data:
662 card_price = offer['cardPrice']
663 date = offer['date'].split('T')[0]
664 offers[date] = card_price
666 define_gpu_days_prices(days, offers, prices)
668 draw_gpu_graph(days, prices, card_name, days_mode=graph_days)
670 user = context.user_data[CURRENT_USER_NAME]
671 logger.info(f'User {user} chose gpu {card_name} for {graph_days} days stats graph.')
673 caption_message_text = f'submenu: {submenu_title}\n' \
674 f'gpu: {card_name}\n' \
675 f'days: {str(graph_days)}\n' + select_graph_text
677 update_query_message_with_keyboard(
678 update=update,
679 context=context,
680 keyboard=keyboard_GRAPH_PERIODS,
681 image_path='graphic.png',
682 caption_text=caption_message_text
683 )
685 return GRAPH_SUBMENU_ON_GPU
688def define_gpu_days_prices(days, offers, prices):
689 """Распределить записи о видеокартах (offers) по дням (days) и ценам (prices)
690 для построения графика."""
691 for day in days:
692 is_from_begin = True
693 choosing_day = day
694 while offers.get(choosing_day) is None:
695 if choosing_day == '2022-11-20':
696 choose_days_list = list(offers.keys())
697 if len(choose_days_list) > 0:
698 choosing_day = list(offers.keys())[0]
699 is_from_begin = False
700 break
701 date_year, date_month, date_day = [int(i) for i in choosing_day.split('-')]
702 prev_day = str(
703 datetime.date(
704 date_year, date_month, date_day
705 ) - datetime.timedelta(days=1)
706 )
707 choosing_day = prev_day
708 if is_from_begin:
709 prices.append(offers.get(choosing_day))
710 else:
711 prices.append(numpy.NaN)
714def draw_gpu_graph(graph_days, graph_prices, card_name, days_mode=30):
715 """Построить график статистики цен на видеокарту (graph_prices) по дням (graph_days) с названием
716 видеокарты (card_name) и режимом отображения (days_mode) по умолчанию 30 дней."""
717 plt.set_loglevel('WARNING')
718 fig = plt.figure(figsize=(23.83, 11.68), dpi=100)
719 num_days = range(len(graph_days[-days_mode:]))
720 plt.plot(num_days, graph_prices[-days_mode:], label=card_name)
721 plt.legend(bbox_to_anchor=(0.5, -0.11), loc='upper center', ncols=4)
722 plt.xlim('2022-11-20', str(datetime.date.today()))
723 plt.xticks(num_days, graph_days[-days_mode:], rotation=45, ha='right')
724 plt.grid(axis='x', linestyle='--')
725 plt.title(f'Statistics for {card_name}', fontdict={'size': 16})
726 plt.xlabel(f'Period: {days_mode} days', fontdict={'size': 14})
727 plt.ylabel(f'Price, RUB', fontdict={'size': 14})
728 plt.savefig('graphic.png', bbox_inches='tight')
729 plt.clf()
730 plt.close(fig)
733def graph_func(update: Update, context: CallbackContext) -> int:
734 """Показать график цен"""
735 user_data = context.user_data
736 submenu = user_data[CURRENT_SUBMENU]
738 submenu_title, shop, vendor = '', '', 20
739 if submenu == str(FOR_SHOP):
740 submenu_title = 'for_shop'
741 shop = user_data[CURRENT_SHOP]
743 if shop == str(DNS_SHOP):
744 shop = "DNS"
745 elif shop == str(MVIDEO_SHOP):
746 shop = "MVIDEO"
747 elif shop == str(CITILINK_SHOP):
748 shop = "CITILINK"
749 else:
750 print('unknown_shop')
751 elif submenu == str(FOR_VENDOR):
752 submenu_title = 'for_vendor'
753 vendor = user_data[CURRENT_VENDOR]
754 shop = ''
755 else:
756 print('unknown_submenu')
758 architecture = user_data[CURRENT_ARCH]
759 arch = ''
760 if architecture == str(NVIDIA):
761 arch = 'NVIDIA'
762 elif architecture == str(AMD):
763 arch = 'AMD'
764 elif architecture == str(INTEL):
765 arch = 'INTEL'
766 elif architecture == str(MATROX):
767 arch = 'MATROX'
768 else:
769 print('unknown architecture')
771 query = update.callback_query
773 if user_data[CURRENT_GRAPH_START] == 0:
774 user_data[CURRENT_GRAPH_START] = 1
775 graph_state = user_data[CURRENT_GRAPH_STATE]
776 else:
777 graph_state = query.data
778 user_data[CURRENT_GRAPH_STATE] = graph_state
780 series = ''
781 if graph_state == str(SHOW_30_DAYS):
782 series = user_data[CURRENT_SERIES]
783 context.user_data[CURRENT_GRAPH_DAYS] = 30
784 elif graph_state == str(SHOW_60_DAYS):
785 series = user_data[CURRENT_SERIES]
786 context.user_data[CURRENT_GRAPH_DAYS] = 60
787 elif graph_state == str(SHOW_90_DAYS):
788 series = user_data[CURRENT_SERIES]
789 context.user_data[CURRENT_GRAPH_DAYS] = 90
790 elif graph_state == str(GRAPH_MIN):
791 series = user_data[CURRENT_SERIES]
792 context.user_data[CURRENT_GRAPH_LEVEL] = 0
793 elif graph_state == str(GRAPH_AVERAGE):
794 series = user_data[CURRENT_SERIES]
795 context.user_data[CURRENT_GRAPH_LEVEL] = 1
796 elif graph_state == str(GRAPH_MAX):
797 series = user_data[CURRENT_SERIES]
798 context.user_data[CURRENT_GRAPH_LEVEL] = 2
799 elif graph_state == 8:
800 context.user_data[CURRENT_SERIES] = query.data
801 series = query.data
803 graph_days = context.user_data[CURRENT_GRAPH_DAYS]
804 graph_level_int = context.user_data[CURRENT_GRAPH_LEVEL] + 3
806 graph_level = ''
807 if graph_level_int == GRAPH_MIN:
808 graph_level = 'min'
809 elif graph_level_int == GRAPH_AVERAGE:
810 graph_level = 'average'
811 elif graph_level_int == GRAPH_MAX:
812 graph_level = 'max'
814 vendor = vendors_dict.get(int(vendor)) if vendor != '' else ''
815 series = series_dict.get(int(series)).replace(" ", "+")
817 is_graph_data_empty = True
818 if submenu_title == 'for_shop':
819 url = f'http://173.18.0.3:8080/price/for-shop?seriesName={series}&shopName={shop}'
820 response = requests.get(url=url)
821 graph_data = json.loads(response.text)
823 if graph_data:
824 is_graph_data_empty = False
825 offers, prices, vendors_names = {}, {}, []
826 days = get_days_list()
827 for offer in graph_data:
828 card_vendor = offer['vendorName']
829 if card_vendor not in vendors_names:
830 vendors_names.append(card_vendor)
831 allocate_names_and_dates(card_vendor, offer, offers)
833 define_prices_by_graph_level(days, graph_level, offers, prices, vendors_names)
835 draw_graph(vendors_names, days, prices, series, shop, graph_mode='shop', days_mode=graph_days)
836 elif submenu_title == 'for_vendor':
837 url = f'http://173.18.0.3:8080/price/for-vendor?seriesName={series}&vendorName={vendor}'
838 response = requests.get(url=url)
839 graph_data = json.loads(response.text)
841 if graph_data:
842 is_graph_data_empty = False
843 offers, prices, shops_names = {}, {}, ['MVIDEO', 'CITILINK', 'DNS']
844 days = get_days_list()
846 for shop_i in shops_names:
847 offers[shop_i] = {}
849 for offer in graph_data:
850 shop_name = offer['shopName']
851 allocate_names_and_dates(shop_name, offer, offers)
853 define_prices_by_graph_level(days, graph_level, offers, prices, shops_names)
855 draw_graph(shops_names, days, prices, series, vendor, graph_mode='vendor', days_mode=graph_days)
856 else:
857 print('unknown_submenu')
859 query.answer()
861 if is_graph_data_empty:
862 reply_markup_keyboard = InlineKeyboardMarkup(keyboard_ONLY_BACK)
863 with open(no_search_results_dir, 'rb') as photo:
864 image = telegram.InputMediaPhoto(photo)
865 else:
866 reply_markup_keyboard = InlineKeyboardMarkup(keyboard_GRAPH)
867 with open('graphic.png', 'rb') as photo:
868 image = telegram.InputMediaPhoto(photo)
870 query.edit_message_media(
871 media=image
872 )
874 user = context.user_data[CURRENT_USER_NAME]
875 series = series.replace('+', ' ')
877 if is_graph_data_empty:
878 logger.info(f'User {user} get no data for {series}.')
879 query.edit_message_caption(
880 caption=no_data_text,
881 reply_markup=reply_markup_keyboard
882 )
883 else:
884 logger.info(f'User {user} chose {series} button for stats graph '
885 f'with {graph_days} days and {graph_level} level.')
887 query.edit_message_caption(
888 caption=f'submenu: {submenu_title}\n'
889 f'shop: {shop}\n'
890 f'vendor: {vendor}\n'
891 f'arch: {arch}\n'
892 f'series: {series}\n'
893 f'days: {str(graph_days)}\n'
894 f'level: {graph_level}\n' + select_graph_text,
895 reply_markup=reply_markup_keyboard
896 )
898 # Переход в состояние GRAPH_SUBMENU
899 return GRAPH_SUBMENU
902def define_prices_by_graph_level(days, graph_level, offers, prices, shops_names):
903 """Передать записи о видеокартах (offers) по уровню графика (graph_level)
904 для дальнейшего распределения."""
905 result_mode = ''
906 if graph_level == 'min':
907 result_mode = define_names_days_prices(shops_names, days, offers, prices, mode='min')
908 elif graph_level == 'max':
909 result_mode = define_names_days_prices(shops_names, days, offers, prices, mode='max')
910 elif graph_level == 'average':
911 result_mode = define_names_days_prices(shops_names, days, offers, prices, mode='average')
912 else:
913 result_mode = graph_level
914 logger.warning(f'Unknown graph level {graph_level}!')
915 return result_mode
918def allocate_names_and_dates(card_vendor, offer, offers):
919 """Распределить запись о видеокарте (offer) по словарю записей (offers)
920 с указанием производителя (card_vendor)."""
921 if card_vendor not in offers:
922 offers[card_vendor] = {}
923 date = offer['date'].split('T')[0]
924 if date not in offers[card_vendor]:
925 offers[card_vendor][date] = {}
926 offers[card_vendor][date][offer['cardName']] = offer['cardPrice']
929def get_days_list():
930 """Получить список дат всех дней, начиная со дня сбора 20.11.2022 и до сегодняшнего дня."""
931 base = datetime.date(2022, 11, 20)
932 now = datetime.datetime.today().date()
933 days = [str(base + datetime.timedelta(days=x)) for x in range((now - base).days + 1)]
934 return days
937def draw_graph(vendors, days, prices, series, shop, graph_mode='shop', days_mode=30):
938 """Построить график по статистики по дням (days) и ценам (prices) для имен (vendors/shops)
939 с указанием серии (series), магазина (shop), режима отображения (graph_mode)
940 и количества дней (days_mode)."""
942 series = series.replace('+', ' ')
944 plt.set_loglevel('WARNING')
945 fig = plt.figure(figsize=(23.83, 11.68), dpi=100)
946 num_days = range(len(days[-days_mode:]))
947 for card_vendor in vendors:
948 plt.plot(num_days, prices[card_vendor][-days_mode:], label=card_vendor)
949 plt.legend(bbox_to_anchor=(0.5, -0.11), loc='upper center', ncols=4)
950 plt.xlim('2022-11-20', str(datetime.date.today()))
951 plt.xticks(num_days, days[-days_mode:], rotation=45, ha='right')
952 plt.grid(axis='x', linestyle='--')
953 if graph_mode == 'shop':
954 plt.title(f'Statistics for {series} in {shop} store', fontdict={'size': 16})
955 elif graph_mode == 'vendor':
956 plt.title(f'Statistics for {shop} {series} in stores', fontdict={'size': 16})
957 plt.xlabel(f'Period: {days_mode} days', fontdict={'size': 14})
958 plt.ylabel(f'Price, RUB', fontdict={'size': 14})
959 plt.savefig('graphic.png', bbox_inches='tight')
960 plt.clf()
961 plt.close(fig)
964def define_names_days_prices(vendors, days, offers, prices, mode='min'):
965 """Распределить записи о видеокартах (offers) по дням (days) и ценам (prices)
966 с указанием имен и режима отображения (mode) по умолчанию min."""
967 for card_vendor in vendors:
968 prices[card_vendor] = []
969 for day in days:
970 is_from_begin = True
971 choosing_day = day
972 while offers[card_vendor].get(choosing_day) is None:
973 if choosing_day == '2022-11-20':
974 choose_days_list = list(offers[card_vendor].keys())
975 if len(choose_days_list) > 0:
976 choosing_day = list(offers[card_vendor].keys())[0]
977 is_from_begin = False
978 break
979 date_year, date_month, date_day = [int(i) for i in choosing_day.split('-')]
980 prev_day = str(
981 datetime.date(
982 date_year, date_month, date_day
983 ) - datetime.timedelta(days=1)
984 )
985 choosing_day = prev_day
986 if is_from_begin:
987 if mode == 'min':
988 vendor = min(offers[card_vendor][choosing_day], key=offers[card_vendor][choosing_day].get)
989 prices[card_vendor].append(offers[card_vendor][choosing_day][vendor])
990 elif mode == 'max':
991 vendor = max(offers[card_vendor][choosing_day], key=offers[card_vendor][choosing_day].get)
992 prices[card_vendor].append(offers[card_vendor][choosing_day][vendor])
993 elif mode == 'average':
994 prices[card_vendor].append(
995 sum(offers[card_vendor][choosing_day].values()) // len(offers[card_vendor][choosing_day]))
996 else:
997 print('unknown mode')
998 else:
999 prices[card_vendor].append(numpy.NaN)
1000 return mode
1003def help_func(update: Update, context: CallbackContext):
1004 """Возвращает информацию о всех командах и функциях."""
1005 update.message.reply_text(text=help_text)
1008def gpu_search_func(update: Update, context: CallbackContext) -> int:
1009 """Возвращает информацию поиска по конкретной видеокарте."""
1010 context.user_data[CURRENT_DATA] = update.message.text
1011 gpu_name = context.user_data[CURRENT_DATA]
1012 user = update.message.from_user.full_name
1013 logger.info(f'User {user} entered gpu name {gpu_name}.')
1014 gpu_search_list.clear()
1015 # Запрос gpu_name в БД
1016 url = f'http://173.18.0.3:8080/is-card-present?cardName={gpu_name}'
1017 response = requests.get(url=url)
1018 is_card_present = json.loads(response.text)
1020 if is_card_present:
1021 gpu_search_list.append(gpu_name)
1023 if len(gpu_search_list) != 0:
1024 inline_keyboard_gpu_search_buttons(gpu_search_list)
1025 reply_markup_keyboard = InlineKeyboardMarkup(keyboard_ON_SEARCH)
1026 with open(search_dir, 'rb') as photo:
1027 update.message.reply_photo(
1028 photo=photo,
1029 caption=search_results_for_text + gpu_name + ": ",
1030 reply_markup=reply_markup_keyboard
1031 )
1032 # Переход в состояние ON_SEARCH
1033 return ON_SEARCH
1034 else:
1035 with open(no_search_results_dir, 'rb') as photo:
1036 update.message.reply_photo(
1037 photo=photo,
1038 caption=no_search_results_text,
1039 reply_markup=InlineKeyboardMarkup(keyboard_ONLY_BACK)
1040 )
1041 # Переход в состояние ON_GPU_QUESTION -> выход в меню
1042 return ON_GPU_QUESTION
1045def gpu_info(update: Update, context: CallbackContext) -> int:
1046 """Возвращает информацию по видеокарте."""
1047 query = update.callback_query
1048 if query.data.startswith("['gpu'"):
1049 gpu_index = ast.literal_eval(query.data)[1]
1050 gpu_name = keyboard_ON_SEARCH[int(gpu_index)][0].text
1051 context.user_data[CURRENT_DATA] = gpu_name
1052 user = context.user_data[CURRENT_USER_NAME]
1053 logger.info(f'User {user} chose gpu name {gpu_name}.')
1054 context.user_data[CURRENT_GPU] = gpu_name
1056 reply_markup_keyboard = InlineKeyboardMarkup(keyboard_GRAPH_PERIODS)
1058 with open(shops_logo_dir, 'rb') as photo:
1059 image = telegram.InputMediaPhoto(photo)
1061 query.edit_message_media(
1062 media=image
1063 )
1065 query.edit_message_caption(
1066 caption=select_graph_text,
1067 reply_markup=reply_markup_keyboard
1068 )
1070 # Переход в состояние GRAPH_SUBMENU_ON_GPU
1071 return GRAPH_SUBMENU_ON_GPU
1074def inline_keyboard_gpu_search_buttons(gpu_search: []):
1075 """Вставка кнопок в клавиатуру из найденных видеокарт (gpu_search) по запросу из БД."""
1076 # Получаем только уникальные видеокарты из поиска
1077 unique_gpu = list({gpu: gpu for gpu in gpu_search}.values())
1078 # Очистка клавиатуры
1079 keyboard_ON_SEARCH.clear()
1080 for gpu in unique_gpu:
1081 keyboard_ON_SEARCH.append(
1082 [InlineKeyboardButton(
1083 gpu, callback_data="['gpu', '" + str(unique_gpu.index(gpu)) + "']"
1084 )]
1085 )
1088def end_on_gpu(update: Update, context: CallbackContext) -> int:
1089 """Конец разговора по видеокарте, возврат к основному разговору."""
1090 user = context.user_data[CURRENT_USER_NAME]
1091 logger.info(f'User {user} ended for gpu searching.')
1092 new_start(update, context)
1093 return BACK_TO_MENU
1096def start_fallback(update, context) -> int:
1097 """Функция обертка для start, чтобы сделать fallback
1098 по нажатию /start в любом состоянии разговора."""
1099 start(update, context)
1100 return BACK_TO_MENU
1103def error_attention(update: Update, context: CallbackContext):
1104 """Вывод сообщения об ошибке, если пользователь ввел текст не выбирая кнопок."""
1105 user = update.message.from_user.full_name.encode(encoding='utf-8').decode()
1106 logger.info(f'User {user} entered message without a reason.')
1108 update.message.reply_text(
1109 text=error_enter_text
1110 )
1113def new_start(update: Update, context: CallbackContext):
1114 """Возвращает текст и клавиатуру к главному меню."""
1115 user = context.user_data[CURRENT_USER_NAME]
1116 logger.info(f'User {user} returned to the menu.')
1118 update_query_message_with_keyboard(
1119 update=update,
1120 context=context,
1121 keyboard=keyboard_MENU,
1122 image_path=shops_logo_dir,
1123 caption_text=using_buttons_text,
1124 )
1126 return MENU