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

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') 

17 

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) 

25 

26 

27def start(update: Update, context: CallbackContext) -> int: 

28 """Вывести сообщение и клавиатуру меню на команду '/start'""" 

29 reset_context(context) 

30 

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.') 

34 

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) 

42 

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 ) 

50 

51 # Переход в состояние MENU 

52 return MENU 

53 

54 

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.') 

59 

60 reset_context(context) 

61 

62 context.user_data[CURRENT_USER_NAME] = user 

63 

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 ) 

71 

72 return MENU 

73 

74 

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] = '' 

88 

89 

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) 

99 

100 with open(image_path, 'rb') as photo: 

101 image = telegram.InputMediaPhoto(photo) 

102 

103 query.edit_message_media( 

104 media=image 

105 ) 

106 

107 query.edit_message_caption( 

108 caption=caption_text, 

109 reply_markup=reply_markup_keyboard 

110 ) 

111 

112 

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.') 

120 

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 ) 

128 

129 return STATS_SUBMENU 

130 elif update.callback_query.data == str(POPULARITY): 

131 logger.info(f'User {user} pressed the popularity button.') 

132 

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 ) 

140 

141 return POPULARITY_SUBMENU 

142 else: 

143 logger.warning(f'User {user} pressed incorrect button for menu!') 

144 

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 ) 

152 

153 return MENU 

154 

155 

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.') 

164 

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 ) 

173 

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.') 

177 

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 ) 

186 

187 return VENDORS_SUBMENU 

188 else: 

189 logger.warning(f'User {user} pressed incorrect button for stats!') 

190 

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 ) 

198 

199 return MENU 

200 

201 

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.') 

209 

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 ) 

217 

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.') 

221 

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 ) 

229 

230 return POPULARITY_VENDORS_SUBMENU 

231 else: 

232 logger.warning(f'User {user} pressed incorrect button for stats!') 

233 

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 ) 

241 

242 return MENU 

243 

244 

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' 

255 

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) 

259 

260 card_names, places = define_card_names_places(graph_data) 

261 

262 draw_popularity_cards_places(card_names, places, shop) 

263 

264 user = context.user_data[CURRENT_USER_NAME] 

265 logger.info(f'User {user} chose shop {shop} for popularity graph.') 

266 

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 ) 

274 

275 return POPULARITY_SHOPS_GRAPH_SUBMENU 

276 

277 

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) 

308 

309 

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') 

325 

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') 

342 

343 return card_names, places 

344 

345 

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 ) 

353 

354 

355def popularity_vendors_graph(update: Update, context: CallbackContext) -> int: 

356 """Показать график популярности видеокарт по производителю""" 

357 query = update.callback_query 

358 

359 vendor = vendors_dict.get(int(query.data)) if query.data != '' else '' 

360 

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) 

364 

365 message_caption, popularity_places_shops = define_popularity_places_shops(graph_data, vendor) 

366 

367 draw_popularity_vendors_graph(popularity_places_shops, vendor) 

368 

369 user = context.user_data[CURRENT_USER_NAME] 

370 logger.info(f'User {user} chose vendor {vendor} for popularity graph.') 

371 

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 ) 

379 

380 return POPULARITY_VENDORS_GRAPH_SUBMENU 

381 

382 

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 

403 

404 

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) 

441 

442 

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.') 

447 

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 

452 

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 ) 

460 

461 return GPU_SUBMENU 

462 

463 

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 ) 

477 

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.') 

481 

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 ) 

492 

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.') 

496 

497 return ARCHITECTURE_SUBMENU 

498 else: 

499 logger.warning(f'User {user} chose incorrect button for stats!') 

500 

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 ) 

508 

509 return MENU 

510 

511 

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.') 

517 

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 ) 

526 

527 return NVIDIA_SERIES_SUBMENU 

528 elif update.callback_query.data == str(AMD): 

529 logger.info(f'User {user} chose AMD arch for stats.') 

530 

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 ) 

539 

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.') 

543 

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 ) 

551 

552 return OTHER_ARCH_SUBMENU 

553 elif update.callback_query.data == str(INTEL): 

554 logger.info(f'User {user} chose INTEL arch for stats.') 

555 

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 ) 

564 

565 return INTEL_SERIES_SUBMENU 

566 elif update.callback_query.data == str(MATROX): 

567 logger.info(f'User {user} chose MATROX arch for stats.') 

568 

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 ) 

577 

578 return MATROX_SERIES_SUBMENU 

579 else: 

580 logger.warning(f'User {user} chose incorrect arch for stats!') 

581 

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 ) 

589 

590 return MENU 

591 

592 

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 

602 

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 ) 

610 

611 return series_buttons_dict.get(int(series_button))['returning'] if series_button != '' else MENU 

612 

613 

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 

623 

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 ) 

631 

632 return series_buttons_dict.get(int(series_button))['returning'] if series_button != '' else MENU 

633 

634 

635def graph_for_gpu_func(update: Update, context: CallbackContext) -> int: 

636 """Показать график цен по видеокарте""" 

637 

638 submenu_title, shop_title, vendor = 'for_gpu', '', 20 

639 

640 query = update.callback_query 

641 

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 

649 

650 graph_days = context.user_data[CURRENT_GRAPH_GPU_DAYS] 

651 

652 card_name = context.user_data[CURRENT_GPU] 

653 

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) 

657 

658 offers, prices = {}, [] 

659 days = get_days_list() 

660 

661 for offer in graph_data: 

662 card_price = offer['cardPrice'] 

663 date = offer['date'].split('T')[0] 

664 offers[date] = card_price 

665 

666 define_gpu_days_prices(days, offers, prices) 

667 

668 draw_gpu_graph(days, prices, card_name, days_mode=graph_days) 

669 

670 user = context.user_data[CURRENT_USER_NAME] 

671 logger.info(f'User {user} chose gpu {card_name} for {graph_days} days stats graph.') 

672 

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 

676 

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 ) 

684 

685 return GRAPH_SUBMENU_ON_GPU 

686 

687 

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) 

712 

713 

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) 

731 

732 

733def graph_func(update: Update, context: CallbackContext) -> int: 

734 """Показать график цен""" 

735 user_data = context.user_data 

736 submenu = user_data[CURRENT_SUBMENU] 

737 

738 submenu_title, shop, vendor = '', '', 20 

739 if submenu == str(FOR_SHOP): 

740 submenu_title = 'for_shop' 

741 shop = user_data[CURRENT_SHOP] 

742 

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') 

757 

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') 

770 

771 query = update.callback_query 

772 

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 

779 

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 

802 

803 graph_days = context.user_data[CURRENT_GRAPH_DAYS] 

804 graph_level_int = context.user_data[CURRENT_GRAPH_LEVEL] + 3 

805 

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' 

813 

814 vendor = vendors_dict.get(int(vendor)) if vendor != '' else '' 

815 series = series_dict.get(int(series)).replace(" ", "+") 

816 

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) 

822 

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) 

832 

833 define_prices_by_graph_level(days, graph_level, offers, prices, vendors_names) 

834 

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) 

840 

841 if graph_data: 

842 is_graph_data_empty = False 

843 offers, prices, shops_names = {}, {}, ['MVIDEO', 'CITILINK', 'DNS'] 

844 days = get_days_list() 

845 

846 for shop_i in shops_names: 

847 offers[shop_i] = {} 

848 

849 for offer in graph_data: 

850 shop_name = offer['shopName'] 

851 allocate_names_and_dates(shop_name, offer, offers) 

852 

853 define_prices_by_graph_level(days, graph_level, offers, prices, shops_names) 

854 

855 draw_graph(shops_names, days, prices, series, vendor, graph_mode='vendor', days_mode=graph_days) 

856 else: 

857 print('unknown_submenu') 

858 

859 query.answer() 

860 

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) 

869 

870 query.edit_message_media( 

871 media=image 

872 ) 

873 

874 user = context.user_data[CURRENT_USER_NAME] 

875 series = series.replace('+', ' ') 

876 

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.') 

886 

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 ) 

897 

898 # Переход в состояние GRAPH_SUBMENU 

899 return GRAPH_SUBMENU 

900 

901 

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 

916 

917 

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'] 

927 

928 

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 

935 

936 

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).""" 

941 

942 series = series.replace('+', ' ') 

943 

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) 

962 

963 

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 

1001 

1002 

1003def help_func(update: Update, context: CallbackContext): 

1004 """Возвращает информацию о всех командах и функциях.""" 

1005 update.message.reply_text(text=help_text) 

1006 

1007 

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) 

1019 

1020 if is_card_present: 

1021 gpu_search_list.append(gpu_name) 

1022 

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 

1043 

1044 

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 

1055 

1056 reply_markup_keyboard = InlineKeyboardMarkup(keyboard_GRAPH_PERIODS) 

1057 

1058 with open(shops_logo_dir, 'rb') as photo: 

1059 image = telegram.InputMediaPhoto(photo) 

1060 

1061 query.edit_message_media( 

1062 media=image 

1063 ) 

1064 

1065 query.edit_message_caption( 

1066 caption=select_graph_text, 

1067 reply_markup=reply_markup_keyboard 

1068 ) 

1069 

1070 # Переход в состояние GRAPH_SUBMENU_ON_GPU 

1071 return GRAPH_SUBMENU_ON_GPU 

1072 

1073 

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 ) 

1086 

1087 

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 

1094 

1095 

1096def start_fallback(update, context) -> int: 

1097 """Функция обертка для start, чтобы сделать fallback 

1098 по нажатию /start в любом состоянии разговора.""" 

1099 start(update, context) 

1100 return BACK_TO_MENU 

1101 

1102 

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.') 

1107 

1108 update.message.reply_text( 

1109 text=error_enter_text 

1110 ) 

1111 

1112 

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.') 

1117 

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 ) 

1125 

1126 return MENU