diff --git a/(K+U+B)^3=KUB v2.py b/(K+U+B)^3=KUB v2.py index 385488445..5bfa792ba 100644 --- a/(K+U+B)^3=KUB v2.py +++ b/(K+U+B)^3=KUB v2.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/gil9red/SimplePyScripts/issues/13#issuecomment-943412289 @@ -15,5 +15,5 @@ for num in range(100, 1000): kub = set(str(num)) - if len(kub) == 3 and sum(map(int, kub))**3 == num: + if len(kub) == 3 and sum(map(int, kub)) ** 3 == num: print(f'({"+".join(str(num))})^3={num}') diff --git a/(K+U+B)^3=KUB.py b/(K+U+B)^3=KUB.py index e49f99cc9..20618a4b5 100644 --- a/(K+U+B)^3=KUB.py +++ b/(K+U+B)^3=KUB.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stepik.org/lesson/360560/step/13?unit=345000 @@ -23,5 +23,5 @@ kub = k * 100 + u * 10 + b # Проверка, что число трехзначное или не совпадает условие - if kub <= 999 and kub == (k+u+b) ** 3: - print(f'({k}+{u}+{b})^3={kub}') + if kub <= 999 and kub == (k + u + b) ** 3: + print(f"({k}+{u}+{b})^3={kub}") diff --git a/.gitignore b/.gitignore index 6a1c0e53b..ac96cbc34 100644 --- a/.gitignore +++ b/.gitignore @@ -108,5 +108,13 @@ docs/_build/ target/ # Current project +.gigaide/ +backup/ +database/ TOKEN.txt LOGIN_PASSWORD.txt +geckodriver.log +*.sqlite* +*.db +log.txt +settings.* diff --git a/AnalysisOfChatLog/AnalysisOfChatLog.py b/AnalysisOfChatLog/AnalysisOfChatLog.py index da11354b3..02cb13777 100644 --- a/AnalysisOfChatLog/AnalysisOfChatLog.py +++ b/AnalysisOfChatLog/AnalysisOfChatLog.py @@ -1,25 +1,27 @@ # coding=utf-8 -__author__ = 'ipetrash' +__author__ = "ipetrash" # Есть лог-файл какого-то чата. Посчитать «разговорчивость» пользователей в нем в виде ник — количество фраз. # Посчитать среднее число букв на участника чата. -if __name__ == '__main__': - #file_name = raw_input("Chat log file: ") - file_name = "C:\Users\ipetrash\Desktop\chat log.txt" - user_count = {} - for line in file(file_name): - line = line.decode("windows-1251").replace("\n", "") - user, count = line.split(":")[0], len(line.split(":")[1].split(' ')) - 1 - if user in user_count: - user_count[user] += count - else: - user_count[user] = count - - sum = 0.0 - for user, count in user_count.items(): - sum += count - print(user + " = %d" % count) - - print("Average = %s" % (sum / len(user_count)) ) \ No newline at end of file + +file_name = r"chat log.txt" + +user_count: dict[str, int] = dict() +for line in open(file_name, encoding="utf-8"): + line = line.strip() + + user, text = line.split(":", maxsplit=1) + count = len(text.strip().split(" ")) - 1 + if user in user_count: + user_count[user] += count + else: + user_count[user] = count + +total = 0 +for user, count in user_count.items(): + total += count + print(f"{user} = {count}") + +print(f"Average = {total // len(user_count)}") diff --git a/AnalysisOfChatLog/chat log.txt b/AnalysisOfChatLog/chat log.txt new file mode 100644 index 000000000..800b904d7 --- /dev/null +++ b/AnalysisOfChatLog/chat log.txt @@ -0,0 +1,14 @@ +xxx: Многомного зарядный револьвер конечно жесть. Зачем оно надо? Ведь мы прекресно знаем из американских фильмов, что в обычном шестизарядном барабане пули не кончяются никогда, машина взрывается от одного выстрела девятым калибром, а стоит тебе спасти девушку от смерти, как она тут же с радостью даёт. +yyy: Чувак, в 1855 году не было автомобилей. От одного выстрела взрывалась лошадь. +zzz: Да-да, у ковбоев были специальные отравленные пули - с ДВУМЯ каплями никотина. +xxx: интересно, сколько интересно щас емкость всего инета? +xxx: нашел: Объем общемировых данных вырастет с 33 зеттабайт в 2018 году до 175 в 2025. +xxx: я таких приставок даже не знаю +yyy: да бля +yyy: 99 процентов это порно +yyy: остальные 99 процентов это видео с котиками +yyy: еще 99 процентов это порно с котиками. +yyy: и того 297 процентов говна +xxx: Пойду заниматься похоронным бизнесом. Кто со мной жмите класс. +yyy: Супер мега идея, я щас умру от восторга! +ххх: Будешь первым клиентом) \ No newline at end of file diff --git a/BFS__breadth_first_search.py b/BFS__breadth_first_search.py index eaa7f04fa..ff69f3612 100644 --- a/BFS__breadth_first_search.py +++ b/BFS__breadth_first_search.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/47902476/5909792 from collections import deque -from typing import List, Tuple, Any +from typing import Any # Breadth-first search -def bfs(grid: List[List[Any]], start: Tuple[Any, Any], goal: Any, wall: Any) -> List[Tuple[Any, Any]]: +def bfs( + grid: list[list[Any]], start: tuple[Any, Any], goal: Any, wall: Any +) -> list[tuple[Any, Any]]: width, height = len(grid[0]), len(grid) queue = deque([[start]]) seen = {start} @@ -23,12 +25,17 @@ def bfs(grid: List[List[Any]], start: Tuple[Any, Any], goal: Any, wall: Any) -> return path for x2, y2 in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): - if 0 <= x2 < width and 0 <= y2 < height and grid[y2][x2] != wall and (x2, y2) not in seen: + if ( + 0 <= x2 < width + and 0 <= y2 < height + and grid[y2][x2] != wall + and (x2, y2) not in seen + ): queue.append(path + [(x2, y2)]) seen.add((x2, y2)) -if __name__ == '__main__': +if __name__ == "__main__": start = 5, 2 wall, goal = "#", "*" grid = [ @@ -39,8 +46,8 @@ def bfs(grid: List[List[Any]], start: Tuple[Any, Any], goal: Any, wall: Any) -> list("......*..."), ] x, y = start - grid[y][x] = '@' # Start - print('\n'.join(''.join(row) for row in grid)) + grid[y][x] = "@" # Start + print("\n".join("".join(row) for row in grid)) # .......... # ..*#...##. # ..##.@.#*. @@ -54,8 +61,8 @@ def bfs(grid: List[List[Any]], start: Tuple[Any, Any], goal: Any, wall: Any) -> # [(5, 2), (4, 2), (4, 3), (4, 4), (5, 4), (6, 4)] for x, y in path[1:]: - grid[y][x] = 'x' - print('\n'.join(''.join(row) for row in grid)) + grid[y][x] = "x" + print("\n".join("".join(row) for row in grid)) # .......... # ..*#...##. # ..##x@.#*. diff --git a/Base64_examples/decode_to_file.py b/Base64_examples/decode_to_file.py index cfbdff5ce..a2ce75c7e 100644 --- a/Base64_examples/decode_to_file.py +++ b/Base64_examples/decode_to_file.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from base64 import b64decode -def decode_base64_to_file(file_name: str, text_base64: str): - with open(file_name, 'wb') as f: +def decode_base64_to_file(file_name: str, text_base64: str) -> None: + with open(file_name, "wb") as f: data = b64decode(text_base64) f.write(data) -if __name__ == '__main__': +if __name__ == "__main__": # Hello World! - text = 'SGVsbG8gV29ybGQh' + text = "SGVsbG8gV29ybGQh" - decode_base64_to_file('result.txt', text) + decode_base64_to_file("result.txt", text) diff --git a/Base64_examples/decode_to_file_certificate__and_open.py b/Base64_examples/decode_to_file_certificate__and_open.py index 58ab98547..b2375126c 100644 --- a/Base64_examples/decode_to_file_certificate__and_open.py +++ b/Base64_examples/decode_to_file_certificate__and_open.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -FILE_NAME = 'cert.cer' +FILE_NAME = "cert.cer" TEXT = """ MIIBeTCCASigAwIBAgIEBoLBizAIBgYqhQMCAgMwMTELMAkGA1UEBhMCUlUxEjAQBgNVBAoMCUNy eXB0b1BybzEOMAwGA1UEAwwFQWxpYXMwHhcNMTcwNjA3MDk1MzM5WhcNMTgwNjA3MDk1MzM5WjAx @@ -16,8 +16,10 @@ """ from decode_to_file import decode_base64_to_file + decode_base64_to_file(FILE_NAME, TEXT) # Open file import os + os.startfile(FILE_NAME) diff --git a/Base64_examples/example_encode_and_decode.py b/Base64_examples/example_encode_and_decode.py index cae57b246..ba4960a1c 100644 --- a/Base64_examples/example_encode_and_decode.py +++ b/Base64_examples/example_encode_and_decode.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import base64 text = "Hello py! Привет py!" -print("Text: {}".format(text)) +print(f"Text: {text}") b16 = base64.b16encode(text.encode()) b32 = base64.b32encode(text.encode()) diff --git a/Base64_examples/gui_base64.py b/Base64_examples/gui_base64.py index 946d5aeb6..ba49bbf97 100644 --- a/Base64_examples/gui_base64.py +++ b/Base64_examples/gui_base64.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import base64 import traceback import sys +from os.path import split as path_split + try: from PyQt5.QtWidgets import * from PyQt5.QtCore import * @@ -22,12 +24,12 @@ from PySide.QtCore import * -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) @@ -129,15 +131,14 @@ def log_uncaught_exceptions(ex_cls, ex, tb): "utf_16_le", "utf_7", "utf_8", - "utf_8_sig" + "utf_8_sig", ] class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - from os.path import split as path_split self.setWindowTitle(path_split(__file__)[1]) self.button_direct = QPushButton() @@ -147,10 +148,10 @@ def __init__(self): self.cb_encoding.addItems(STANDART_ENCODINGS) self.cb_encoding.setFixedWidth(100) - index = self.cb_encoding.findText('utf_8') + index = self.cb_encoding.findText("utf_8") self.cb_encoding.setCurrentIndex(index) - self.cb_raw = QCheckBox('raw') + self.cb_raw = QCheckBox("raw") button_layout = QHBoxLayout() button_layout.addWidget(self.button_direct) @@ -170,9 +171,9 @@ def __init__(self): self.label_error.setTextInteractionFlags(Qt.TextSelectableByMouse) self.label_error.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.button_detail_error = QPushButton('...') + self.button_detail_error = QPushButton("...") self.button_detail_error.setFixedSize(20, 20) - self.button_detail_error.setToolTip('Detail error') + self.button_detail_error.setToolTip("Detail error") self.button_detail_error.clicked.connect(self.show_detail_error_massage) self.last_error_message = None @@ -204,11 +205,11 @@ def __init__(self): self.setLayout(layout) - def show_detail_error_massage(self): - message = self.last_error_message + '\n\n' + self.last_detail_error_message + def show_detail_error_massage(self) -> None: + message = self.last_error_message + "\n\n" + self.last_detail_error_message mb = QErrorMessage() - mb.setWindowTitle('Error') + mb.setWindowTitle("Error") # Сообщение ошибки содержит отступы, символы-переходы на следующую строку, # которые поломаются при вставке через QErrorMessage.showMessage, и нет возможности # выбрать тип текста, то делаем такой хак. @@ -216,7 +217,7 @@ def show_detail_error_massage(self): mb.exec_() - def input_text_changed(self): + def input_text_changed(self) -> None: self.label_error.clear() self.button_detail_error.hide() @@ -238,7 +239,7 @@ def input_text_changed(self): else: # Параметр errors='replace' нужен для того, чтобы при декодировании в строку проблемные символы # заменялись символами-заменителями (�) - text = text.decode(encoding=codec_name, errors='replace') + text = text.decode(encoding=codec_name, errors="replace") self.text_edit_output.setPlainText(text) @@ -253,16 +254,18 @@ def input_text_changed(self): self.last_detail_error_message = str(tb) self.button_detail_error.show() - self.label_error.setText('Error: ' + self.last_error_message) + self.label_error.setText("Error: " + self.last_error_message) - def change_convert_direct(self): + def change_convert_direct(self) -> None: self.direct_encode_text = not self.direct_encode_text - self.button_direct.setText('text -> base64' if self.direct_encode_text else 'base64 -> text') + self.button_direct.setText( + "text -> base64" if self.direct_encode_text else "base64 -> text" + ) self.input_text_changed() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/Bot Buff Knight Advanced/__foo_test.py b/Bot Buff Knight Advanced/__foo_test.py deleted file mode 100644 index 56f082ba3..000000000 --- a/Bot Buff Knight Advanced/__foo_test.py +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -__author__ = 'ipetrash' - - -import glob -from timeit import default_timer as timer - -import cv2 -import numpy as np - -from main import find_rect_contours, filter_button, filter_fairy, filter_fairy_and_button -from common import get_current_datetime_str - - -def draw_rects(img, contours_rects, color=(0, 255, 0)): - for x, y, w, h in contours_rects: - cv2.rectangle(img, (x, y), (x + w, y + h), color, thickness=5) - # cv2.rectangle(img, (x, y), (x + w, y + h), color, thickness=-1) - - for i in range(h // 10): - cv2.line(img, (x, y + i * 10 + 5), (x + w, y + i * 10 + 5), color, thickness=2) - - cv2.line(img, (0, y), (img.shape[1], y), color, thickness=1) - - -BLUE_HSV_MIN = 105, 175, 182 -BLUE_HSV_MAX = 121, 255, 255 - -ORANGE_HSV_MIN = 7, 200, 200 -ORANGE_HSV_MAX = 20, 255, 255 - -FAIRY_HSV_MIN = 73, 101, 101 -FAIRY_HSV_MAX = 95, 143, 255 - - -for file_name in glob.glob('screenshots__Buff Knight Advanced/*.png'): - print(file_name) - - img = cv2.imread(file_name) - - # # Resize - # img = cv2.resize(img, None, fx=0.7, fy=0.7, interpolation=cv2.INTER_CUBIC) - - img_screenshot = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - image_source_hsv = cv2.cvtColor(np.array(img_screenshot), cv2.COLOR_RGB2HSV) - # cv2.imshow('img', img) - print(img.shape[:2]) - - img_with_rect = img.copy() - - t = timer() - - try: - rects_blue = find_rect_contours(image_source_hsv, BLUE_HSV_MIN, BLUE_HSV_MAX) - rects_orange = find_rect_contours(image_source_hsv, ORANGE_HSV_MIN, ORANGE_HSV_MAX) - rects_fairy = find_rect_contours(image_source_hsv, FAIRY_HSV_MIN, FAIRY_HSV_MAX) - - # print('rects_fairy:', rects_fairy) - - # Фильтрование оставшихся объектов - rects_blue = list(filter(filter_button, rects_blue)) - rects_orange = list(filter(filter_button, rects_orange)) - rects_fairy = list(filter(filter_fairy, rects_fairy)) - - # print('rects_fairy1:', rects_fairy) - - if rects_blue or rects_orange: - new_rects_fairy = [] - - # Фея и кнопки находятся рядом, поэтому имеет смысл убрать те "феи", что не имеют рядом синих или оранжевых - for rect_fairy in rects_fairy: - x, y = rect_fairy[:2] - - found = False - - found_blue = bool(list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_blue))) - found_orange = bool(list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_orange))) - # print(x, y, found_blue or found_orange) - - if found_blue or found_orange: - new_rects_fairy.append(rect_fairy) - - rects_fairy = new_rects_fairy - - print('rects_fairy2:', rects_fairy) - - if not rects_fairy: - continue - - if len(rects_fairy) > 1: - file_name = f'many_fairy__{get_current_datetime_str()}.png' - print(file_name) - cv2.imwrite(file_name, img_with_rect) - continue - - # Фильтр кнопок. Нужно оставить только те кнопки, что рядом с феей - rect_fairy = rects_fairy[0] - rects_blue = list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_blue)) - rects_orange = list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_orange)) - - # Если одновременно обе кнопки - if rects_blue and rects_orange: - file_name = f'many_buttons__{get_current_datetime_str()}.png' - print(file_name) - cv2.imwrite(file_name, img_with_rect) - continue - - if not rects_blue and not rects_orange: - continue - - print(f'rects_blue({len(rects_blue)}): {rects_blue}') - print(f'rects_orange({len(rects_orange)}): {rects_orange}') - print(f'rects_fairy({len(rects_fairy)}): {rects_fairy}') - - draw_rects(img_with_rect, rects_blue, (255, 0, 0)) - draw_rects(img_with_rect, rects_orange, (0, 0, 255)) - draw_rects(img_with_rect, rects_fairy, (255, 255, 255)) - - cv2.putText(img_with_rect, 'blue ' + str(rects_blue), (10, 500 + 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2, cv2.LINE_AA) - cv2.putText(img_with_rect, 'orange ' + str(rects_orange), (10, 500 + 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2, cv2.LINE_AA) - cv2.putText(img_with_rect, 'fairy ' + str(rects_fairy), (10, 500 + 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2, cv2.LINE_AA) - - finally: - print(f'Elapsed: {timer() - t} secs') - print() - - cv2.imshow('img_with_rect ' + file_name, img_with_rect) - - # cv2.imwrite(new_file_name, img_with_rect) - # cv2.imwrite('img_with_rect.png', img_with_rect) - -cv2.waitKey() diff --git a/Bot Buff Knight Advanced/common.py b/Bot Buff Knight Advanced/common.py deleted file mode 100644 index 9eeb891ef..000000000 --- a/Bot Buff Knight Advanced/common.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from datetime import datetime -import cv2 -import numpy as np - - -def get_logger(name, file='log.txt', encoding='utf-8', log_stdout=True, log_file=True): - import logging - log = logging.getLogger(name) - log.setLevel(logging.DEBUG) - - formatter = logging.Formatter('[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s') - - if log_file: - from logging.handlers import RotatingFileHandler - fh = RotatingFileHandler(file, maxBytes=10000000, backupCount=5, encoding=encoding) - fh.setFormatter(formatter) - log.addHandler(fh) - - if log_stdout: - import sys - sh = logging.StreamHandler(stream=sys.stdout) - sh.setFormatter(formatter) - log.addHandler(sh) - - return log - - -def get_current_datetime_str() -> str: - return datetime.now().strftime('%Y-%m-%d %H%M%S.%f') - - -def find_contours(image_source_hsv, hsv_min, hsv_max): - thresholded_image = image_source_hsv - - # Отфильтровываем только то, что нужно, по диапазону цветов - thresholded_image = cv2.inRange( - thresholded_image, - np.array(hsv_min, np.uint8), - np.array(hsv_max, np.uint8) - ) - - # Убираем шум - thresholded_image = cv2.erode( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) - ) - thresholded_image = cv2.dilate( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) - ) - - # Замыкаем оставшиеся крупные объекты - thresholded_image = cv2.dilate( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) - ) - thresholded_image = cv2.erode( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) - ) - - # Находим контуры - contours, _ = cv2.findContours( - thresholded_image, - cv2.RETR_EXTERNAL, - cv2.CHAIN_APPROX_SIMPLE - ) - - return contours - - -def find_rect_contours(image_source_hsv, hsv_min, hsv_max): - return [cv2.boundingRect(c) for c in find_contours(image_source_hsv, hsv_min, hsv_max)] diff --git a/Bot Buff Knight Advanced/main.py b/Bot Buff Knight Advanced/main.py deleted file mode 100644 index 08518ff2b..000000000 --- a/Bot Buff Knight Advanced/main.py +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -__author__ = 'ipetrash' - - -import threading -import os -import time - -from timeit import default_timer as timer - -# pip install opencv-python -import cv2 -import numpy as np -import pyautogui -import keyboard - -from common import get_logger, get_current_datetime_str, find_rect_contours - - -def filter_button(rect): - x, y, w, h = rect - - # TODO: эффективнее для разных разрешений сравнивать процентно, а не пиксельно - - rule_1 = w > 30 and h > 30 - rule_2 = w > h - - # У кнопок ширина больше высоты - return rule_1 and rule_2 - - -def filter_fairy(rect): - x, y, w, h = rect - - # TODO: эффективнее для разных разрешений сравнивать процентно, а не пиксельно - - # У феи ширина и высота больше определенной цифры - rule_1 = w > 20 and h > 20 - - # У феи высота больше ширины - rule_2 = h > w - - # Фея приблизительно по центру экрана летает - rule_3 = y > 300 and y < 900 - - return rule_1 and rule_2 and rule_3 - - -def filter_fairy_and_button(rect_fairy, rect_button): - x, y = rect_fairy[:2] - x2, y2, _, h2 = rect_button - - # TODO: эффективнее для разных разрешений сравнивать процентно, а не пиксельно - return abs(x2 - x) <= 50 and abs(y2 + h2 - y) <= 50 - - -def save_screenshot(prefix, img_hsv): - file_name = DIR + '/{}__{}.png'.format(prefix, get_current_datetime_str()) - log.debug(file_name) - cv2.imwrite(file_name, cv2.cvtColor(np.array(img_hsv), cv2.COLOR_HSV2BGR)) - - -log = get_logger('Bot Buff Knight Advanced') - -DIR = 'saved_screenshots' -if not os.path.exists(DIR): - os.mkdir(DIR) - -BLUE_HSV_MIN = 105, 175, 182 -BLUE_HSV_MAX = 121, 255, 255 - -ORANGE_HSV_MIN = 7, 200, 200 -ORANGE_HSV_MAX = 20, 255, 255 - -FAIRY_HSV_MIN = 73, 101, 101 -FAIRY_HSV_MAX = 95, 143, 255 - - -RUN_COMBINATION = 'Ctrl+Shift+R' -QUIT_COMBINATION = 'Ctrl+Shift+Q' -AUTO_ATTACK_COMBINATION = 'Space' - -BOT_DATA = { - 'START': False, - 'AUTO_ATTACK': False, -} - - -def change_start(): - BOT_DATA['START'] = not BOT_DATA['START'] - log.debug('START: %s', BOT_DATA['START']) - - -def change_auto_attack(): - BOT_DATA['AUTO_ATTACK'] = not BOT_DATA['AUTO_ATTACK'] - log.debug('AUTO_ATTACK: %s', BOT_DATA['AUTO_ATTACK']) - - -log.debug('Press "%s" for RUN / PAUSE', RUN_COMBINATION) -log.debug('Press "%s" for QUIT', QUIT_COMBINATION) -log.debug('Press "%s" for AUTO_ATTACK', AUTO_ATTACK_COMBINATION) - - -def process_auto_attack(): - while True: - if not BOT_DATA['START']: - time.sleep(0.01) - continue - - # Симуляция атаки - if BOT_DATA['AUTO_ATTACK']: - pyautogui.typewrite('C') - - time.sleep(0.01) - - -def process_find_fairy(img_hsv): - rects_blue = find_rect_contours(img_hsv, BLUE_HSV_MIN, BLUE_HSV_MAX) - rects_orange = find_rect_contours(img_hsv, ORANGE_HSV_MIN, ORANGE_HSV_MAX) - rects_fairy = find_rect_contours(img_hsv, FAIRY_HSV_MIN, FAIRY_HSV_MAX) - - # Фильтрование оставшихся объектов - rects_blue = list(filter(filter_button, rects_blue)) - rects_orange = list(filter(filter_button, rects_orange)) - rects_fairy = list(filter(filter_fairy, rects_fairy)) - - # Фильтр объектов, похожих на фею - if rects_blue or rects_orange: - new_rects_fairy = [] - - # Фея и кнопки находятся рядом, поэтому имеет смысл убрать те "феи", что не имеют рядом синих или оранжевых - for rect_fairy in rects_fairy: - found_blue = bool(list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_blue))) - found_orange = bool(list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_orange))) - - # Если возле феи что-то нашлось - if found_blue or found_orange: - new_rects_fairy.append(rect_fairy) - - rects_fairy = new_rects_fairy - - if not rects_fairy: - return - - if len(rects_fairy) > 1: - save_screenshot('many_fairy', img_hsv) - return - - # Фильтр кнопок. Нужно оставить только те кнопки, что рядом с феей - rect_fairy = rects_fairy[0] - rects_blue = list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_blue)) - rects_orange = list(filter(lambda rect: filter_fairy_and_button(rect_fairy, rect), rects_orange)) - - # Если одновременно обе кнопки - if rects_blue and rects_orange: - save_screenshot('many_buttons', img_hsv) - return - - if not rects_blue and not rects_orange: - return - - # Найдена синяя кнопка - if rects_blue: - log.debug('FOUND BLUE') - save_screenshot('found_blue', img) - - pyautogui.typewrite('D') - - # Найдена оранжевая кнопка - if rects_orange: - log.debug('FOUND ORANGE') - save_screenshot('found_orange', img) - - pyautogui.typewrite('A') - - -if __name__ == '__main__': - keyboard.add_hotkey(QUIT_COMBINATION, lambda: log.debug('Quit by Escape') or os._exit(0)) - keyboard.add_hotkey(AUTO_ATTACK_COMBINATION, change_auto_attack) - keyboard.add_hotkey(RUN_COMBINATION, change_start) - - # Запуск потока для автоатаки - thread_auto_attack = threading.Thread(target=process_auto_attack) - thread_auto_attack.start() - - while True: - if not BOT_DATA['START']: - time.sleep(0.01) - continue - - t = timer() - - try: - img_screenshot = pyautogui.screenshot() - log.debug('img_screenshot: %s', img_screenshot) - - img = cv2.cvtColor(np.array(img_screenshot), cv2.COLOR_RGB2HSV) - - # Поиск феи - process_find_fairy(img) - - # TODO: возможность автоматического использования хилок и восстановления маны - - finally: - log.debug(f'Elapsed: {timer() - t} secs') - - time.sleep(0.01) diff --git a/Builder__example.py b/Builder__example.py deleted file mode 100644 index 85b352bd9..000000000 --- a/Builder__example.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -class Foo: - def __init__(self): - self.items = [] - self.key_by_value = dict() - - def add_item(self, value): - self.items.append(value) - return self - - def add_items(self, values): - self.items += values - return self - - def set_value(self, key, value): - self.key_by_value[key] = value - return self - - def get_value(self, key): - return self.key_by_value[key] - - def __repr__(self): - return 'Foo'.format(len(self.items), len(self.key_by_value)) - - -foo = Foo()\ - .add_item(1)\ - .add_items('abc')\ - .set_value('x', 1)\ - .set_value('x[]', [1, 2, 3]) - -print(foo) -print(foo.items) -print(foo.key_by_value) -print(foo.get_value('x')) -print(foo.get_value('x[]')) diff --git a/CONTACT__examples/config.py b/CONTACT__examples/config.py index 6b2320374..d7d312554 100644 --- a/CONTACT__examples/config.py +++ b/CONTACT__examples/config.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -URL_NG_SERVER = '' +URL_NG_SERVER = "" # Идентификатор программного обеспечения интегратора -INT_SOFT_ID = '' +INT_SOFT_ID = "" # Код точки обслуживания -POINT_CODE = '' +POINT_CODE = "" diff --git a/CONTACT__examples/connect_to_ng_server_via_socket_.py b/CONTACT__examples/connect_to_ng_server_via_socket_.py index b7392a73f..eb4407d9d 100644 --- a/CONTACT__examples/connect_to_ng_server_via_socket_.py +++ b/CONTACT__examples/connect_to_ng_server_via_socket_.py @@ -1,20 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import socket from config import * + # URL_NG_SERVER = 'http://10.7.8.31:12000' -HOST = URL_NG_SERVER.replace('https://', '').replace('http://', '') +HOST = URL_NG_SERVER.replace("https://", "").replace("http://", "") # HOST = 10.7.8.31, PORT = 12000 -HOST, PORT = HOST.split(':') +HOST, PORT = HOST.split(":") PORT = int(PORT) -post_data = """ +post_data = f""" -""".format(INT_SOFT_ID=INT_SOFT_ID, POINT_CODE=POINT_CODE) +""" http_request = ( - 'POST / HTTP/1.1\r\n', - 'Host: {host}:{port}\r\n', - 'Accept-Encoding: gzip, deflate\r\n', - 'User-Agent: {user_agent}\r\n', - 'Connection: close\r\n', - 'Accept: */*\r\n', - 'Content-Length: {content_length}\r\n', - '\r\n', - '{body}' + "POST / HTTP/1.1\r\n", + "Host: {host}:{port}\r\n", + "Accept-Encoding: gzip, deflate\r\n", + "User-Agent: {user_agent}\r\n", + "Connection: close\r\n", + "Accept: */*\r\n", + "Content-Length: {content_length}\r\n", + "\r\n", + "{body}", ) -http_request = ''.join(http_request) +http_request = "".join(http_request) http_request = http_request.format( host=HOST, port=PORT, - user_agent='iHuman', + user_agent="iHuman", content_length=len(post_data), body=post_data, ) @@ -48,13 +50,12 @@ print(repr(http_request)) -import socket sock = socket.socket() sock.connect((HOST, PORT)) sock.send(http_request.encode()) -print('Socket name: {}'.format(sock.getsockname())) -print('\nResponse:') +print(f"Socket name: {sock.getsockname()}") +print("\nResponse:") while True: data = sock.recv(1024) diff --git a/CONTACT__examples/create_and_fill_database_from_dictionary.py b/CONTACT__examples/create_and_fill_database_from_dictionary.py index 2936bc3c3..4537d3050 100644 --- a/CONTACT__examples/create_and_fill_database_from_dictionary.py +++ b/CONTACT__examples/create_and_fill_database_from_dictionary.py @@ -1,7 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import glob +import sqlite3 +import os + +from bs4 import BeautifulSoup def build_sql_table(table_name: str, format_fields_of_table: [dict]) -> str: @@ -51,25 +58,29 @@ def build_sql_table(table_name: str, format_fields_of_table: [dict]) -> str: """ - def build_field_table(format_field: dict, indent=' ' * 4) -> str: - field_name = format_field['attrname'] + def build_field_table(format_field: dict, indent=" " * 4) -> str: + field_name = format_field["attrname"] - field_type = 'TEXT' - if format_field['fieldtype'] == 'i4': - field_type = 'INTEGER' + field_type = "TEXT" + if format_field["fieldtype"] == "i4": + field_type = "INTEGER" - if field_name.upper() == 'ID': - field_type += ' PRIMARY KEY' + if field_name.upper() == "ID": + field_type += " PRIMARY KEY" - return indent + '{} {}'.format(field_name, field_type) + return f"{indent}{field_name} {field_type}" - fields_list = [build_field_table(format_field) for format_field in format_fields_of_table] + fields_list = [ + build_field_table(format_field) for format_field in format_fields_of_table + ] table = """\ CREATE TABLE IF NOT EXISTS {table_name} ( {fields_list} ); -""".format(table_name=table_name.upper(), fields_list=',\n'.join(fields_list)) +""".format( + table_name=table_name.upper(), fields_list=",\n".join(fields_list) + ) return table @@ -89,26 +100,32 @@ def build_insert(row_of_table: dict) -> str: keys = sorted(key for key in row_of_table.keys()) # return "INSERT OR REPLACE INTO {table_name} ({fields}) VALUES ({values});".format( - return "INSERT OR IGNORE INTO {table_name} ({fields}) VALUES ({values});".format( - table_name=table_name.upper(), - fields=','.join(key.upper() for key in keys), - values=','.join(repr(row_of_table[key]).replace('\\"', '').replace("\\'", '') for key in keys), + return ( + "INSERT OR IGNORE INTO {table_name} ({fields}) VALUES ({values});".format( + table_name=table_name.upper(), + fields=",".join(key.upper() for key in keys), + values=",".join( + repr(row_of_table[key]).replace('\\"', "").replace("\\'", "") + for key in keys + ), + ) ) - return '\n'.join(build_insert(row_data.attrs) for row_data in rows_of_table) + return "\n".join(build_insert(row_data.attrs) for row_data in rows_of_table) def create_connect(): - import sqlite3 - return sqlite3.connect('database.sqlite') + return sqlite3.connect("database.sqlite") -def create_table(table_name: str, sql_table: str, sql_table_data_rows: str, drop_table=False): +def create_table( + table_name: str, sql_table: str, sql_table_data_rows: str, drop_table=False +) -> None: # Создание таблицы connect = create_connect() try: if drop_table: - connect.execute('DROP TABLE IF EXISTS ?;', (table_name,)) + connect.execute("DROP TABLE IF EXISTS ?;", (table_name,)) connect.execute(sql_table) connect.executescript(sql_table_data_rows) @@ -119,25 +136,24 @@ def create_table(table_name: str, sql_table: str, sql_table_data_rows: str, drop connect.close() -if __name__ == '__main__': - import glob - for file_name in glob.glob('contact_dicts/*.xml'): - import os +if __name__ == "__main__": + for file_name in glob.glob("contact_dicts/*.xml"): table_name = os.path.splitext(os.path.basename(file_name))[0] - print('Append {} from {}'.format(table_name, file_name)) + print(f"Append {table_name} from {file_name}") - from bs4 import BeautifulSoup - root = BeautifulSoup(open(file_name, 'rb'), 'lxml') + root = BeautifulSoup(open(file_name, "rb"), "lxml") - format_fields_of_dict = [row for row in root.select('metadata > fields > field')] - rows_of_dict = [row for row in root.select('rowdata > row')] + format_fields_of_dict = [ + row for row in root.select("metadata > fields > field") + ] + rows_of_dict = [row for row in root.select("rowdata > row")] sql_table = build_sql_table(table_name, format_fields_of_dict) sql_table_data_rows = build_sql_rows_data_table(table_name, rows_of_dict) # print(sql_table + "\n\n" + sql_table_data_rows) - print(' Append {} rows\n'.format(len(rows_of_dict))) + print(f" Append {len(rows_of_dict)} rows\n") create_table(table_name, sql_table, sql_table_data_rows) # print() diff --git a/CONTACT__examples/create_full_dict__GET_CHANGES_from_exported_dicts.py b/CONTACT__examples/create_full_dict__GET_CHANGES_from_exported_dicts.py index d70931aa5..97cab3430 100644 --- a/CONTACT__examples/create_full_dict__GET_CHANGES_from_exported_dicts.py +++ b/CONTACT__examples/create_full_dict__GET_CHANGES_from_exported_dicts.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -10,6 +10,11 @@ """ +import base64 +import glob +import os +import zlib + def bytes_to_compress_to_base64(bytes_text: bytes) -> str: """ @@ -17,17 +22,14 @@ def bytes_to_compress_to_base64(bytes_text: bytes) -> str: """ - import zlib compress_bytes_xml = zlib.compress(bytes_text) - - import base64 base64_compress = base64.b64encode(compress_bytes_xml) - return base64_compress.decode('utf-8') + return base64_compress.decode("utf-8") -if __name__ == '__main__': - DIR_DICT_CONTACT = 'mini_full_dict__CONTACT' - FILE_NAME_FULL_DICT = 'new_mini_full_dict__CONTACT.xml' +if __name__ == "__main__": + DIR_DICT_CONTACT = "mini_full_dict__CONTACT" + FILE_NAME_FULL_DICT = "new_mini_full_dict__CONTACT.xml" HTML_PATTERN_RESPONSE__GET_CHANGES = """\ str: child_list = list() - import glob - for file_name in glob.glob(DIR_DICT_CONTACT + '/*.xml'): + for file_name in glob.glob(DIR_DICT_CONTACT + "/*.xml"): print(file_name) - import os dict_name = os.path.splitext(os.path.basename(file_name))[0].upper() - bytes_xml = open(file_name, 'rb').read() + bytes_xml = open(file_name, "rb").read() base64_str = bytes_to_compress_to_base64(bytes_xml) - child_list.append("<{0}>{1}".format(dict_name, base64_str)) + child_list.append(f"<{dict_name}>{base64_str}") - with open(FILE_NAME_FULL_DICT, 'w', encoding='utf-8') as f: - f.write(HTML_PATTERN_RESPONSE__GET_CHANGES.format(''.join(child_list))) + with open(FILE_NAME_FULL_DICT, "w", encoding="utf-8") as f: + f.write(HTML_PATTERN_RESPONSE__GET_CHANGES.format("".join(child_list))) print() - print('Write to {}'.format(FILE_NAME_FULL_DICT)) + print(f"Write to {FILE_NAME_FULL_DICT}") diff --git a/CONTACT__examples/export_dicts_from__full_dict__GET_CHANGES.py b/CONTACT__examples/export_dicts_from__full_dict__GET_CHANGES.py index a49cf7acb..7167ee2f7 100644 --- a/CONTACT__examples/export_dicts_from__full_dict__GET_CHANGES.py +++ b/CONTACT__examples/export_dicts_from__full_dict__GET_CHANGES.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import base64 import zlib import sys +import os from typing import Union @@ -20,7 +21,9 @@ def get_bytes_from_base64_zlib(text_or_tag_or_bytes: Union[str, bytes, Tag]) -> """ - if not isinstance(text_or_tag_or_bytes, str) and not isinstance(text_or_tag_or_bytes, bytes): + if not isinstance(text_or_tag_or_bytes, str) and not isinstance( + text_or_tag_or_bytes, bytes + ): text = text_or_tag_or_bytes.text else: text = text_or_tag_or_bytes @@ -29,24 +32,23 @@ def get_bytes_from_base64_zlib(text_or_tag_or_bytes: Union[str, bytes, Tag]) -> return zlib.decompress(compress_data) -if __name__ == '__main__': - file_name_full_dict = 'mini_full_dict__CONTACT.xml' - export_dir = 'mini_full_dict__CONTACT' +if __name__ == "__main__": + file_name_full_dict = "mini_full_dict__CONTACT.xml" + export_dir = "mini_full_dict__CONTACT" - import os os.makedirs(export_dir, exist_ok=True) # Parsing - root = BeautifulSoup(open(file_name_full_dict, 'rb'), 'lxml') - response = root.select_one('response') + root = BeautifulSoup(open(file_name_full_dict, "rb"), "lxml") + response = root.select_one("response") # Если ошибка - if response['re'] != '0': - print('Error text: "{}"'.format(response['err_text'])) + if response["re"] != "0": + print(f'Error text: "{response["err_text"]}"') sys.exit() - print('Справочник полный?:', response['full'] == '1') - print('Версия справочника:', response['version']) + print("Справочник полный?:", response["full"] == "1") + print("Версия справочника:", response["version"]) print() for child in response.children: @@ -54,9 +56,9 @@ def get_bytes_from_base64_zlib(text_or_tag_or_bytes: Union[str, bytes, Tag]) -> if child.name is None: continue - file_name = os.path.join(export_dir, child.name) + '.xml' + file_name = os.path.join(export_dir, child.name) + ".xml" print(file_name) - with open(file_name, 'w', encoding='cp1251') as f: + with open(file_name, "w", encoding="cp1251") as f: data = get_bytes_from_base64_zlib(child) - f.write(data.decode(encoding='cp1251')) + f.write(data.decode(encoding="cp1251")) diff --git a/CONTACT__examples/export_logo_from_dict.py b/CONTACT__examples/export_logo_from_dict.py index e1b4112fc..99830a0ff 100644 --- a/CONTACT__examples/export_logo_from_dict.py +++ b/CONTACT__examples/export_logo_from_dict.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -9,26 +9,27 @@ """ +import base64 +import os -if __name__ == '__main__': - FILE_NAME_DICT_LOGO = 'mini_full_dict__CONTACT/logo.xml' - DIR_LOGO_IMAGES = 'logo_images' +from bs4 import BeautifulSoup - import os - os.makedirs(DIR_LOGO_IMAGES, exist_ok=True) - from bs4 import BeautifulSoup - root = BeautifulSoup(open(FILE_NAME_DICT_LOGO, 'rb'), 'lxml') +FILE_NAME_DICT_LOGO = "mini_full_dict__CONTACT/logo.xml" - for row in root.select('rowdata > row'): - logo_name = row['logo_name'] - print(logo_name) +DIR_LOGO_IMAGES = "logo_images" +os.makedirs(DIR_LOGO_IMAGES, exist_ok=True) - logo_data = row['logo_data'] - import base64 - img_data = base64.b64decode(logo_data) +root = BeautifulSoup(open(FILE_NAME_DICT_LOGO, "rb"), "lxml") - file_name = os.path.join(DIR_LOGO_IMAGES, logo_name) +for row in root.select("rowdata > row"): + logo_name = row["logo_name"] + print(logo_name) - with open(file_name, 'wb') as f: - f.write(img_data) + logo_data = row["logo_data"] + img_data = base64.b64decode(logo_data) + + file_name = os.path.join(DIR_LOGO_IMAGES, logo_name) + + with open(file_name, "wb") as f: + f.write(img_data) diff --git a/CONTACT__examples/get_full_dictionary__method_GET_CHANGES.py b/CONTACT__examples/get_full_dictionary__method_GET_CHANGES.py index 66ebc276b..eb41f8024 100644 --- a/CONTACT__examples/get_full_dictionary__method_GET_CHANGES.py +++ b/CONTACT__examples/get_full_dictionary__method_GET_CHANGES.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -12,12 +12,14 @@ from config import * +import requests -FILE_NAME_FULL_DICT = 'full_dict__CONTACT.xml' +FILE_NAME_FULL_DICT = "full_dict__CONTACT.xml" -if __name__ == '__main__': - post_data = """ + +if __name__ == "__main__": + post_data = f""" - """.format(INT_SOFT_ID=INT_SOFT_ID, POINT_CODE=POINT_CODE) + """ - import requests rs = requests.post(URL_NG_SERVER, data=post_data) print(rs) print(len(rs.text)) - with open(FILE_NAME_FULL_DICT, 'wb') as f: + with open(FILE_NAME_FULL_DICT, "wb") as f: f.write(rs.content) - print('Write to: {}'.format(FILE_NAME_FULL_DICT)) + print(f"Write to: {FILE_NAME_FULL_DICT}") diff --git a/CONTACT__examples/reducing number row in rows dicts.py b/CONTACT__examples/reducing number row in rows dicts.py index 29234d602..8b83ff852 100644 --- a/CONTACT__examples/reducing number row in rows dicts.py +++ b/CONTACT__examples/reducing number row in rows dicts.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -12,30 +12,31 @@ """ -if __name__ == '__main__': - # NOTE: справочники CONTACT хранятся в кодировке cp1251 (windows-1251) +import glob +import os - import glob - for file_name in glob.glob('mini_contact_dicts/*.xml'): - print(file_name) +from bs4 import BeautifulSoup - from bs4 import BeautifulSoup - root = BeautifulSoup(open(file_name, 'rb'), 'lxml', from_encoding='cp1251') - rows = root.select('row') - old_len = len(rows) +# NOTE: справочники CONTACT хранятся в кодировке cp1251 (windows-1251) - for row in rows[20:]: - row.decompose() +for file_name in glob.glob("mini_contact_dicts/*.xml"): + print(file_name) - rows = root.select('row') - print(' len {} -> {}'.format(old_len, len(rows))) + root = BeautifulSoup(open(file_name, "rb"), "lxml", from_encoding="cp1251") - import os - file_name = 'mini_contact_dicts/' + os.path.basename(file_name) + rows = root.select("row") + old_len = len(rows) - with open(file_name, 'w', encoding='cp1251') as f: - f.write(str(root)) + for row in rows[20:]: + row.decompose() - print(' Write to {}'.format(file_name)) - print() + rows = root.select("row") + print(f" len {old_len} -> {len(rows)}") + + file_name = "mini_contact_dicts/" + os.path.basename(file_name) + with open(file_name, "w", encoding="cp1251") as f: + f.write(str(root)) + + print(f" Write to {file_name}") + print() diff --git a/Callable modules/main.py b/Callable modules/main.py index 4f5582299..107b5729d 100644 --- a/Callable modules/main.py +++ b/Callable modules/main.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import print_this -print_this('1232') + + +print_this("1232") print_this(32) diff --git a/Callable modules/print_this.py b/Callable modules/print_this.py index 239db0df5..6f8f95e67 100644 --- a/Callable modules/print_this.py +++ b/Callable modules/print_this.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import sys # SOURCE: https://stackoverflow.com/a/1060872/5909792 class mod_call(object): - def __call__(self, text): + def __call__(self, text) -> None: print(text) -import sys sys.modules[__name__] = mod_call() diff --git a/CamelCase_to_snake_case.py b/CamelCase_to_snake_case.py index cdae0607c..2ac5445ea 100644 --- a/CamelCase_to_snake_case.py +++ b/CamelCase_to_snake_case.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re @@ -9,15 +9,15 @@ # SOURCE: https://stackoverflow.com/a/1176023/5909792 def convert(text: str) -> str: - s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text) - return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", text) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() -if __name__ == '__main__': - assert convert('CamelCase') == 'camel_case' - assert convert('CamelCamelCase') == 'camel_camel_case' - assert convert('Camel2Camel2Case') == 'camel2_camel2_case' - assert convert('getHTTPResponseCode') == 'get_http_response_code' - assert convert('get2HTTPResponseCode') == 'get2_http_response_code' - assert convert('HTTPResponseCode') == 'http_response_code' - assert convert('HTTPResponseCodeXYZ') == 'http_response_code_xyz' +if __name__ == "__main__": + assert convert("CamelCase") == "camel_case" + assert convert("CamelCamelCase") == "camel_camel_case" + assert convert("Camel2Camel2Case") == "camel2_camel2_case" + assert convert("getHTTPResponseCode") == "get_http_response_code" + assert convert("get2HTTPResponseCode") == "get2_http_response_code" + assert convert("HTTPResponseCode") == "http_response_code" + assert convert("HTTPResponseCodeXYZ") == "http_response_code_xyz" diff --git a/CreateNoteXmlNoteManagers/CreateNoteXmlNoteManagers.py b/CreateNoteXmlNoteManagers/CreateNoteXmlNoteManagers.py index f2931b079..cb60a4485 100644 --- a/CreateNoteXmlNoteManagers/CreateNoteXmlNoteManagers.py +++ b/CreateNoteXmlNoteManagers/CreateNoteXmlNoteManagers.py @@ -1,37 +1,48 @@ # coding=utf-8 -__author__ = 'ipetrash' +__author__ = "ipetrash" + import argparse import sys import os -def main(namespace): +def main(namespace) -> None: # @param namespace argparse.Namespace Содержит переданные в аргументах объекты. - indent = ' ' * 8 + indent = " " * 8 - dirs = filter(lambda x: os.path.isdir(os.path.join(namespace.dir, x)), os.listdir(namespace.dir)) + dirs = filter( + lambda x: os.path.isdir(os.path.join(namespace.dir, x)), + os.listdir(namespace.dir), + ) for dir in dirs: - print(indent + """""" % dir) - - -def create_parser(): - parser = argparse.ArgumentParser(prog="CreateNoteXml", - description=u"Cкрипт генерирует xml-файл программы NotesManager.", - epilog=u"(с) Petrash Ilya 2014. Автор: Илья Петраш.") - parser.add_argument('-dir', - help=u'Путь к директории notes.', - type=str) + print( + indent + + """""" % dir + ) + + +def create_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="CreateNoteXml", + description="Cкрипт генерирует xml-файл программы NotesManager.", + epilog="(с) Petrash Ilya 2014. Автор: Илья Петраш.", + ) + parser.add_argument("-dir", help="Путь к директории notes.", type=str) return parser -if __name__ == '__main__': +if __name__ == "__main__": parser = create_parser() - sys.argv = [sys.argv[0], r'-dir=C:\Users\ipetrash\Desktop\NotesManager.v0.0.3.Windows\notes', ] + # TODO: Remove + # sys.argv = [ + # sys.argv[0], + # r"-dir=C:\Users\ipetrash\Desktop\NotesManager.v0.0.3.Windows\notes", + # ] if len(sys.argv) == 1: parser.print_help() else: namespace = parser.parse_args() - main(namespace) \ No newline at end of file + main(namespace) diff --git a/Crypto Git Repository/api.py b/Crypto Git Repository/api.py index 404062aaf..bbd03226b 100644 --- a/Crypto Git Repository/api.py +++ b/Crypto Git Repository/api.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +# pip install GitPython +import git + +from config import REPO_PATH, URL_GIT repo = None def __get_repo(): - # pip install GitPython - import git - from config import REPO_PATH, URL_GIT - try: return git.Repo(REPO_PATH) @@ -31,9 +33,9 @@ def get_repo(): repo = get_repo() -def print_log(reverse=False): - logs = repo.git.log('--pretty=format:%H%x09%an%x09%ad%x09%s').splitlines() - print('Logs[{}]:'.format(len(logs))) +def print_log(reverse=False) -> None: + logs = repo.git.log("--pretty=format:%H%x09%an%x09%ad%x09%s").splitlines() + print(f"Logs[{len(logs)}]:") if reverse: logs.reverse() @@ -42,28 +44,27 @@ def print_log(reverse=False): print(log) -def append(file_or_list): +def append(file_or_list) -> None: if type(file_or_list) == str: file_or_list = [file_or_list] repo.index.add(file_or_list) -def remove(file_or_list): +def remove(file_or_list) -> None: if type(file_or_list) == str: file_or_list = [file_or_list] repo.index.remove(file_or_list) -def commit(message): +def commit(message) -> None: repo.index.commit(message) -def pull(): +def pull() -> None: repo.remotes.origin.pull() -def push(): +def push() -> None: repo.remotes.origin.push() - diff --git a/Crypto Git Repository/append_to_remote_repo.py b/Crypto Git Repository/append_to_remote_repo.py index afff168a0..6cfcc0d41 100644 --- a/Crypto Git Repository/append_to_remote_repo.py +++ b/Crypto Git Repository/append_to_remote_repo.py @@ -1,27 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import random +import string +import os +import uuid def create_random_file(repo): - import uuid file_name = str(uuid.uuid4()) - - import os full_file_name = os.path.join(repo.working_tree_dir, file_name) - with open(full_file_name, 'w') as f: - import random - import string - text = "".join(random.choice(string.ascii_letters + string.digits) for _ in range(64)) + with open(full_file_name, "w") as f: + text = "".join( + random.choice(string.ascii_letters + string.digits) + for _ in range(64) + ) f.write(text) return file_name, file_name -if __name__ == '__main__': +if __name__ == "__main__": import api + repo = api.repo print(repo) print() @@ -30,7 +35,7 @@ def create_random_file(repo): print() file_name = create_random_file(repo)[1] - message = 'Create: ' + file_name + message = "Create: " + file_name print(message) api.append(file_name) diff --git a/Crypto Git Repository/config.py b/Crypto Git Repository/config.py index a48ee6d4f..392ec7dac 100644 --- a/Crypto Git Repository/config.py +++ b/Crypto Git Repository/config.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import os LOGIN = None @@ -10,16 +13,13 @@ # http://user:password@proxy_host:proxy_port PROXY = None -NEW_REPO = 'Test-Repo' - -import os +NEW_REPO = "Test-Repo" REPO_PATH = os.path.abspath(NEW_REPO) # How use without input login and password: # git clone https://username:password@github.com/username/repository.git -URL_GIT = 'https://{0}:{1}@github.com/{0}/{2}.git'.format(LOGIN, PASSWORD, NEW_REPO) +URL_GIT = f"https://{LOGIN}:{PASSWORD}@github.com/{LOGIN}/{NEW_REPO}.git" if PROXY: - import os - os.environ['http_proxy'] = PROXY + os.environ["http_proxy"] = PROXY diff --git a/Crypto Git Repository/create_local_repo.py b/Crypto Git Repository/create_local_repo.py index c280d66f4..d936ae89c 100644 --- a/Crypto Git Repository/create_local_repo.py +++ b/Crypto Git Repository/create_local_repo.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - import api - repo = api.repo +import api - print('Repo:', repo) - print() - api.print_log() +repo = api.repo + +print("Repo:", repo) +print() + +api.print_log() diff --git a/Crypto Git Repository/delete_random_file__from__remote_repo.py b/Crypto Git Repository/delete_random_file__from__remote_repo.py index 1f5d731f9..a9edbd6f4 100644 --- a/Crypto Git Repository/delete_random_file__from__remote_repo.py +++ b/Crypto Git Repository/delete_random_file__from__remote_repo.py @@ -1,31 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import os +import random def get_random_file(repo): - file_names = list() + file_names = [] - import os for root, dirs, files in os.walk(repo.working_tree_dir): - if '.git' in root: + if ".git" in root: continue for file in files: file_names.append(os.path.join(root, file)) if file_names: - import random return random.choice(file_names) - else: - return None + return # NOTE: first execute append_to_remote_repo.py -if __name__ == '__main__': +if __name__ == "__main__": import api + repo = api.repo print(repo) print() @@ -35,9 +37,8 @@ def get_random_file(repo): abs_file_name = get_random_file(repo) if abs_file_name: - import os file_name = os.path.basename(abs_file_name) - message = 'Remove: ' + file_name + message = "Remove: " + file_name print(message) api.remove(file_name) diff --git a/Crypto Git Repository/print_repo_log.py b/Crypto Git Repository/print_repo_log.py index db556a910..ca967ec24 100644 --- a/Crypto Git Repository/print_repo_log.py +++ b/Crypto Git Repository/print_repo_log.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - import api - api.print_log(reverse=True) +import api + + +api.print_log(reverse=True) diff --git a/Crypto Git Repository/update_from_remote_repository__pull.py b/Crypto Git Repository/update_from_remote_repository__pull.py index cf97a3538..fb33fb143 100644 --- a/Crypto Git Repository/update_from_remote_repository__pull.py +++ b/Crypto Git Repository/update_from_remote_repository__pull.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - import api - api.pull() +import api + + +api.pull() diff --git a/DNS price tracking/common.py b/DNS price tracking/common.py index d42424e29..0bbf9589c 100644 --- a/DNS price tracking/common.py +++ b/DNS price tracking/common.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import json @@ -9,4 +9,4 @@ def get_tracked_products() -> List[dict]: - return json.load(open('tracked_products.json', encoding='utf-8')) + return json.load(open("tracked_products.json", encoding="utf-8")) diff --git a/DNS price tracking/db.py b/DNS price tracking/db.py index c322c9177..91a0635fe 100644 --- a/DNS price tracking/db.py +++ b/DNS price tracking/db.py @@ -1,33 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt +import pathlib +import os +import shutil from peewee import * -# Absolute file name -import pathlib -DB_FILE_NAME = str(pathlib.Path(__file__).resolve().parent / 'tracked_products.sqlite') +# Absolute file name +DB_FILE_NAME = str(pathlib.Path(__file__).resolve().parent / "tracked_products.sqlite") -def db_create_backup(backup_dir='backup'): - import datetime as DT - import os - import shutil +def db_create_backup(backup_dir="backup") -> None: os.makedirs(backup_dir, exist_ok=True) - file_name = str(DT.datetime.today().date()) + '.sqlite' + file_name = str(dt.datetime.today().date()) + ".sqlite" file_name = os.path.join(backup_dir, file_name) shutil.copy(DB_FILE_NAME, file_name) # Ensure foreign-key constraints are enforced. -db = SqliteDatabase(DB_FILE_NAME, pragmas={'foreign_keys': 1}) +db = SqliteDatabase(DB_FILE_NAME, pragmas={"foreign_keys": 1}) class BaseModel(Model): @@ -40,7 +39,7 @@ class Product(BaseModel): url = TextField(unique=True) def get_technopoint_url(self) -> str: - return self.url.replace('www.dns-shop.ru', 'technopoint.ru') + return self.url.replace("www.dns-shop.ru", "technopoint.ru") def get_last_price_dns(self, actual_price=True): prices = self.prices @@ -68,43 +67,47 @@ def get_last_price_technopoint(self, actual_price=True): return last_price.value_technopoint - def append_price(self, value_dns, value_technopoint): - Price.create(product=self, value_dns=value_dns, value_technopoint=value_technopoint) + def append_price(self, value_dns, value_technopoint) -> None: + Price.create( + product=self, value_dns=value_dns, value_technopoint=value_technopoint + ) - def __str__(self): + def __str__(self) -> str: # TODO: выводить последнюю цену и актуальную - return f'Product(title={self.title!r}, ' \ - f'last_price_dns={self.get_last_price_dns()}, ' \ - f'last_price_technopoint={self.get_last_price_technopoint()}, ' \ - f'url={self.url!r})' + return ( + f"Product(title={self.title!r}, " + f"last_price_dns={self.get_last_price_dns()}, " + f"last_price_technopoint={self.get_last_price_technopoint()}, " + f"url={self.url!r})" + ) class Price(BaseModel): value_dns = DecimalField(null=True) value_technopoint = DecimalField(null=True) - date = DateTimeField(default=DT.datetime.now) - product = ForeignKeyField(Product, backref='prices') + date = DateTimeField(default=dt.datetime.now) + product = ForeignKeyField(Product, backref="prices") class Meta: - indexes = ( - (("product_id", "date", "value_dns", "value_technopoint"), True), - ) + indexes = ((("product_id", "date", "value_dns", "value_technopoint"), True),) - def __str__(self): - return f'Price(value_dns={self.value_dns}, ' \ - f'value_technopoint={self.value_technopoint}, ' \ - f'date={self.date}, product_id={self.product.id})' + def __str__(self) -> str: + return ( + f"Price(value_dns={self.value_dns}, " + f"value_technopoint={self.value_technopoint}, " + f"date={self.date}, product_id={self.product.id})" + ) db.connect() db.create_tables([Product, Price]) -if __name__ == '__main__': +if __name__ == "__main__": for product in Product.select(): print(product) for p in product.prices.select(): - print(' ', p) + print(" ", p) print() diff --git a/DNS price tracking/main.py b/DNS price tracking/main.py index 57ca6e296..c2eae3c79 100644 --- a/DNS price tracking/main.py +++ b/DNS price tracking/main.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT import time import traceback - -# Import https://github.com/gil9red/SimplePyScripts/blob/8fa9b9c23d10b5ee7ff0161da997b463f7a861bf/wait/wait.py import sys -sys.path.append('../wait') -sys.path.append('../html_parsing/www_dns_shop_ru') -from wait import wait +from datetime import datetime + +# pip install simple-wait +from simple_wait import wait + +sys.path.append("../html_parsing/www_dns_shop_ru") from get_price import get_price from common import get_tracked_products @@ -24,7 +24,7 @@ while True: - print(f'Started at {DT.datetime.now():%d/%m/%Y %H:%M:%S}\n') + print(f"Started at {datetime.now():%d/%m/%Y %H:%M:%S}\n") db_create_backup() @@ -33,19 +33,25 @@ try: for product_data in get_tracked_products(): if product_data in checked_products: - print(f"Duplicate: {repr(product_data['title'])}, url: {product_data['url']}\n") + print( + f"Duplicate: {repr(product_data['title'])}, url: {product_data['url']}\n" + ) continue - product, _ = Product.get_or_create(title=product_data['title'], url=product_data['url']) + product, _ = Product.get_or_create( + title=product_data["title"], url=product_data["url"] + ) print(product) last_price_dns = product.get_last_price_dns() last_price_technopoint = product.get_last_price_technopoint() - print(f'Last DNS: {last_price_dns}, Technopoint: {last_price_technopoint}') + print(f"Last DNS: {last_price_dns}, Technopoint: {last_price_technopoint}") current_url_price_dns = get_price(product.url) current_url_price_tp = get_price(product.get_technopoint_url()) - print(f'Current url price: DNS={current_url_price_dns}, Technopoint={current_url_price_tp}') + print( + f"Current url price: DNS={current_url_price_dns}, Technopoint={current_url_price_tp}" + ) is_change_dns = current_url_price_dns and current_url_price_dns != last_price_dns is_change_technopoint = current_url_price_tp and current_url_price_tp != last_price_technopoint @@ -53,17 +59,19 @@ # Добавляем новую цену, если цена отличается или у продукта еще нет цен if is_change_dns or is_change_technopoint or is_first_price: - text = f'Append new price: DNS={current_url_price_dns}, Technopoint={current_url_price_tp}.' \ - f' Reason: ' + text = ( + f"Append new price: DNS={current_url_price_dns}, Technopoint={current_url_price_tp}." + f" Reason: " + ) if is_first_price: - text += 'First price' + text += "First price" else: if is_change_dns and is_change_technopoint: - text += 'DNS and Technopoint' + text += "DNS and Technopoint" elif is_change_dns: - text += 'DNS' + text += "DNS" else: - text += 'Technopoint' + text += "Technopoint" print(text) product.append_price(current_url_price_dns, current_url_price_tp) @@ -81,7 +89,7 @@ tb = traceback.format_exc() print(tb) - print('Wait 15 minutes') - time.sleep(15 * 60) # 15 minutes + print("Wait 15 minutes") + wait(minutes=15) print() diff --git a/DNS price tracking/migrations/001_auto.py b/DNS price tracking/migrations/001_auto.py index fc9ef4731..a7eddd304 100644 --- a/DNS price tracking/migrations/001_auto.py +++ b/DNS price tracking/migrations/001_auto.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#schema-migrations @@ -11,7 +11,7 @@ # TODO: имя в конфиг -DB_NAME = '../tracked_products.sqlite' +DB_NAME = "../tracked_products.sqlite" my_db = SqliteDatabase(DB_NAME) @@ -20,8 +20,12 @@ with my_db.atomic(): migrate( - migrator.rename_column('price', 'value', 'value_dns'), - migrator.add_column('price', 'value_technopoint', DecimalField(null=True)), - migrator.drop_index('price', 'price_product_id_date_value'), - migrator.add_index('price', ('product_id', 'date', 'value_dns', 'value_technopoint'), unique=True), + migrator.rename_column("price", "value", "value_dns"), + migrator.add_column("price", "value_technopoint", DecimalField(null=True)), + migrator.drop_index("price", "price_product_id_date_value"), + migrator.add_index( + "price", + ("product_id", "date", "value_dns", "value_technopoint"), + unique=True, + ), ) diff --git a/DNS price tracking/web.py b/DNS price tracking/web.py index ca24080d4..d38570718 100644 --- a/DNS price tracking/web.py +++ b/DNS price tracking/web.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging @@ -49,23 +49,21 @@ def index(): "price_techopoint": get_float(p.value_technopoint), }) - return render_template('index.html', products=products, prices=prices) + return render_template("index.html", products=products, prices=prices) -@app.route('/favicon.ico') +@app.route("/favicon.ico") def favicon(): return send_from_directory( - os.path.join(app.root_path, 'static/img'), - 'favicon.png' + os.path.join(app.root_path, "static/img"), + "favicon.png", ) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True - app.run( - port=10010 - ) + app.run(port=10010) # # Public IP # app.run(host='0.0.0.0') diff --git "a/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/simple_distance_Levenshtein/fix_word.py" "b/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/simple_distance_Levenshtein/fix_word.py" index 4c3dec194..a0bf9d9e9 100644 --- "a/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/simple_distance_Levenshtein/fix_word.py" +++ "b/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/simple_distance_Levenshtein/fix_word.py" @@ -1,11 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def fix_word(word, need_word): - from simple_distance import distance +from simple_distance import distance + + +def fix_word(word: str, need_word: str) -> str | None: number = distance(word, need_word) # Считаем что разница в 2 символа и меньше еще нормальная @@ -15,13 +17,13 @@ def fix_word(word, need_word): return need_word -if __name__ == '__main__': - need_word = 'Привет' +if __name__ == "__main__": + need_word = "Привет" - print(fix_word('Привет', need_word)) # Привет - print(fix_word('Првет', need_word)) # Привет - print(fix_word('Прывет', need_word)) # Привет - print(fix_word('Привед', need_word)) # Привет - print(fix_word('Превед', need_word)) # Привет + print(fix_word("Привет", need_word)) # Привет + print(fix_word("Првет", need_word)) # Привет + print(fix_word("Прывет", need_word)) # Привет + print(fix_word("Привед", need_word)) # Привет + print(fix_word("Превед", need_word)) # Привет - print(fix_word('Преед', need_word)) # None + print(fix_word("Преед", need_word)) # None diff --git "a/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/simple_distance_Levenshtein/match_two_words.py" "b/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/simple_distance_Levenshtein/match_two_words.py" index 8270a7cd4..505a3db5c 100644 --- "a/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/simple_distance_Levenshtein/match_two_words.py" +++ "b/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/simple_distance_Levenshtein/match_two_words.py" @@ -1,24 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def match_two_words(word_1, word_2): - from simple_distance import distance +from simple_distance import distance + + +def match_two_words(word_1: str, word_2: str) -> bool: number = distance(word_1, word_2) # Считаем что разница в 2 символа и меньше еще нормальная return number < 3 -if __name__ == '__main__': - need_word = 'Привет' +if __name__ == "__main__": + need_word = "Привет" - print(match_two_words('Привет', need_word)) # True - print(match_two_words('Првет', need_word)) # True - print(match_two_words('Прывет', need_word)) # True - print(match_two_words('Привед', need_word)) # True - print(match_two_words('Превед', need_word)) # True + print(match_two_words("Привет", need_word)) # True + print(match_two_words("Првет", need_word)) # True + print(match_two_words("Прывет", need_word)) # True + print(match_two_words("Привед", need_word)) # True + print(match_two_words("Превед", need_word)) # True - print(match_two_words('Преед', need_word)) # False + print(match_two_words("Преед", need_word)) # False diff --git "a/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/simple_distance_Levenshtein/simple_distance.py" "b/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/simple_distance_Levenshtein/simple_distance.py" index d083eeaa7..c91ea7c46 100644 --- "a/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/simple_distance_Levenshtein/simple_distance.py" +++ "b/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/simple_distance_Levenshtein/simple_distance.py" @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.wikibooks.org/wiki/Реализации_алгоритмов/Расстояние_Левенштейна#Python @@ -18,7 +18,11 @@ def distance(a, b): for i in range(1, m + 1): previous_row, current_row = current_row, [i] + [0] * n for j in range(1, n + 1): - add, delete, change = previous_row[j] + 1, current_row[j - 1] + 1, previous_row[j - 1] + add, delete, change = ( + previous_row[j] + 1, + current_row[j - 1] + 1, + previous_row[j - 1], + ) if a[j - 1] != b[i - 1]: change += 1 @@ -27,7 +31,7 @@ def distance(a, b): return current_row[n] -if __name__ == '__main__': - print(distance('Привет', 'Превет')) # 1 - print(distance('Привет', 'Првет')) # 1 - print(distance('Привет', 'Првед')) # 2 +if __name__ == "__main__": + print(distance("Привет", "Превет")) # 1 + print(distance("Привет", "Првет")) # 1 + print(distance("Привет", "Првед")) # 2 diff --git "a/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__difflib_SequenceMatcher.py" "b/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__difflib_SequenceMatcher.py" index 4cd58881c..7ce5f16bd 100644 --- "a/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__difflib_SequenceMatcher.py" +++ "b/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__difflib_SequenceMatcher.py" @@ -1,10 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import Optional, List from difflib import SequenceMatcher # pip install pymorphy2 @@ -14,17 +13,16 @@ morph = pymorphy2.MorphAnalyzer() -def get_tokens(text: str) -> List[pymorphy2.analyzer.Parse]: +def get_tokens(text: str) -> list[pymorphy2.analyzer.Parse]: return [morph.parse(word)[0] for word in simple_word_tokenize(text)] -ALL_WORDS = ['замена', 'заменить', 'касса'] +ALL_WORDS = ["замена", "заменить", "касса"] -def fix_command(word: str) -> Optional[str]: +def fix_command(word: str) -> str | None: rations = [ - (word2, SequenceMatcher(None, word, word2).ratio()) - for word2 in ALL_WORDS + (word2, SequenceMatcher(None, word, word2).ratio()) for word2 in ALL_WORDS ] rations = [(word, ratio) for word, ratio in rations if ratio >= 0.7] if not rations: @@ -34,9 +32,9 @@ def fix_command(word: str) -> Optional[str]: commands = [ - 'замена кассы', - 'заменить кассs', - 'замени кассу', + "замена кассы", + "заменить кассs", + "замени кассу", ] for command in commands: words = get_tokens(command) diff --git "a/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__pyxdameraulevenshtein/example.py" "b/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__pyxdameraulevenshtein/example.py" index 6fa86ca01..c375215fb 100644 --- "a/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__pyxdameraulevenshtein/example.py" +++ "b/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__pyxdameraulevenshtein/example.py" @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pyxDamerauLevenshtein @@ -9,62 +9,180 @@ # SOURCE: https://github.com/gfairchild/pyxDamerauLevenshtein/blob/master/examples/examples.py -from pyxdameraulevenshtein import damerau_levenshtein_distance, normalized_damerau_levenshtein_distance, damerau_levenshtein_distance_ndarray, normalized_damerau_levenshtein_distance_ndarray import random import string import timeit + import numpy as np +from pyxdameraulevenshtein import ( + damerau_levenshtein_distance, + normalized_damerau_levenshtein_distance, + damerau_levenshtein_distance_ndarray, + normalized_damerau_levenshtein_distance_ndarray, +) -print('# edit distances (low edit distance means words are more similar):') -print("damerau_levenshtein_distance('{}', '{}') = {}".format('smtih', 'smith', damerau_levenshtein_distance('smtih', 'smith'))) -print("damerau_levenshtein_distance('{}', '{}') = {}".format('snapple', 'apple', damerau_levenshtein_distance('snapple', 'apple'))) -print("damerau_levenshtein_distance('{}', '{}') = {}".format('testing', 'testtn', damerau_levenshtein_distance('testing', 'testtn'))) -print("damerau_levenshtein_distance('{}', '{}') = {}".format('saturday', 'sunday', damerau_levenshtein_distance('saturday', 'sunday'))) -print("damerau_levenshtein_distance('{}', '{}') = {}".format('Saturday', 'saturday', damerau_levenshtein_distance('Saturday', 'saturday'))) -print("damerau_levenshtein_distance('{}', '{}') = {}".format('orange', 'pumpkin', damerau_levenshtein_distance('orange', 'pumpkin'))) -print("damerau_levenshtein_distance('{}', '{}') = {}".format('gifts', 'profit', damerau_levenshtein_distance('gifts', 'profit'))) -print("damerau_levenshtein_distance('{}', '{}') = {} # unicode example\n".format('Sjöstedt', 'Sjostedt', damerau_levenshtein_distance('Sjöstedt', 'Sjostedt'))) # unicode example +print("# edit distances (low edit distance means words are more similar):") +print( + "damerau_levenshtein_distance('{}', '{}') = {}".format( + "smtih", "smith", damerau_levenshtein_distance("smtih", "smith") + ) +) +print( + "damerau_levenshtein_distance('{}', '{}') = {}".format( + "snapple", "apple", damerau_levenshtein_distance("snapple", "apple") + ) +) +print( + "damerau_levenshtein_distance('{}', '{}') = {}".format( + "testing", "testtn", damerau_levenshtein_distance("testing", "testtn") + ) +) +print( + "damerau_levenshtein_distance('{}', '{}') = {}".format( + "saturday", "sunday", damerau_levenshtein_distance("saturday", "sunday") + ) +) +print( + "damerau_levenshtein_distance('{}', '{}') = {}".format( + "Saturday", "saturday", damerau_levenshtein_distance("Saturday", "saturday") + ) +) +print( + "damerau_levenshtein_distance('{}', '{}') = {}".format( + "orange", "pumpkin", damerau_levenshtein_distance("orange", "pumpkin") + ) +) +print( + "damerau_levenshtein_distance('{}', '{}') = {}".format( + "gifts", "profit", damerau_levenshtein_distance("gifts", "profit") + ) +) +print( + "damerau_levenshtein_distance('{}', '{}') = {} # unicode example\n".format( + "Sjöstedt", "Sjostedt", damerau_levenshtein_distance("Sjöstedt", "Sjostedt") + ) +) # unicode example -print('# normalized edit distances (low ratio means words are similar):') -print("normalized_damerau_levenshtein_distance('{}', '{}') = {}".format('smtih', 'smith', normalized_damerau_levenshtein_distance('smtih', 'smith'))) -print("normalized_damerau_levenshtein_distance('{}', '{}') = {}".format('snapple', 'apple', normalized_damerau_levenshtein_distance('snapple', 'apple'))) -print("normalized_damerau_levenshtein_distance('{}', '{}') = {}".format('testing', 'testtn', normalized_damerau_levenshtein_distance('testing', 'testtn'))) -print("normalized_damerau_levenshtein_distance('{}', '{}') = {}".format('saturday', 'sunday', normalized_damerau_levenshtein_distance('saturday', 'sunday'))) -print("normalized_damerau_levenshtein_distance('{}', '{}') = {}".format('Saturday', 'saturday', normalized_damerau_levenshtein_distance('Saturday', 'saturday'))) -print("normalized_damerau_levenshtein_distance('{}', '{}') = {}".format('orange', 'pumpkin', normalized_damerau_levenshtein_distance('orange', 'pumpkin'))) -print("normalized_damerau_levenshtein_distance('{}', '{}') = {}".format('gifts', 'profit', normalized_damerau_levenshtein_distance('gifts', 'profit'))) -print("normalized_damerau_levenshtein_distance('{}', '{}') = {} # unicode example\n".format('Sjöstedt', 'Sjostedt', normalized_damerau_levenshtein_distance('Sjöstedt', 'Sjostedt'))) # unicode example +print("# normalized edit distances (low ratio means words are similar):") +print( + "normalized_damerau_levenshtein_distance('{}', '{}') = {}".format( + "smtih", "smith", normalized_damerau_levenshtein_distance("smtih", "smith") + ) +) +print( + "normalized_damerau_levenshtein_distance('{}', '{}') = {}".format( + "snapple", "apple", normalized_damerau_levenshtein_distance("snapple", "apple") + ) +) +print( + "normalized_damerau_levenshtein_distance('{}', '{}') = {}".format( + "testing", + "testtn", + normalized_damerau_levenshtein_distance("testing", "testtn"), + ) +) +print( + "normalized_damerau_levenshtein_distance('{}', '{}') = {}".format( + "saturday", + "sunday", + normalized_damerau_levenshtein_distance("saturday", "sunday"), + ) +) +print( + "normalized_damerau_levenshtein_distance('{}', '{}') = {}".format( + "Saturday", + "saturday", + normalized_damerau_levenshtein_distance("Saturday", "saturday"), + ) +) +print( + "normalized_damerau_levenshtein_distance('{}', '{}') = {}".format( + "orange", + "pumpkin", + normalized_damerau_levenshtein_distance("orange", "pumpkin"), + ) +) +print( + "normalized_damerau_levenshtein_distance('{}', '{}') = {}".format( + "gifts", "profit", normalized_damerau_levenshtein_distance("gifts", "profit") + ) +) +print( + "normalized_damerau_levenshtein_distance('{}', '{}') = {} # unicode example\n".format( + "Sjöstedt", + "Sjostedt", + normalized_damerau_levenshtein_distance("Sjöstedt", "Sjostedt"), + ) +) # unicode example -print('# edit distances for a single sequence against an array of sequences') -array = np.array(['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']) -print("damerau_levenshtein_distance_ndarray('{}', np.array({})) = {}".format('Saturday', array, damerau_levenshtein_distance_ndarray('Saturday', array))) -print("normalized_damerau_levenshtein_distance_ndarray('{}', np.array({})) = {}\n".format('Saturday', array, normalized_damerau_levenshtein_distance_ndarray('Saturday', array))) +print("# edit distances for a single sequence against an array of sequences") +array = np.array( + ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] +) +print( + "damerau_levenshtein_distance_ndarray('{}', np.array({})) = {}".format( + "Saturday", array, damerau_levenshtein_distance_ndarray("Saturday", array) + ) +) +print( + "normalized_damerau_levenshtein_distance_ndarray('{}', np.array({})) = {}\n".format( + "Saturday", + array, + normalized_damerau_levenshtein_distance_ndarray("Saturday", array), + ) +) -print('# normalized edit distances for a single sequence against an array of sequences - unicode') -array = np.array(['Sjöstedt', 'Sjostedt', 'Söstedt', 'Sjöedt']) -print("damerau_levenshtein_distance_ndarray('{}', np.array({})) = {}".format('Sjöstedt', array, damerau_levenshtein_distance_ndarray('Sjöstedt', array))) -print("normalized_damerau_levenshtein_distance_ndarray('{}', np.array({})) = {}\n".format('Sjöstedt', array, normalized_damerau_levenshtein_distance_ndarray('Sjöstedt', array))) +print( + "# normalized edit distances for a single sequence against an array of sequences - unicode" +) +array = np.array(["Sjöstedt", "Sjostedt", "Söstedt", "Sjöedt"]) +print( + "damerau_levenshtein_distance_ndarray('{}', np.array({})) = {}".format( + "Sjöstedt", array, damerau_levenshtein_distance_ndarray("Sjöstedt", array) + ) +) +print( + "normalized_damerau_levenshtein_distance_ndarray('{}', np.array({})) = {}\n".format( + "Sjöstedt", + array, + normalized_damerau_levenshtein_distance_ndarray("Sjöstedt", array), + ) +) # random words will be comprised of ascii letters, numbers, and spaces -print('# performance testing:') -chars = string.ascii_letters + string.digits + ' ' -word1 = ''.join([random.choice(chars) for i in range(30)]) # generate a random string of characters of length 30 -word2 = ''.join([random.choice(chars) for i in range(30)]) # and another -print("""\ +print("# performance testing:") +chars = string.ascii_letters + string.digits + " " +word1 = "".join( + [random.choice(chars) for i in range(30)] +) # generate a random string of characters of length 30 +word2 = "".join([random.choice(chars) for i in range(30)]) # and another +print( + """\ timeit.timeit("damerau_levenshtein_distance('{}', '{}')", 'from pyxdameraulevenshtein import damerau_levenshtein_distance', number=500000) = {} seconds""".format( - word1, word2, timeit.timeit("damerau_levenshtein_distance('{}', '{}')".format(word1, word2), - 'from pyxdameraulevenshtein import damerau_levenshtein_distance', - number=500000)) + word1, + word2, + timeit.timeit( + "damerau_levenshtein_distance('{}', '{}')".format(word1, word2), + "from pyxdameraulevenshtein import damerau_levenshtein_distance", + number=500000, + ), + ) ) -print("""\ +print( + """\ timeit.timeit("damerau_levenshtein_distance('{}', '{}')", 'from pyxdameraulevenshtein import damerau_levenshtein_distance', number=500000) = {} seconds # short-circuit makes this faster""".format( - word1, word1, timeit.timeit("damerau_levenshtein_distance('{}', '{}')".format(word1, word1), - 'from pyxdameraulevenshtein import damerau_levenshtein_distance', - number=500000)) + word1, + word1, + timeit.timeit( + "damerau_levenshtein_distance('{}', '{}')".format(word1, word1), + "from pyxdameraulevenshtein import damerau_levenshtein_distance", + number=500000, + ), + ) ) diff --git "a/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__pyxdameraulevenshtein/fix_command.py" "b/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__pyxdameraulevenshtein/fix_command.py" index c1ebcbe22..3bb0a61ad 100644 --- "a/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__pyxdameraulevenshtein/fix_command.py" +++ "b/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__pyxdameraulevenshtein/fix_command.py" @@ -1,43 +1,54 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pyxDamerauLevenshtein # https://github.com/gfairchild/pyxDamerauLevenshtein # SOURCE: https://github.com/gfairchild/pyxDamerauLevenshtein/blob/master/examples/examples.py +import numpy as np +from pyxdameraulevenshtein import normalized_damerau_levenshtein_distance_ndarray + ALL_COMMANDS = [ - 'команды', - 'ругнись', - 'насмеши', - 'погода', - 'котики', - 'что посмотреть', - 'угадай', - 'hex2str', 'str2hex', - 'bin2str', 'str2bin', - 'qrcode', - 'добавь напоминание', 'удали напоминание', - 'короткая ссылка', - 'найди гифку', - 'курс валют', - 'курс криптовалют', - 'язык Йоды', - 'язык падонков', 'олбанский', 'олбанский язык', - 'православный язык', + "команды", + "ругнись", + "насмеши", + "погода", + "котики", + "что посмотреть", + "угадай", + "hex2str", + "str2hex", + "bin2str", + "str2bin", + "qrcode", + "добавь напоминание", + "удали напоминание", + "короткая ссылка", + "найди гифку", + "курс валют", + "курс криптовалют", + "язык Йоды", + "язык падонков", + "олбанский", + "олбанский язык", + "православный язык", ] def fix_command(text): - import numpy as np array = np.array(ALL_COMMANDS) - from pyxdameraulevenshtein import normalized_damerau_levenshtein_distance_ndarray # TODO: поиграться с damerau_levenshtein_distance_ndarray - result = list(zip(ALL_COMMANDS, list(normalized_damerau_levenshtein_distance_ndarray(text, array)))) + result = list( + zip( + ALL_COMMANDS, + list(normalized_damerau_levenshtein_distance_ndarray(text, array)), + ) + ) # print('\n' + text, sorted(result, key=lambda x: x[1])) command, rate = min(result, key=lambda x: x[1]) @@ -50,67 +61,67 @@ def fix_command(text): return command -if __name__ == '__main__': +if __name__ == "__main__": # SHOW RESULT - def check(text): - format_text = '{:<%s} -> {}' % (len(max(ALL_COMMANDS, key=len)) + 2) + def check(text) -> None: + format_text = "{:<%s} -> {}" % (len(max(ALL_COMMANDS, key=len)) + 2) command = fix_command(text) if command is None: - result = 'is None (не удалось распознать команду: "{}")'.format(text) + result = f'is None (не удалось распознать команду: "{text}")' print(format_text.format(text, result)) else: print(format_text.format(text, command)) - check('команды') - check('комнды') - check('команду') - check('кманды') - check('комманды') - check('короткую ссылку') - check('курс валют') - check('курсы валюты') - check('добавь напоминание') - check('добавь напаменание') - check('добавить напоминание') - check('добавить команду') - check('hex2str') - check('hex2dtr') - check('qrcode') - check('qtcode') - check('qrcodr') - check('курс криптовалют') - check('курс крептоволют') + check("команды") + check("комнды") + check("команду") + check("кманды") + check("комманды") + check("короткую ссылку") + check("курс валют") + check("курсы валюты") + check("добавь напоминание") + check("добавь напаменание") + check("добавить напоминание") + check("добавить команду") + check("hex2str") + check("hex2dtr") + check("qrcode") + check("qtcode") + check("qrcodr") + check("курс криптовалют") + check("курс крептоволют") # # Run test - def run_tests(): - def test(text, expected): + def run_tests() -> None: + def test(text, expected) -> None: command = fix_command(text) - assert expected == command, 'Expected: "{}", get: "{}"'.format(expected, command) + assert expected == command, f'Expected: "{expected}", get: "{command}"' - expected = 'команды' - test('команды', expected) - test('комнды', expected) - test('команду', expected) - test('команду', expected) - test('комманды', expected) + expected = "команды" + test("команды", expected) + test("комнды", expected) + test("команду", expected) + test("команду", expected) + test("комманды", expected) - expected = 'короткая ссылка' - test('короткую ссылку', expected) + expected = "короткая ссылка" + test("короткую ссылку", expected) - expected = 'курс валют' - test('курс валют', expected) - test('курсы валюты', expected) + expected = "курс валют" + test("курс валют", expected) + test("курсы валюты", expected) - expected = 'добавь напоминание' - test('добавь напоминание', expected) - test('добавь напаменание', expected) - test('добавить напоминание', expected) + expected = "добавь напоминание" + test("добавь напоминание", expected) + test("добавь напаменание", expected) + test("добавить напоминание", expected) expected = None - test('добавить команду', expected) - test('купить слона', expected) - test('кмнд', expected) + test("добавить команду", expected) + test("купить слона", expected) + test("кмнд", expected) run_tests() diff --git "a/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__pyxdameraulevenshtein/match_two_words.py" "b/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__pyxdameraulevenshtein/match_two_words.py" index e4940afe8..6f4b332fc 100644 --- "a/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__pyxdameraulevenshtein/match_two_words.py" +++ "b/Damerau\342\200\223Levenshtein_distance__misprints__\320\276\320\277\320\265\321\207\320\260\321\202\320\272\320\270/use__pyxdameraulevenshtein/match_two_words.py" @@ -1,28 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pyxDamerauLevenshtein # SOURCE: https://github.com/gfairchild/pyxDamerauLevenshtein +from pyxdameraulevenshtein import damerau_levenshtein_distance -def match_two_words(word_1, word_2): - from pyxdameraulevenshtein import damerau_levenshtein_distance +def match_two_words(word_1: str, word_2: str) -> bool: number = damerau_levenshtein_distance(word_1, word_2) # Считаем что разница в 2 символа и меньше еще нормальная return number < 3 -if __name__ == '__main__': - need_word = 'Привет' +if __name__ == "__main__": + need_word = "Привет" - print(match_two_words('Привет', need_word)) # True - print(match_two_words('Првет', need_word)) # True - print(match_two_words('Прывет', need_word)) # True - print(match_two_words('Привед', need_word)) # True - print(match_two_words('Превед', need_word)) # True + print(match_two_words("Привет", need_word)) # True + print(match_two_words("Првет", need_word)) # True + print(match_two_words("Прывет", need_word)) # True + print(match_two_words("Привед", need_word)) # True + print(match_two_words("Превед", need_word)) # True - print(match_two_words('Преед', need_word)) # False + print(match_two_words("Преед", need_word)) # False diff --git a/Dark Souls/backup__save_files/by_autosave/common.py b/Dark Souls/backup__save_files/by_autosave/common.py deleted file mode 100644 index 9582fad13..000000000 --- a/Dark Souls/backup__save_files/by_autosave/common.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import os -import time -from glob import glob -import traceback - -# For import utils.py -import sys -sys.path.append('..') - -from utils import get_logger, backup - - -log = get_logger(__file__) - - -def backup_saves(path_ds_save: str, forced=False): - try: - for path_file_name in glob(path_ds_save): - log.debug(f"Check: {path_file_name}") - - # Get timestamps - save_timestamp = os.path.getmtime(path_file_name) - now_timestamp = time.time() - - # Save backup. If less than 600 secs have passed since the last modification of the file - is_modified = (now_timestamp - save_timestamp) < 600 - ok = forced or is_modified - log.debug(f"{'Need backup' if ok else 'Not need backup'}. " - f"Reason: Forced={forced}, Is modified file save={is_modified}") - if not ok: - continue - - file_name_backup = backup(path_file_name, now_timestamp) - log.debug(f"Saving backup: {file_name_backup}") - - except: - print("ERROR:\n" + traceback.format_exc()) - time.sleep(5 * 60) - - -def run(path_ds_save: str, timeout_minutes=5): - # Example: r'~\Documents\NBGI\DarkSouls\*\DRAKS0005.sl2' - path_ds_save = os.path.expanduser(path_ds_save) - - print(f'{path_ds_save}\n') - - backup_saves(path_ds_save, forced=True) - - while True: - print(f"\nSleeping for {timeout_minutes} minutes\n") - time.sleep(timeout_minutes * 60) - - backup_saves(path_ds_save, False) - diff --git a/Dark Souls/backup__save_files/by_autosave/ds1.py b/Dark Souls/backup__save_files/by_autosave/ds1.py deleted file mode 100644 index 8b3904421..000000000 --- a/Dark Souls/backup__save_files/by_autosave/ds1.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# C:\Users\\Documents\NBGI\DarkSouls\\DRAKS0005.sl2 -# OR r'~\Documents\NBGI\DarkSouls\*\*.sl2' -PATH_DS_SAVE = r'~\Documents\NBGI\DarkSouls\*\DRAKS0005.sl2' - -if __name__ == '__main__': - from common import run - run(PATH_DS_SAVE) diff --git a/Dark Souls/backup__save_files/by_autosave/ds1_remastered.py b/Dark Souls/backup__save_files/by_autosave/ds1_remastered.py deleted file mode 100644 index 38518a01e..000000000 --- a/Dark Souls/backup__save_files/by_autosave/ds1_remastered.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# C:\Users\\Documents\NBGI\DARK SOULS REMASTERED\\DRAKS0005.sl2 -# OR r'~\Documents\NBGI\DARK SOULS REMASTERED\*\*.sl2' -PATH_DS_SAVE = r'~\Documents\NBGI\DARK SOULS REMASTERED\*\DRAKS0005.sl2' - -if __name__ == '__main__': - from common import run - run(PATH_DS_SAVE) diff --git a/Dark Souls/backup__save_files/by_autosave/ds2.py b/Dark Souls/backup__save_files/by_autosave/ds2.py deleted file mode 100644 index f16ebcc48..000000000 --- a/Dark Souls/backup__save_files/by_autosave/ds2.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# C:\Users\\AppData\Roaming\DarkSoulsII\*\*.sl2' -PATH_DS_SAVE = r'~\AppData\Roaming\DarkSoulsII\*\*.sl2' - -if __name__ == '__main__': - from common import run - run(PATH_DS_SAVE) diff --git a/Dark Souls/backup__save_files/by_autosave/ds3.py b/Dark Souls/backup__save_files/by_autosave/ds3.py deleted file mode 100644 index 66998db9b..000000000 --- a/Dark Souls/backup__save_files/by_autosave/ds3.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# C:\Users\\AppData\Roaming\DarkSoulsIII\*\*.sl2' -PATH_DS_SAVE = r'~\AppData\Roaming\DarkSoulsIII\*\*.sl2' - -if __name__ == '__main__': - from common import run - run(PATH_DS_SAVE) diff --git a/Dark Souls/backup__save_files/by_hotkey/common.py b/Dark Souls/backup__save_files/by_hotkey/common.py deleted file mode 100644 index b33a53fc1..000000000 --- a/Dark Souls/backup__save_files/by_hotkey/common.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import os -import time -import traceback -from glob import glob - -# For import utils.py -import sys -sys.path.append('..') - -from utils import get_logger, backup - -# pip install keyboard -import keyboard - - -def beep(): - try: - import winsound - winsound.Beep(1000, duration=50) - except: - # ignore - pass - - -log = get_logger(__file__) - -# Default hotkey -HOTKEY = "CTRL + F1" - - -def backup_saves(path_ds_save: str): - try: - for path_file_name in glob(path_ds_save): - file_name_backup = backup(path_file_name) - log.debug(f"Saving backup: {file_name_backup}") - - beep() - - except: - print("ERROR:\n" + traceback.format_exc()) - time.sleep(5 * 60) - - -def run(path_ds_save: str, hotkey=HOTKEY): - # Example: r'~\Documents\NBGI\DarkSouls\*\DRAKS0005.sl2' - path_ds_save = os.path.expanduser(path_ds_save) - - print(f'{path_ds_save}') - print(f'Using hotkey: {hotkey}\n') - - # Run hotkey - keyboard.add_hotkey(hotkey, lambda: backup_saves(path_ds_save)) - keyboard.wait() diff --git a/Dark Souls/backup__save_files/by_hotkey/ds1.py b/Dark Souls/backup__save_files/by_hotkey/ds1.py deleted file mode 100644 index 4a5dc5135..000000000 --- a/Dark Souls/backup__save_files/by_hotkey/ds1.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# C:\Users\\Documents\NBGI\DarkSouls\\DRAKS0005.sl2 -# OR r'~\Documents\NBGI\DarkSouls\*\*.sl2' -PATH_DS_SAVE = r'~\Documents\NBGI\DarkSouls\*\DRAKS0005.sl2' - -if __name__ == '__main__': - from common import run - # Example set another hotkey: - # run(PATH_DS_SAVE, hotkey="Ctrl + Alt + F + D + F3") - run(PATH_DS_SAVE) diff --git a/Dark Souls/backup__save_files/by_hotkey/ds1_remastered.py b/Dark Souls/backup__save_files/by_hotkey/ds1_remastered.py deleted file mode 100644 index 38518a01e..000000000 --- a/Dark Souls/backup__save_files/by_hotkey/ds1_remastered.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# C:\Users\\Documents\NBGI\DARK SOULS REMASTERED\\DRAKS0005.sl2 -# OR r'~\Documents\NBGI\DARK SOULS REMASTERED\*\*.sl2' -PATH_DS_SAVE = r'~\Documents\NBGI\DARK SOULS REMASTERED\*\DRAKS0005.sl2' - -if __name__ == '__main__': - from common import run - run(PATH_DS_SAVE) diff --git a/Dark Souls/backup__save_files/by_hotkey/ds2.py b/Dark Souls/backup__save_files/by_hotkey/ds2.py deleted file mode 100644 index f16ebcc48..000000000 --- a/Dark Souls/backup__save_files/by_hotkey/ds2.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# C:\Users\\AppData\Roaming\DarkSoulsII\*\*.sl2' -PATH_DS_SAVE = r'~\AppData\Roaming\DarkSoulsII\*\*.sl2' - -if __name__ == '__main__': - from common import run - run(PATH_DS_SAVE) diff --git a/Dark Souls/backup__save_files/by_hotkey/ds3.py b/Dark Souls/backup__save_files/by_hotkey/ds3.py deleted file mode 100644 index 66998db9b..000000000 --- a/Dark Souls/backup__save_files/by_hotkey/ds3.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# C:\Users\\AppData\Roaming\DarkSoulsIII\*\*.sl2' -PATH_DS_SAVE = r'~\AppData\Roaming\DarkSoulsIII\*\*.sl2' - -if __name__ == '__main__': - from common import run - run(PATH_DS_SAVE) diff --git a/Dark Souls/backup__save_files/utils.py b/Dark Souls/backup__save_files/utils.py deleted file mode 100644 index dc9e3d237..000000000 --- a/Dark Souls/backup__save_files/utils.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import os -import time -import shutil - - -def get_logger(name): - import logging - log = logging.getLogger(name) - log.setLevel(logging.DEBUG) - - formatter = logging.Formatter('[%(asctime)s] %(message)s') - - import sys - sh = logging.StreamHandler(stream=sys.stdout) - sh.setFormatter(formatter) - log.addHandler(sh) - - return log - - -def backup(path_file_name: str, now_timestamp=None) -> str: - if now_timestamp is None: - now_timestamp = time.time() - - path_dir, file_name = os.path.split(path_file_name) - - path_dir_backup = os.path.join(path_dir, "BACKUP") - - # Create backup dir - os.makedirs(path_dir_backup, exist_ok=True) - - time_backup = time.strftime("%y-%m-%d_%H%M%S", time.localtime(now_timestamp)) - file_name_backup = file_name + '.backup_' + time_backup - file_name_backup = os.path.join(path_dir_backup, file_name_backup) - - shutil.copyfile(path_file_name, file_name_backup) - - return file_name_backup diff --git a/Dark Souls/bonfires.py b/Dark Souls/bonfires.py deleted file mode 100644 index ed89807eb..000000000 --- a/Dark Souls/bonfires.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import requests -from bs4 import BeautifulSoup - - -rs = requests.get('https://darksouls.fandom.com/ru/wiki/Костер') -root = BeautifulSoup(rs.content, 'html.parser') - -for h2 in root.select('h2'): - headline = h2.select_one('.mw-headline') - if not headline: - continue - - if headline.get_text(strip=True) != 'Список костров': - continue - - for li in h2.find_next_sibling('ol').select('li'): - print(li.get_text(strip=True, separator=' ')) diff --git a/Dark Souls/common.py b/Dark Souls/common.py deleted file mode 100644 index 714b0d70e..000000000 --- a/Dark Souls/common.py +++ /dev/null @@ -1,317 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from collections import defaultdict -from typing import List, Tuple, Dict, Union -from urllib.parse import urljoin - -import requests -from bs4 import BeautifulSoup - - -URL_DS1 = "http://ru.darksouls.wikia.com/wiki/Категория:Локации_(Dark_Souls)" -URL_DS2 = "http://ru.darksouls.wikia.com/wiki/Категория:Локации_(Dark_Souls_II)" -URL_DS3 = "http://ru.darksouls.wikia.com/wiki/Категория:Локации_(Dark_Souls_III)" - - -class Parser: - def __init__(self, url_locations: str, log=True): - self._url_locations = url_locations - self._is_log = log - - self._visited_locations: List[str] = [] - self._links: List[Tuple[str, str]] = [] - self._link_boss: List[Tuple[str, str]] = [] - - self._location_by_url: Dict[str, str] = dict() - self._boss_by_url: Dict[str, str] = dict() - - self._bosses: List[str] = [] - self._location_by_bosses: Dict[str, List[Tuple[str, str]]] = defaultdict(list) - - @staticmethod - def DS1(log=True) -> 'Parser': - return Parser(URL_DS1, log) - - @staticmethod - def DS2(log=True) -> 'Parser': - return Parser(URL_DS2, log) - - @staticmethod - def DS3(log=True) -> 'Parser': - return Parser(URL_DS3, log) - - def _log(self, *args, **kwargs): - self._is_log and print(*args, **kwargs) - - @staticmethod - def __fix_title(title: str) -> str: - # Для удобства обработки приводим к одному регистру - title = title.lower().strip() - - # Удаляем из названий " (Dark Souls III)" - title = title.replace('(dark souls iii)', '').strip() - - # Исправляем название "Крепость Сен" -> "Крепость Сена" - if title == 'крепость сен': - title = 'крепость сена' - - # В переходах "Темная Бездна Былого" было указано "Темные руины", но имеется ввиду локация "Темнолесье" - if title == 'темные руины': - title = 'темнолесье' - - # Приводим к общему виду - return title.title() - - @staticmethod - def get_links_location(url_location: str) -> List[Tuple[str, str]]: - """ - Функция для поиска переходов из локации - - """ - - rs = requests.get(url_location) - root = BeautifulSoup(rs.content, 'html.parser') - - table_locations = None - - for table in root.select('table.pi-horizontal-group'): - if 'Переходы:' in table.text: - table_locations = table - break - - locations = [] - - # Если не нашли - if not table_locations: - return locations - - for a in table_locations.select('a'): - url = urljoin(rs.url, a['href']) - title = Parser.__fix_title(a.text) - - locations.append((title, url)) - - return locations - - @staticmethod - def get_bosses_by_location(url_location: str) -> List[Tuple[str, str]]: - """ - Функция для поиска боссов локации - - """ - - rs = requests.get(url_location) - root = BeautifulSoup(rs.content, 'html.parser') - - table_bosses = None - - for table in root.select('table.pi-horizontal-group'): - if 'Босс локации:' in table.text: - table_bosses = table - break - - bosses = [] - - # Если не нашли - if not table_bosses: - return bosses - - for a in table_bosses.select('a'): - url = urljoin(rs.url, a['href']) - title = Parser.__fix_title(a.text) - - bosses.append((title, url)) - - return bosses - - def parse(self) -> 'Parser': - visited_locations = set() - links = set() - link_boss = set() - - rs = requests.get(self._url_locations) - root = BeautifulSoup(rs.content, 'html.parser') - - for a in root.select('.category-page__member-link'): - abs_url = urljoin(rs.url, a['href']) - - # У любой локации есть связанная с ней локация. Если нет -- значит это страница - # не про локацию - locations = self.get_links_location(abs_url) - if not locations: - continue - - title = Parser.__fix_title(a.text) - visited_locations.add(title) - - self._location_by_url[title] = abs_url - - self._log(title, abs_url) - self._log(' Переходы:') - - # Найдем связанные локации - for x_title, x_url in locations: - self._log(' {} -> {}'.format(x_title, x_url)) - self._location_by_url[x_title] = x_url - - # Проверяем что локации с обратной связью не занесены - if (x_title, title) not in links: - links.add((title, x_title)) - - self._log(' Боссы:') - - # Найдем боссов локации - bosses = self.get_bosses_by_location(abs_url) - self._location_by_bosses[title] += bosses - - for boss_name, boss_url in bosses: - self._log(' {} -> {}'.format(boss_name, boss_url)) - self._boss_by_url[boss_name] = boss_url - self._bosses.append(boss_name) - - link_boss.add((title, boss_name)) - - self._log() - - self._visited_locations = sorted(visited_locations) - self._links = sorted(links) - self._link_boss = sorted(link_boss) - - return self - - def get_url_locations(self) -> str: - return self._url_locations - - def get_locations(self) -> List[str]: - return self._visited_locations - - def get_links(self) -> List[Tuple[str, str]]: - return self._links - - def get_bosses(self) -> List[str]: - return self._bosses - - def get_bosses_of_location(self) -> List[Tuple[str, str]]: - return self._link_boss - - def get_location_by_bosses(self, location_name: str = None) -> Union[Dict[str, List[Tuple[str, str]]], List[Tuple[str, str]]]: - if location_name is None: - return self._location_by_bosses - - return self._location_by_bosses.get(location_name) - - def get_location_by_url(self, location_name: str = None) -> Union[Dict[str, str], str]: - if location_name is None: - return self._location_by_url - - return self._location_by_url.get(location_name) - - def get_boss_by_url(self, boss_name: str = None) -> Union[Dict[str, str], str]: - if boss_name is None: - return self._boss_by_url - - return self._boss_by_url.get(boss_name) - - -def get_links_location(url_location: str) -> List[Tuple[str, str]]: - """ - Функция для поиска переходов из локации - - """ - - return Parser.get_links_location(url_location) - - -def get_bosses_by_location(url_location: str) -> List[Tuple[str, str]]: - """ - Функция для поиска боссов локации - - """ - - return Parser.get_bosses_by_location(url_location) - - -def parse_locations(url_locations: str, log=True) -> (List[str], List[Tuple[str, str]], List[Tuple[str, str]]): - p = Parser(url_locations, log).parse() - return p.get_locations(), p.get_links(), p.get_bosses_of_location() - - -def parse_locations_ds1(log=True) -> (List[str], List[Tuple[str, str]], List[Tuple[str, str]]): - return parse_locations(URL_DS1, log) - - -def parse_locations_ds2(log=True) -> (List[str], List[Tuple[str, str]], List[Tuple[str, str]]): - return parse_locations(URL_DS2, log) - - -def parse_locations_ds3(log=True) -> (List[str], List[Tuple[str, str]], List[Tuple[str, str]]): - return parse_locations(URL_DS3, log) - - -def find_links_ds1(log=True) -> List[Tuple[str, str]]: - return parse_locations_ds1(log)[1] - - -def find_links_ds2(log=True) -> List[Tuple[str, str]]: - return parse_locations_ds2(log)[1] - - -def find_links_ds3(log=True) -> List[Tuple[str, str]]: - return parse_locations_ds3(log)[1] - - -def find_bosses_of_location_ds1(log=True) -> List[Tuple[str, str]]: - return parse_locations_ds1(log)[2] - - -def find_bosses_of_location_ds2(log=True) -> List[Tuple[str, str]]: - return parse_locations_ds2(log)[2] - - -def find_bosses_of_location_ds3(log=True) -> List[Tuple[str, str]]: - return parse_locations_ds3(log)[2] - - -if __name__ == '__main__': - visited_locations, links, link_boss = parse_locations_ds1() - - # Выведем итоговый список - print(len(visited_locations), visited_locations) - print(len(links), links) - print(len(link_boss), link_boss) - - print() - - links = find_links_ds1(log=False) - print(len(links), links) - - print() - - # DS1 - bosses = get_bosses_by_location('http://ru.darksouls.wikia.com/wiki/Северное_Прибежище_Нежити') - print(len(bosses), bosses) - - # DS2 - bosses = get_bosses_by_location('http://ru.darksouls.wikia.com/wiki/Маджула') - print(len(bosses), bosses) - - # DS3 - bosses = get_bosses_by_location('http://ru.darksouls.wikia.com/wiki/Анор_Лондо_(Dark_Souls_III)') - print(len(bosses), bosses) - - print() - - p = Parser(URL_DS1, log=False).parse() - # OR: - # p = ParserDS1(log=False).parse() - print('get_url_locations:', p.get_url_locations()) - print('get_locations:', p.get_locations()) - print('get_bosses:', p.get_bosses()) - print('get_links:', p.get_links()) - print('get_bosses_of_location:', p.get_bosses_of_location()) - print('get_boss_by_url:', p.get_boss_by_url()) - print('get_location_by_url:', p.get_location_by_url()) diff --git a/Dark Souls/generate_HTML_location_graph__with_d3js/generate_graph_html.py b/Dark Souls/generate_HTML_location_graph__with_d3js/generate_graph_html.py deleted file mode 100644 index c41aa9ffa..000000000 --- a/Dark Souls/generate_HTML_location_graph__with_d3js/generate_graph_html.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List, Tuple - - -def get_dataset_text(links: List[Tuple[str, str]]) -> str: - # Список всех узлов графа - nodes = set() - for a, b in links: - nodes.add(a) - nodes.add(b) - - # Задаем отсортированный порядок узлов - nodes = sorted(nodes) - - # Список ребер - edges = list() - - for a, b in links: - source = nodes.index(a) - target = nodes.index(b) - - edges.append({'source': source, 'target': target}) - - nodes_dict = [{"name": title} for title in nodes] - - # Пример структуры графа. - # nodes -- список узлов - # edges -- список ребер, описываются как {source: <индекс узла>, target: <индекс узла>}, - # - # var dataset = { - # nodes: [ - # {name: "Adam"}, {name: "Bob"}, {name: "Carrie"}, {name: "Donovan"}, - # {name: "Edward"}, {name: "Felicity"}, {name: "George"}, - # {name: "Hannah"}, {name: "Iris"}, {name: "Jerry"} - # ], - # edges: [ - # {source: 0, target: 1}, {source: 0, target: 2}, {source: 0, target: 3}, - # {source: 0, target: 4}, {source: 1, target: 5}, {source: 2, target: 5}, - # {source: 2, target: 5}, {source: 3, target: 4}, {source: 5, target: 8}, - # {source: 5, target: 9}, {source: 6, target: 7}, {source: 7, target: 8}, - # {source: 8, target: 9} - # ] - # }; - text = """\ - var dataset = {{ - nodes: {}, - edges: {} - }}; - """.format(nodes_dict, edges) - - return text - - -def generate(file_name_to: str, links: List[Tuple[str, str]], title="Graph"): - template_title = '{{title}}' - template_dataset = '{{dataset}}' - - dataset_text = get_dataset_text(links) - - with open('template_graph.html', 'r', encoding='utf-8') as f: - text = f.read() - text = text.replace(template_title, title) - text = text.replace(template_dataset, dataset_text) - - with open(file_name_to, 'w', encoding='utf-8') as f: - f.write(text) - - -if __name__ == '__main__': - links = [('Арена Братства Крови', 'Чистилище Нежити'), ('Башня Луны', 'Забытая Крепость'), ('Башня Солнца', 'Железная Цитадель'), ('Безлюдная Пристань', 'Забытая Крепость'), ('Безлюдная Пристань', 'Огненная Башня Хейда'), ('Большой Собор', 'Ледяная Элеум Лойс'), ('Большой Собор', 'Предвечный Хаос'), ('Бухта Брайтстоун-Тселдора', 'Воспоминания Дракона'), ('Бухта Брайтстоун-Тселдора', 'Двери Фарроса'), ('Бухта Брайтстоун-Тселдора', 'Личные Палаты Лорда'), ('Гнездо Дракона', 'Храм Дракона'), ('Гнездо Дракона', 'Цитадель Алдии'), ('Двери Фарроса', 'Темнолесье'), ('Долина Жатвы', 'Земляной Пик'), ('Долина Жатвы', 'Роща Охотника'), ('Железная Цитадель', 'Земляной Пик'), ('Железная Цитадель', 'Мглистая Башня'), ('Железный Проход', 'Мглистая Башня'), ('Забытая Крепость', 'Лес Павших Гигантов'), ('Забытая Крепость', 'Холм Грешников'), ('Замок Дранглик', 'Королевский Проход'), ('Замок Дранглик', 'Трон Желания'), ('Замок Дранглик', 'Храм Зимы'), ('Карта Дранглика', 'Маджула'), ('Королевский Проход', 'Храм Аманы'), ('Ледяная Элеум Лойс', 'Холодные Окраины'), ('Ледяная Элеум Лойс', 'Храм Зимы'), ('Лес Павших Гигантов', 'Маджула'), ('Лес Павших Гигантов', 'Память Ваммара'), ('Лес Павших Гигантов', 'Память Джейта'), ('Лес Павших Гигантов', 'Память Орро'), ('Маджула', 'Междумирье'), ('Маджула', 'Могила Святых'), ('Маджула', 'Огненная Башня Хейда'), ('Маджула', 'Помойка'), ('Маджула', 'Роща Охотника'), ('Маджула', 'Темнолесье'), ('Мглистая Башня', 'Память Старого Железного Короля'), ('Могила Святых', 'Помойка'), ('Огненная Башня Хейда', 'Собор Лазурного Пути'), ('Память Короля', 'Склеп Нежити'), ('Пещера Мертвых', 'Шульва, Священный Город'), ('Помойка', 'Черная Расселина'), ('Роща Охотника', 'Чистилище Нежити'), ('Святилище Дракона', 'Убежище Дракона'), ('Святилище Дракона', 'Шульва, Священный Город'), ('Склеп Нежити', 'Храм Аманы'), ('Темная Бездна Былого', 'Замок Дранглик'), ('Темная Бездна Былого', 'Темные Руины'), ('Темная Бездна Былого', 'Черная Расселина'), ('Темнолесье', 'Храм Зимы'), ('Темнолесье', 'Цитадель Алдии'), ('Черная Расселина', 'Шульва, Священный Город')] - dataset = get_dataset_text(links) - print(dataset) - - links = [('Adam', 'Bob'), ('Adam', 'Ivan'), ('Bob', 'Iris'), ('Bob', 'Jerry'), ('Iris', 'George')] - generate('example.html', links) - - links = [('Сквайр', 'Рыцарь'), ('Сквайр', 'Охотник на ведьм'), ('Охотник на ведьм', 'Инквизитор'), ('Инквизитор', 'Великий инквизитор'), ('Рыцарь', 'Рыцарь Империи'), ('Рыцарь Империи', 'Ангел'), ('Рыцарь Империи', 'Паладин'), ('Паладин', 'Святой мститель'), ('Паладин', 'Защитник веры')] - generate('example_disciples.html', links, title='Graph Disciples') diff --git a/Dark Souls/generate_HTML_location_graph__with_d3js/generate_graph_html__Dark_Souls.py b/Dark Souls/generate_HTML_location_graph__with_d3js/generate_graph_html__Dark_Souls.py deleted file mode 100644 index fa298ea65..000000000 --- a/Dark Souls/generate_HTML_location_graph__with_d3js/generate_graph_html__Dark_Souls.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from generate_graph_html import generate - - -# ../common.py/find_links_ds1 -# ../print_locations/print_locations__Dark_Souls.py -links = [('Алтарь Огня', 'Бездна'), ('Алтарь Огня', 'Горнило Первого Пламени'), ('Алтарь Огня', 'Храм Огня'), ('Алтарь Света', 'Город Нежити'), ('Алтарь Света', 'Уезд Нежити'), ('Анор Лондо', 'Архивы Герцога'), ('Анор Лондо', 'Крепость Сена'), ('Анор Лондо', 'Нарисованный Мир Ариамис'), ('Архивы Герцога', 'Кристальный Грот'), ('Бездна', 'Руины Нового Лондо'), ('Владение Квилег', 'Руины Демонов'), ('Владение Квилег', 'Чумной Город'), ('Глубины', 'Город Нежити'), ('Глубины', 'Чумной Город'), ('Город Нежити', 'Озеро Темных Корней'), ('Город Нежити', 'Уезд Нежити'), ('Город Нежити', 'Храм Огня'), ('Долина Драконов', 'Озеро Темных Корней'), ('Долина Драконов', 'Руины Нового Лондо'), ('Долина Драконов', 'Чумной Город'), ('Забытый Изалит', 'Руины Демонов'), ('Зал Стоицизма', 'Поселок Олачиль'), ('Катакомбы', 'Склеп Великанов'), ('Катакомбы', 'Храм Огня'), ('Королевский Лес', 'Поселок Олачиль'), ('Королевский Лес', 'Святилище Олачиля'), ('Королевский Лес', 'Ущелье Бездны'), ('Крепость Сена', 'Уезд Нежити'), ('Озеро Золы', 'Полость'), ('Озеро Темных Корней', 'Сад Темных Корней'), ('Озеро Темных Корней', 'Священный Сад'), ('Подземелье Поселка Олачиль', 'Поселок Олачиль'), ('Подземелье Поселка Олачиль', 'Ущелье Бездны'), ('Полость', 'Чумной Город'), ('Поселок Олачиль', 'Битва За Стоицизм'), ('Руины Нового Лондо', 'Храм Огня'), ('Сад Темных Корней', 'Уезд Нежити'), ('Святилище Олачиля', 'Священный Сад'), ('Северное Прибежище Нежити', 'Храм Огня'), ('Уезд Нежити', 'Глубины'), ('Уезд Нежити', 'Храм Огня')] -generate('locations__Dark_Souls.html', links, title='Locations Dark Souls') - -# ../common.py/find_links_ds2 -# ../print_locations/print_locations__Dark_Souls_II.py -links = [('Арена Братства Крови', 'Чистилище Нежити'), ('Башня Луны', 'Забытая Крепость'), ('Башня Солнца', 'Железная Цитадель'), ('Безлюдная Пристань', 'Забытая Крепость'), ('Безлюдная Пристань', 'Огненная Башня Хейда'), ('Большой Собор', 'Ледяная Элеум Лойс'), ('Большой Собор', 'Предвечный Хаос'), ('Бухта Брайтстоун-Тселдора', 'Воспоминания Дракона'), ('Бухта Брайтстоун-Тселдора', 'Двери Фарроса'), ('Бухта Брайтстоун-Тселдора', 'Личные Палаты Лорда'), ('Гнездо Дракона', 'Храм Дракона'), ('Гнездо Дракона', 'Цитадель Алдии'), ('Двери Фарроса', 'Темнолесье'), ('Долина Жатвы', 'Земляной Пик'), ('Долина Жатвы', 'Роща Охотника'), ('Железная Цитадель', 'Земляной Пик'), ('Железная Цитадель', 'Мглистая Башня'), ('Железный Проход', 'Мглистая Башня'), ('Забытая Крепость', 'Лес Павших Гигантов'), ('Забытая Крепость', 'Холм Грешников'), ('Замок Дранглик', 'Королевский Проход'), ('Замок Дранглик', 'Трон Желания'), ('Замок Дранглик', 'Храм Зимы'), ('Карта Дранглика', 'Маджула'), ('Королевский Проход', 'Храм Аманы'), ('Ледяная Элеум Лойс', 'Холодные Окраины'), ('Ледяная Элеум Лойс', 'Храм Зимы'), ('Лес Павших Гигантов', 'Маджула'), ('Лес Павших Гигантов', 'Память Ваммара'), ('Лес Павших Гигантов', 'Память Джейта'), ('Лес Павших Гигантов', 'Память Орро'), ('Маджула', 'Междумирье'), ('Маджула', 'Могила Святых'), ('Маджула', 'Огненная Башня Хейда'), ('Маджула', 'Помойка'), ('Маджула', 'Роща Охотника'), ('Маджула', 'Темнолесье'), ('Мглистая Башня', 'Память Старого Железного Короля'), ('Могила Святых', 'Помойка'), ('Огненная Башня Хейда', 'Собор Лазурного Пути'), ('Память Короля', 'Склеп Нежити'), ('Пещера Мертвых', 'Шульва, Священный Город'), ('Помойка', 'Черная Расселина'), ('Роща Охотника', 'Чистилище Нежити'), ('Святилище Дракона', 'Убежище Дракона'), ('Святилище Дракона', 'Шульва, Священный Город'), ('Склеп Нежити', 'Храм Аманы'), ('Темная Бездна Былого', 'Замок Дранглик'), ('Темная Бездна Былого', 'Темнолесье'), ('Темная Бездна Былого', 'Черная Расселина'), ('Темнолесье', 'Храм Зимы'), ('Темнолесье', 'Цитадель Алдии'), ('Черная Расселина', 'Шульва, Священный Город')] -generate('locations__Dark_Souls_2.html', links, title='Locations Dark Souls 2') - -# ../common.py/find_links_ds3 -# ../print_locations/print_locations__Dark_Souls_III.py -links = [('Анор Лондо', 'Иритилл Холодной Долины'), ('Великий Архив', 'Замок Лотрика'), ('Высокая Стена Лотрика', 'Замок Лотрика'), ('Высокая Стена Лотрика', 'Кладбище Пепла'), ('Высокая Стена Лотрика', 'Поселение Нежити'), ('Город За Стеной', 'Груда Отбросов'), ('Груда Отбросов', 'Нарисованный Мир Арианделя'), ('Груда Отбросов', 'Печь Первого Пламени'), ('Заброшенные Могилы', 'Сад Снедаемого Короля'), ('Замок Лотрика', 'Сад Снедаемого Короля'), ('Иритилл Холодной Долины', 'Катакомбы Картуса'), ('Иритилл Холодной Долины', 'Подземелье Иритилла'), ('Катакомбы Картуса', 'Тлеющее Озеро'), ('Катакомбы Картуса', 'Цитадель Фаррона'), ('Кладбище Пепла', 'Храм Огня'), ('Нарисованный Мир Арианделя', 'Храм Глубин'), ('Оскверненная Столица', 'Подземелье Иритилла'), ('Печь Первого Пламени', 'Храм Огня'), ('Пик Древних Драконов', 'Подземелье Иритилла'), ('Поединок Нежити', 'Храм Огня'), ('Поселение Нежити', 'Путь Жертв'), ('Путь Жертв', 'Храм Глубин'), ('Путь Жертв', 'Цитадель Фаррона'), ('Храм Огня', 'Высокая Стена Лотрика')] -generate('locations__Dark_Souls_3.html', links, title='Locations Dark Souls 3') diff --git a/Dark Souls/graph_ds2_locations__graphviz__DOT/ds2_locations.py b/Dark Souls/graph_ds2_locations__graphviz__DOT/ds2_locations.py deleted file mode 100644 index 7818ed087..000000000 --- a/Dark Souls/graph_ds2_locations__graphviz__DOT/ds2_locations.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# pip install graphviz -from graphviz import Graph - - -g = Graph('g', filename='ds2_locations.gv', comment='Locations Dark Souls') - -# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/45852f0541644dfb0478e98e2df530c1bb93eeae/Dark%20Souls/generate_HTML_location_graph__with_d3js/generate_graph_html__Dark_Souls.py -links = [('Алтарь Огня', 'Бездна'), ('Алтарь Огня', 'Горнило Первого Пламени'), ('Алтарь Огня', 'Храм Огня'), ('Алтарь Света', 'Город Нежити'), ('Алтарь Света', 'Уезд Нежити'), ('Анор Лондо', 'Архивы Герцога'), ('Анор Лондо', 'Крепость Сена'), ('Анор Лондо', 'Нарисованный Мир Ариамис'), ('Архивы Герцога', 'Кристальный Грот'), ('Бездна', 'Руины Нового Лондо'), ('Владение Квилег', 'Руины Демонов'), ('Владение Квилег', 'Чумной Город'), ('Глубины', 'Город Нежити'), ('Глубины', 'Чумной Город'), ('Город Нежити', 'Озеро Темных Корней'), ('Город Нежити', 'Уезд Нежити'), ('Город Нежити', 'Храм Огня'), ('Долина Драконов', 'Озеро Темных Корней'), ('Долина Драконов', 'Руины Нового Лондо'), ('Долина Драконов', 'Чумной Город'), ('Забытый Изалит', 'Руины Демонов'), ('Зал Стоицизма', 'Поселок Олачиль'), ('Катакомбы', 'Склеп Великанов'), ('Катакомбы', 'Храм Огня'), ('Королевский Лес', 'Поселок Олачиль'), ('Королевский Лес', 'Святилище Олачиля'), ('Королевский Лес', 'Ущелье Бездны'), ('Крепость Сена', 'Уезд Нежити'), ('Озеро Золы', 'Полость'), ('Озеро Темных Корней', 'Сад Темных Корней'), ('Озеро Темных Корней', 'Священный Сад'), ('Подземелье Поселка Олачиль', 'Поселок Олачиль'), ('Подземелье Поселка Олачиль', 'Ущелье Бездны'), ('Полость', 'Чумной Город'), ('Поселок Олачиль', 'Битва За Стоицизм'), ('Руины Нового Лондо', 'Храм Огня'), ('Сад Темных Корней', 'Уезд Нежити'), ('Святилище Олачиля', 'Священный Сад'), ('Северное Прибежище Нежити', 'Храм Огня'), ('Уезд Нежити', 'Глубины'), ('Уезд Нежити', 'Храм Огня')] - -for a, b in links: - g.edge(a, b) - -g.attr(label=r'\n\nLocations Dark Souls') -g.view() - -# g.render(format='jpg') diff --git a/Dark Souls/print__deaths_from_save_file/main.py b/Dark Souls/print__deaths_from_save_file/main.py deleted file mode 100644 index 82f08065d..000000000 --- a/Dark Souls/print__deaths_from_save_file/main.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import struct - - -# SOURCE: analog dsfp ("import dsfp") / https://github.com/tarvitz/dsfp -# SOURCE: https://github.com/RKYates/Dark-Souls-Death-Count-cgi-page/blob/master/cgi-bin/results.py - - -def get_chars_and_deaths(file_name: str) -> list: - chars = [] - - with open(file_name, 'rb') as f: - f.seek(0x2c0, 0) - - for slot in range(0, 10): - f.seek(0x100, 1) - name = f.read(32) - - if name[0] != '\00': - f.seek(-0x120, 1) - f.seek(0x1f128, 1) - deaths = f.read(4) - f.seek(-0x04, 1) - f.seek(-0x1f128, 1) - char_name = name.decode('utf-16').split('\00')[0] - char_deaths = struct.unpack('i', deaths)[0] - if char_name: - chars.append((char_name, char_deaths)) - else: - f.seek(-0x120, 1) - - f.seek(0x60190, 1) - - return chars - - -if __name__ == '__main__': - # C:\Users\\Documents\NBGI\DarkSouls\\DRAKS0005.sl2 - file_name = 'DRAKS0005.sl2' - - chars = get_chars_and_deaths(file_name) - for name, deaths in chars: - print('{}, deaths: {}'.format(name, deaths)) - - print() - - # Find and print from local - import os - from glob import glob - - path = os.path.expanduser(r'~\Documents\NBGI\DarkSouls\*\DRAKS0005.sl2') - for file_name in glob(path): - print(file_name) - - chars = get_chars_and_deaths(file_name) - for name, deaths in chars: - print(' {}, deaths: {}'.format(name, deaths)) diff --git a/Dark Souls/print__stats/about_statistics.py b/Dark Souls/print__stats/about_statistics.py deleted file mode 100644 index 71cd6d298..000000000 --- a/Dark Souls/print__stats/about_statistics.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# Fresh values from: -# from print__stats_ds1 import get_stats_ds1 -# from print__stats_ds2 import get_stats_ds2 -# from print__stats_ds3 import get_stats_ds3 -# -# stats_ds1 = [x[0] for x in get_stats_ds1()] -# stats_ds2 = [x[0] for x in get_stats_ds2()] -# stats_ds3 = [x[0] for x in get_stats_ds3()] - -stats_ds1 = ['Вера', 'Выносливость', 'Живучесть', 'Интеллект', 'Ловкость', 'Поиск предметов', 'Сила', 'Сопротивление', 'Уровень', 'Ученость', 'Человечность'] -stats_ds2 = ['Адаптируемость', 'Вера', 'Жизненная сила', 'Интеллект', 'Ловкость', 'Сила', 'Стойкость', 'Уровень', 'Ученость', 'Физическая мощь'] -stats_ds3 = ['Вера', 'Жизненная сила', 'Интеллект', 'Ловкость', 'Сила', 'Стойкость', 'Удача', 'Уровень', 'Ученость', 'Физическая мощь'] - -stats_ds1 = set(stats_ds1) -stats_ds2 = set(stats_ds2) -stats_ds3 = set(stats_ds3) - -common_stats = sorted(stats_ds1 & stats_ds2 & stats_ds3) -print(common_stats) # ['Вера', 'Интеллект', 'Ловкость', 'Сила', 'Уровень', 'Ученость'] -print() - -unique_ds1 = sorted(stats_ds1 - stats_ds2 - stats_ds3) -print(unique_ds1) # ['Выносливость', 'Живучесть', 'Поиск предметов', 'Сопротивление', 'Человечность'] - -unique_ds2 = sorted(stats_ds2 - stats_ds1 - stats_ds3) -print(unique_ds2) # ['Адаптируемость'] - -unique_ds3 = sorted(stats_ds3 - stats_ds1 - stats_ds2) -print(unique_ds3) # ['Удача'] diff --git a/Dark Souls/print__stats/common.py b/Dark Souls/print__stats/common.py deleted file mode 100644 index 9bdae99d7..000000000 --- a/Dark Souls/print__stats/common.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import requests - -from bs4 import BeautifulSoup - - -def get_parsed_two_column_table_stats(url: str) -> [(str, str)]: - rs = requests.get(url) - root = BeautifulSoup(rs.content, 'html.parser') - - table = root.select_one('table') - - items = [] - - for tr in table.select('tr'): - tds = tr.select('td') - if len(tds) != 3: - continue - - title_node, description_node = tds[1:] - - title = title_node.text.strip() - description = description_node.text.strip() - - items.append((title, description)) - - items.sort(key=lambda x: x[0]) - - return items diff --git a/Dark Souls/print__stats/print__stats_ds1.py b/Dark Souls/print__stats/print__stats_ds1.py deleted file mode 100644 index 34aa1972d..000000000 --- a/Dark Souls/print__stats/print__stats_ds1.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from common import get_parsed_two_column_table_stats - - -def get_stats_ds1() -> [(str, str)]: - url = 'http://ru.darksouls.wikia.com/wiki/Характеристики_(Dark_Souls)' - return get_parsed_two_column_table_stats(url) - - -if __name__ == '__main__': - items = get_stats_ds1() - print(f'items ({len(items)}): {items}') - print() - - # ['Вера', 'Выносливость', 'Живучесть', 'Интеллект', 'Ловкость', 'Поиск предметов', 'Сила', 'Сопротивление', 'Уровень', 'Ученость', 'Человечность'] - stats_titles = [x[0] for x in items] - print(stats_titles) - - print() - - for title, description in items: - print('{:20}: {}'.format(title, repr(description))) diff --git a/Dark Souls/print__stats/print__stats_ds2.py b/Dark Souls/print__stats/print__stats_ds2.py deleted file mode 100644 index 9a05a808b..000000000 --- a/Dark Souls/print__stats/print__stats_ds2.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from common import get_parsed_two_column_table_stats - - -def get_stats_ds2() -> [(str, str)]: - url = 'http://ru.darksouls.wikia.com/wiki/Характеристики_(Dark_Souls_II)' - return get_parsed_two_column_table_stats(url) - - -if __name__ == '__main__': - items = get_stats_ds2() - print(f'items ({len(items)}): {items}') - print() - - # ['Адаптируемость', 'Вера', 'Жизненная сила', 'Интеллект', 'Ловкость', 'Сила', 'Стойкость', 'Уровень', 'Ученость', 'Физическая мощь'] - stats_titles = [x[0] for x in items] - print(stats_titles) - - print() - - for title, description in items: - print('{:20}: {}'.format(title, repr(description))) diff --git a/Dark Souls/print__stats/print__stats_ds3.py b/Dark Souls/print__stats/print__stats_ds3.py deleted file mode 100644 index e531abb71..000000000 --- a/Dark Souls/print__stats/print__stats_ds3.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from common import get_parsed_two_column_table_stats - - -def get_stats_ds3() -> [(str, str)]: - url = 'http://ru.darksouls.wikia.com/wiki/Характеристики_(Dark_Souls_III)' - return get_parsed_two_column_table_stats(url) - - -if __name__ == '__main__': - items = get_stats_ds3() - print(f'items ({len(items)}): {items}') - print() - - # ['Вера', 'Жизненная сила', 'Интеллект', 'Ловкость', 'Сила', 'Стойкость', 'Удача', 'Уровень', 'Ученость', 'Физическая мощь'] - stats_titles = [x[0] for x in items] - print(stats_titles) - - print() - - for title, description in items: - print('{:20}: {}'.format(title, repr(description))) diff --git a/Dark Souls/print__stats_from_save_file__using__dsfp/main.py b/Dark Souls/print__stats_from_save_file__using__dsfp/main.py deleted file mode 100644 index c8dfde830..000000000 --- a/Dark Souls/print__stats_from_save_file__using__dsfp/main.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# SOURCE: https://github.com/tarvitz/dsfp - - -# C:\Users\\Documents\NBGI\DarkSouls\\DRAKS0005.sl2 -file_name = '../print__deaths_from_save_file/DRAKS0005.sl2' - -import dsfp -ds = dsfp.DSSaveFileParser(file_name) - -stats_list = ds.get_stats() -print(f'Stats list ({len(stats_list)}): {stats_list}') -print() - -print(f'Character ({len(stats_list)}):') - -for stats in stats_list: - name, deaths = stats['name'], stats['deaths'] - print(' {}, deaths: {}'.format(name, deaths)) diff --git a/Dark Souls/print_bosses_of_location/print_bosses_of_location__Dark_Souls.py b/Dark Souls/print_bosses_of_location/print_bosses_of_location__Dark_Souls.py deleted file mode 100644 index 8bfe8331b..000000000 --- a/Dark Souls/print_bosses_of_location/print_bosses_of_location__Dark_Souls.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import sys -sys.path.append('..') - -from common import Parser, find_bosses_of_location_ds1 - - -p = Parser.DS1(log=False).parse() - -for location in p.get_locations(): - print(f'{location} -> {p.get_location_by_url(location)}') - - for boss, url in p.get_location_by_bosses(location): - print(f' {boss} -> {url}') - - print() - -print() - -bosses_of_location = find_bosses_of_location_ds1() -print(len(bosses_of_location), bosses_of_location) diff --git a/Dark Souls/print_bosses_of_location/print_bosses_of_location__Dark_Souls_II.py b/Dark Souls/print_bosses_of_location/print_bosses_of_location__Dark_Souls_II.py deleted file mode 100644 index e24a20610..000000000 --- a/Dark Souls/print_bosses_of_location/print_bosses_of_location__Dark_Souls_II.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import sys -sys.path.append('..') - -from common import Parser, find_bosses_of_location_ds2 - - -p = Parser.DS2(log=False).parse() - -for location in p.get_locations(): - print(f'{location} -> {p.get_location_by_url(location)}') - - for boss, url in p.get_location_by_bosses(location): - print(f' {boss} -> {url}') - - print() - -print() - -bosses_of_location = find_bosses_of_location_ds2() -print(len(bosses_of_location), bosses_of_location) diff --git a/Dark Souls/print_bosses_of_location/print_bosses_of_location__Dark_Souls_III.py b/Dark Souls/print_bosses_of_location/print_bosses_of_location__Dark_Souls_III.py deleted file mode 100644 index f5175867e..000000000 --- a/Dark Souls/print_bosses_of_location/print_bosses_of_location__Dark_Souls_III.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import sys -sys.path.append('..') - -from common import Parser, find_bosses_of_location_ds3 - - -p = Parser.DS3(log=False).parse() - -for location in p.get_locations(): - print(f'{location} -> {p.get_location_by_url(location)}') - - for boss, url in p.get_location_by_bosses(location): - print(f' {boss} -> {url}') - - print() - -print() - -bosses_of_location = find_bosses_of_location_ds3() -print(len(bosses_of_location), bosses_of_location) diff --git a/Dark Souls/print_locations/Dark_Souls_II__print_locations_from_start_location__with_graph.py b/Dark Souls/print_locations/Dark_Souls_II__print_locations_from_start_location__with_graph.py deleted file mode 100644 index 69cdd4717..000000000 --- a/Dark Souls/print_locations/Dark_Souls_II__print_locations_from_start_location__with_graph.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from Dark_Souls_II__print_locations_from_start_location import print_transitions - - -links = set() -visited_locations = set() -url_start_location = 'http://ru.darksouls.wikia.com/wiki/Междумирье' - -print_transitions(url_start_location, 'Междумирье', visited_locations, links, log=False) - -print() -print(len(visited_locations), sorted(visited_locations)) -print(len(links), sorted(links)) - -# TODO: pretty graph -import networkx as nx -G = nx.Graph() - -for source, target in links: - G.add_edge(source, target) - -pos = nx.spring_layout(G) # positions for all nodes - -# edges -nx.draw_networkx_edges(G, pos, edgelist=G.edges(), width=6) - -# nodes -nx.draw_networkx_nodes(G, pos, node_size=70) - -# labels -nx.draw_networkx_labels(G, pos, font_size=20, font_family='sans-serif') - -import matplotlib.pyplot as plt -# plt.figure(1) -plt.axis('off') -# plt.savefig("ds2_locations_graph.png") # save as png -plt.show() # display diff --git a/Dark Souls/print_timeline_of_release_years/main.py b/Dark Souls/print_timeline_of_release_years/main.py deleted file mode 100644 index 3924b9c3f..000000000 --- a/Dark Souls/print_timeline_of_release_years/main.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# Хронология выхода игр *Souls -# SOURCE: https://github.com/gil9red/SimplePyScripts/tree/e0a12b17adde0e0505ab54ae30bbe97f5f2cb038/html_parsing/Wikipedia__Timeline_of_release_years - - -import requests -from bs4 import BeautifulSoup - - -def get_parsed_two_column_wikitable(url: str) -> [(str, str)]: - rs = requests.get(url) - root = BeautifulSoup(rs.content, 'html.parser') - - table = root.select_one('.wikitable') - - items = [] - - # Timeline of release years - for tr in table.select('tr'): - td_items = tr.select('td') - if len(td_items) != 2: - continue - - year = td_items[0].text.strip() - name = td_items[1].i.text.strip() - items.append((year, name)) - - return items - - -if __name__ == '__main__': - url = 'https://en.wikipedia.org/wiki/Souls_(series)' - for year, name in get_parsed_two_column_wikitable(url): - print(year, name) - -# 2009 Demon's Souls -# 2011 Dark Souls -# 2014 Dark Souls II -# 2015 Dark Souls II: Scholar of the First Sin -# 2016 Dark Souls III -# 2018 Dark Souls: Remastered diff --git a/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/config.py b/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/config.py deleted file mode 100644 index d83a6af15..000000000 --- a/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/config.py +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -URL_DS1 = 'https://darksouls.fandom.com/ru/wiki/Боссы' -URL_DS2 = 'https://darksouls.fandom.com/ru/wiki/Боссы_(Dark_Souls_II)' -URL_DS3 = 'https://darksouls.fandom.com/ru/wiki/Боссы_(Dark_Souls_III)' diff --git a/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/main.py b/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/main.py deleted file mode 100644 index 4dc2f6da9..000000000 --- a/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/main.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import sqlite3 - -from config import URL_DS1, URL_DS2, URL_DS3 -from utils import ( - get_bosses, print_bosses, convert_bosses_to_only_name, - export_to_json, export_to_sqlite, -) - - -bosses_ds1 = get_bosses(URL_DS1) -print_bosses(URL_DS1, bosses_ds1) -export_to_json('dumps/ds1/bosses.json', bosses_ds1) -export_to_json('dumps/ds1/bosses__only_name.json', convert_bosses_to_only_name(bosses_ds1)) - -bosses_ds2 = get_bosses(URL_DS2) -print_bosses(URL_DS2, bosses_ds2) -export_to_json('dumps/ds2/bosses.json', bosses_ds2) -export_to_json('dumps/ds2/bosses__only_name.json', convert_bosses_to_only_name(bosses_ds2)) - -bosses_ds3 = get_bosses(URL_DS3) -print_bosses(URL_DS3, bosses_ds3) -export_to_json('dumps/ds3/bosses.json', bosses_ds3) -export_to_json('dumps/ds3/bosses__only_name.json', convert_bosses_to_only_name(bosses_ds3)) - -# All bosses -bosses_ds123 = { - 'Dark Souls': bosses_ds1, - 'Dark Souls II': bosses_ds2, - 'Dark Souls III': bosses_ds3, -} -export_to_json('dumps/bosses_ds123.json', bosses_ds123) - -bosses_ds123__only_name = { - 'Dark Souls': convert_bosses_to_only_name(bosses_ds1), - 'Dark Souls II': convert_bosses_to_only_name(bosses_ds2), - 'Dark Souls III': convert_bosses_to_only_name(bosses_ds3), -} -export_to_json('dumps/bosses_ds123__only_name.json', bosses_ds123__only_name) - -# -# SQLITE -# -sql_file_name = 'dumps/bosses_ds123.sqlite' -export_to_sqlite(sql_file_name, bosses_ds123) - -# TEST -connect = sqlite3.connect(sql_file_name) -print('Total boss:', connect.execute('SELECT count(*) FROM BOSS').fetchone()[0]) -print('Total boss DS1:', connect.execute('SELECT count(*) FROM BOSS WHERE game = "Dark Souls"').fetchone()[0]) -print('Total boss DS2:', connect.execute('SELECT count(*) FROM BOSS WHERE game = "Dark Souls II"').fetchone()[0]) -print('Total boss DS3:', connect.execute('SELECT count(*) FROM BOSS WHERE game = "Dark Souls III"').fetchone()[0]) -# Total boss: 92 -# Total boss DS1: 26 -# Total boss DS2: 41 -# Total boss DS3: 25 diff --git a/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/print_stats_by_health_and_souls/_utils.py b/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/print_stats_by_health_and_souls/_utils.py deleted file mode 100644 index effc7640f..000000000 --- a/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/print_stats_by_health_and_souls/_utils.py +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import re -import sys -from typing import Union, Dict, List, Tuple -from pathlib import Path - -DIR = Path(__file__).resolve().parent - -sys.path.append(str(DIR.parent)) -sys.path.append(str(DIR.parent.parent)) -sys.path.append(str(DIR.parent.parent.parent)) - -from config import URL_DS1, URL_DS2, URL_DS3 -from utils import Boss, get_bosses - -from ascii_table__simple_pretty__format import print_pretty_table - - -def only_digits(text: Union[str, int], do_sum=None) -> int: - if not isinstance(text, str): - return text - - if isinstance(do_sum, str): - return sum(only_digits(x) for x in text.split(do_sum)) - - return int(re.sub(r'\D', '', text)) - - -def get_health(boss: Boss, items: Dict[str, List[str]]) -> int: - health_items = items['Здоровье'] - - boss_name = boss.name.lower() - - if boss_name == 'горгулья': - return only_digits(health_items[0], do_sum='+') - - if boss_name == 'орнштейн и смоуг': - return only_digits(health_items[1].split('&')[0], do_sum='/') - - # 3x 2,330 - if boss_name == 'стражи руин': - h = health_items[0].split(' ')[-1] - return 3 * only_digits(h) - - if boss_name in ['нагой сит', 'присцилла полукровка', 'черный дракон каламит']: - return only_digits(health_items[1], do_sum='/') - - if boss_name == 'страж святилища': - parts = health_items[1].split('/') - return only_digits(parts[0]) + only_digits(parts[2]) - - if boss_name == 'алчный демон': - return only_digits(health_items[1]) - - if boss_name in ['преследователь', 'знаток кристальных чар']: - return only_digits(health_items[2]) - - if boss_name == 'гибкий часовой': - return only_digits(health_items[3]) - - if 'заллен' in boss_name and 'луд' in boss_name: - return only_digits(health_items[0], do_sum='/') - - if boss_name == 'душа пепла': - first_idx = health_items.index('Первая фаза:') - second_idx = health_items.index('Вторая Фаза:') - return ( - only_digits(health_items[first_idx + 1]) - + only_digits(health_items[second_idx + 1]) - ) - - if boss_name == 'два драконьих всадника': - first_idx = health_items.index('Лучник:') - second_idx = health_items.index('Воин:') - return ( - only_digits(health_items[first_idx + 1]) - + only_digits(health_items[second_idx + 1]) - ) - - if boss_name == 'защитник трона и смотритель трона': - first_idx = health_items.index('Защитник:') - second_idx = health_items.index('Смотритель:') - return ( - only_digits(health_items[first_idx + 1]) - + only_digits(health_items[second_idx + 1]) - ) - - if boss_name == 'копье церкви': - return only_digits(health_items[2]) - - if boss_name == 'хранители бездны': - first_idx = health_items.index('Первая Фаза') - second_idx = health_items.index('Вторая Фаза') - return ( - only_digits(health_items[first_idx + 2]) - + only_digits(health_items[second_idx + 2]) - ) - - if boss_name == 'лотрик, младший принц и лориан, старший принц': - first_idx = health_items.index('Первая Фаза') - second_idx = health_items.index('Вторая Фаза') - lothric_idx = health_items.index('Лотрик') - return ( - only_digits(health_items[first_idx + 2]) - + only_digits(health_items[second_idx + 2]) - + only_digits(health_items[lothric_idx + 1]) - ) - - if boss_name == 'странствующий маг и прихожане': - first_idx = health_items.index('Странствующий маг:') - second_idx = health_items.index('Жрец:') - third_idx = health_items.index('Полый проситель:') - return ( - only_digits(health_items[first_idx + 1]) - + only_digits(health_items[second_idx + 1]) - + only_digits(health_items[third_idx + 1]) - ) - - if boss_name == 'повелители скелетов': - first_idx = health_items.index('Повелитель-воин:') - second_idx = health_items.index('Повелитель-маг:') - third_idx = health_items.index('Повелитель-жнец:') - return ( - only_digits(health_items[first_idx + 1]) - + only_digits(health_items[second_idx + 1]) - + only_digits(health_items[third_idx + 1]) - ) - - if boss_name == 'варг, церах и разорительница гробниц': - first_idx = health_items.index('Скорбящая разорительница гробниц:') - second_idx = health_items.index('Древний солдат Варг:') - third_idx = health_items.index('Старый исследователь Церах:') - return ( - only_digits(health_items[first_idx + 1]) - + only_digits(health_items[second_idx + 1]) - + only_digits(health_items[third_idx + 1]) - ) - - if boss_name == 'безымянный король': - first_idx = health_items.index('Повелитель Шторма') - second_idx = health_items.index('Безымянный Король') - return ( - only_digits(health_items[first_idx + 2]) - + only_digits(health_items[second_idx + 2]) - ) - - if boss_name == 'хранитель могилы чемпиона и великий волк': - first_idx = health_items.index('Хранитель') - second_idx = health_items.index('Великий волк') - return ( - only_digits(health_items[first_idx + 2]) - + only_digits(health_items[second_idx + 2]) - ) - - if boss_name == 'отец ариандель и сестра фриде': - first_idx = health_items.index('Сестра Фриде') - second_idx = health_items.index('Отец Ариандель и сестра Фриде') - third_idx = health_items.index('Черное пламя Фриде') - return ( - only_digits(health_items[first_idx + 2]) - + only_digits(health_items[second_idx + 2]) - + only_digits(health_items[third_idx + 2]) - ) - - if boss_name == 'демон-принц': - first_idx = health_items.index('Демон в агонии/Демон из глубин') - second_idx = health_items.index('Демон-принц') - return ( - only_digits(health_items[first_idx + 2], do_sum='/') - + only_digits(health_items[second_idx + 2]) - ) - - health_value = health_items[0] - return only_digits(health_value) - - -def get_souls(boss: Boss, items: Dict[str, List[str]]) -> int: - souls_items = items['Души'] - - boss_name = boss.name.lower() - - if boss_name == 'страж святилища': - return only_digits(souls_items[1].split('/')[1]) - - if boss_name in ['преследователь', 'знаток кристальных чар']: - return only_digits(souls_items[2]) - - if boss_name == 'гибкий часовой': - return only_digits(souls_items[3]) - - if boss_name == 'алдия, ученый первородного греха': - return 0 - - if 'заллен' in boss_name and 'луд' in boss_name: - return only_digits(souls_items[0].split('/')[0]) - - souls_value = souls_items[0] - return only_digits(souls_value) - - -def parse(url: str, debug_log=False) -> List[Tuple[str, int, int]]: - rows = [] - - for category, bosses in get_bosses(url).items(): - debug_log and print(category) - - for boss in bosses: - items = boss.get_boss_items() - try: - health = get_health(boss, items) - souls = get_souls(boss, items) - debug_log and print(f" {boss.name}. Здоровье: {health}, души: {souls}") - - rows.append((boss.name, health, souls)) - - except ValueError as e: - debug_log and print(e) - debug_log and print(boss.name) - debug_log and print(items['Здоровье']) - debug_log and print(items['Души']) - debug_log and sys.exit() - - debug_log and print() - - return rows - - -def print_stats( - rows: List[Tuple[str, int, int]], - headers=('NAME', 'HEALTH', 'SOULS'), - sort_column=1, -): - rows.sort(key=lambda x: x[sort_column], reverse=True) - - rows.insert(0, headers) - print_pretty_table(rows, align='<') diff --git a/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/utils.py b/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/utils.py deleted file mode 100644 index 909c22bfc..000000000 --- a/Dark Souls/ru_darksouls_wikia_com__fandom__bosses/utils.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import json -import sqlite3 -import os - -from collections import defaultdict -from typing import Dict, List, NamedTuple, Tuple -from urllib.parse import urljoin - -import requests -from bs4 import BeautifulSoup - - -session = requests.Session() -session.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0' - - -class Boss(NamedTuple): - name: str - url: str - - def get_boss_items(self) -> Dict[str, List[str]]: - _, root = parse(self.url) - - key_by_values = dict() - for table in root.select('table.pi-horizontal-group'): - el_items = table.select_one('tbody td[data-source]') - if not el_items: - continue - - data_source = el_items['data-source'] - items = [ - x.strip() - for x in el_items.get_text(strip=True, separator='\n').split('\n') - ] - - key_by_values[data_source] = items - - return key_by_values - - -def parse(url: str) -> Tuple[requests.Response, BeautifulSoup]: - rs = session.get(url) - return rs, BeautifulSoup(rs.content, 'html.parser') - - -def get_bosses(url: str) -> Dict[str, List[Boss]]: - rs, root = parse(url) - - bosses_by_category = defaultdict(list) - - category_name = None - - for tr in root.select('table tr'): - # Заголовок первым идет - th = tr.select_one('th') - if th: - category_name = th.text.strip().upper() - continue - - if not category_name: - continue - - td_list = [] - - for td in tr.select('td'): - name = td.text.strip() - if not name: - continue - - url = urljoin(rs.url, td.select_one('a')['href']) - boss = Boss(name, url) - - td_list.append(boss) - - bosses_by_category[category_name] += td_list - - return bosses_by_category - - -def print_bosses(url: str, bosses: Dict[str, List[Boss]]): - print('{} ({}):'.format(url, sum(len(i) for i in bosses.values()))) - - for category, bosses in bosses.items(): - print(f'{category} ({len(bosses)}):') - - for i, boss in enumerate(bosses, 1): - print(f' {i}. "{boss.name}": {boss.url}') - - print() - - print() - - -def convert_bosses_to_only_name(bosses: Dict[str, List[Boss]]) -> Dict[str, List[str]]: - bosses_only_name = dict() - for category, bosses_list in bosses.items(): - bosses_only_name[category] = [boss.name for boss in bosses_list] - - return bosses_only_name - - -def export_to_json(file_name, bosses): - dir_name = os.path.dirname(file_name) - os.makedirs(dir_name, exist_ok=True) - - json.dump(bosses, open(file_name, 'w', encoding='utf-8'), ensure_ascii=False, indent=4) - - -def export_to_sqlite(file_name: str, bosses_ds123: Dict[str, Dict[str, List[Boss]]]): - dir_name = os.path.dirname(file_name) - os.makedirs(dir_name, exist_ok=True) - - connect = sqlite3.connect(file_name) - - connect.executescript(''' - DROP TABLE IF EXISTS Boss; - - CREATE TABLE Boss ( - id INTEGER PRIMARY KEY, - game TEXT, - category TEXT, - name TEXT, - url TEXT - ); - ''') - - for game, categories in bosses_ds123.items(): - for category, bosses in categories.items(): - for boss in bosses: - connect.execute( - 'INSERT INTO Boss (game, category, name, url) VALUES (?, ?, ?, ?)', - (game, category, boss.name, boss.url) - ) - - connect.commit() diff --git a/DecodingPhoneNumberForCLI/DecodingPhoneNumberForCLI.py b/DecodingPhoneNumberForCLI/DecodingPhoneNumberForCLI.py index 509b1e6cd..624a712b0 100644 --- a/DecodingPhoneNumberForCLI/DecodingPhoneNumberForCLI.py +++ b/DecodingPhoneNumberForCLI/DecodingPhoneNumberForCLI.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ EN: Decoding the phone number for CLI. @@ -19,7 +19,7 @@ # Например, входящая строка 4434###552222311333661 соответствует номеру 4452136 -def decoding_phone_number(string): +def decoding_phone_number(string: str) -> str: string += " " result = "" last = "" @@ -39,7 +39,9 @@ def decoding_phone_number(string): repeat += 1 else: # Если символ не такой же как предыдущий if repeat > 1: # Если символ имеет повторы - if last is "#" and symbol_before_grill: # Если последним символов является # и ... + if ( + last is "#" and symbol_before_grill + ): # Если последним символов является # и ... result += symbol_before_grill # Добавим число, перед #, но саму # добавлять не будем symbol_before_grill = "" else: @@ -51,7 +53,7 @@ def decoding_phone_number(string): return result -if __name__ == '__main__': - string = '4434###552222311333661' - print('CLI = ' + string) +if __name__ == "__main__": + string = "4434###552222311333661" + print("CLI = " + string) print("Phone number = " + decoding_phone_number(string)) diff --git a/Decorators__examples/append_attributes_to_class.py b/Decorators__examples/append_attributes_to_class.py index 61fe0060d..d562c1a37 100644 --- a/Decorators__examples/append_attributes_to_class.py +++ b/Decorators__examples/append_attributes_to_class.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def attrs(**kwargs): @@ -22,4 +22,7 @@ class Foo: print(Foo.author) # Guido van Rossum print(Foo().author) # Guido van Rossum print(Foo().version) # 2.2 -print(list(filter(lambda x: not x.startswith('_'), dir(Foo())))) # ['author', 'version'] +print( + list(filter(lambda x: not x.startswith("_"), dir(Foo()))) +) +# ['author', 'version'] diff --git a/Decorators__examples/append_attributes_to_func.py b/Decorators__examples/append_attributes_to_func.py index bc2dc6428..dbd289174 100644 --- a/Decorators__examples/append_attributes_to_func.py +++ b/Decorators__examples/append_attributes_to_func.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def attrs(**kwargs): @@ -14,10 +14,10 @@ def decorate(f): @attrs(versionadded="2.2", author="Guido van Rossum") -def mymethod(text='ok'): +def mymethod(text="ok"): return text print(mymethod.author) # Guido van Rossum print(mymethod()) # ok -print(mymethod('no')) # ok +print(mymethod("no")) # ok diff --git a/Decorators__examples/append_handlers.py b/Decorators__examples/append_handlers.py index 4bfadcb5b..c44b19bfd 100644 --- a/Decorators__examples/append_handlers.py +++ b/Decorators__examples/append_handlers.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +from collections import defaultdict class Collector: - def __init__(self): + def __init__(self) -> None: self.handlers = [] - - from collections import defaultdict self.handlers_by_name = defaultdict(list) - def add(self, name='default'): + def add(self, name="default"): def decorator(func): self.handlers.append(func) self.handlers_by_name[name].append(func) @@ -24,27 +25,29 @@ def decorator(func): collector = Collector() -@collector.add(name='test') -def hello_world(end='!'): - print('hello world' + end) +@collector.add(name="test") +def hello_world(end="!") -> None: + print("hello world" + end) print(collector.handlers) # [] -print(collector.handlers_by_name) # defaultdict(, {'test': []}) +print(collector.handlers_by_name) +# defaultdict(, {'test': []}) -hello_world('!!!') # hello world!!! +hello_world("!!!") # hello world!!! hello_world() # hello world! print(collector.handlers) # [] collector.handlers[0]() # hello world! -@collector.add(name='this it say_hello!') -def say_hello(): - print('hello!') +@collector.add(name="this it say_hello!") +def say_hello() -> None: + print("hello!") print() -print(collector.handlers) # [, ] +print(collector.handlers) +# [, ] print(len(collector.handlers_by_name)) # 2 for func in collector.handlers: func() diff --git a/Decorators__examples/combine_decorators.py b/Decorators__examples/combine_decorators.py index a5d8eb9d3..86677b0d6 100644 --- a/Decorators__examples/combine_decorators.py +++ b/Decorators__examples/combine_decorators.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import functools @@ -36,6 +36,7 @@ def deco(f): for dec in reversed(decs): f = dec(f) return f + return deco @@ -59,8 +60,8 @@ def hello_2(text): return text -print(hello('Hello World!')) +print(hello("Hello World!")) # HELLO WORLD! -print(hello_2('Hello World!')) +print(hello_2("Hello World!")) # HELLO WORLD! diff --git a/Decorators__examples/combine_decorators__with_args.py b/Decorators__examples/combine_decorators__with_args.py index cbdf4a2db..6e71ff93a 100644 --- a/Decorators__examples/combine_decorators__with_args.py +++ b/Decorators__examples/combine_decorators__with_args.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import functools def get_attrs_str(kwargs: dict) -> str: - attrs = '' + attrs = "" if kwargs: - attrs = ' ' + ' '.join(f'{k}="{v}"' for k, v in kwargs.items()) + attrs = " " + " ".join(f'{k}="{v}"' for k, v in kwargs.items()) return attrs @@ -18,9 +18,9 @@ def get_attrs_str(kwargs: dict) -> str: def makebold(**decorator_kwargs): def actual_decorator(func): @functools.wraps(func) - def wrapped(*args, **kwargs): + def wrapped(*args, **kwargs) -> str: attrs = get_attrs_str(decorator_kwargs) - return f"" + func(*args, **kwargs) + "" + return f"{func(*args, **kwargs)}" return wrapped @@ -30,9 +30,9 @@ def wrapped(*args, **kwargs): def makeitalic(**decorator_kwargs): def actual_decorator(func): @functools.wraps(func) - def wrapped(*args, **kwargs): + def wrapped(*args, **kwargs) -> str: attrs = get_attrs_str(decorator_kwargs) - return f"" + func(*args, **kwargs) + "" + return f"{func(*args, **kwargs)}" return wrapped @@ -47,22 +47,48 @@ def wrapped(*args, **kwargs): return wrapped +def custom_tag( + name: str, + **arguments, +): + def actual_decorator(func): + @functools.wraps(func) + def wrapped(*args, **kwargs) -> str: + attrs = get_attrs_str(arguments) + return f"<{name}{attrs}>{func(*args, **kwargs)}" + + return wrapped + + return actual_decorator + + def composed(*decs): def deco(f): for dec in reversed(decs): f = dec(f) return f + return deco def multi(func): return composed( + custom_tag( + name="a", + href="https://example.com", + title="Hint!", + ), makebold(foo="1"), makeitalic(bar="2"), upper, )(func) +@custom_tag( + name="a", + href="https://example.com", + title="Hint!", +) @makebold(foo="1") @makeitalic(bar="2") @upper @@ -75,8 +101,9 @@ def hello_2(text): return text -print(hello('Hello World!')) -# HELLO WORLD! +if __name__ == '__main__': + print(hello("Hello World!")) + # HELLO WORLD! -print(hello_2('Hello World!')) -# HELLO WORLD! + print(hello_2("Hello World!")) + # HELLO WORLD! diff --git a/Decorators__examples/decorator__args_as_funcs.py b/Decorators__examples/decorator__args_as_funcs.py index 93655d075..b6b27499f 100644 --- a/Decorators__examples/decorator__args_as_funcs.py +++ b/Decorators__examples/decorator__args_as_funcs.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" class TextBuilder: - def __init__(self): + def __init__(self) -> None: self.result = [] # Функция, принимающая аргументы и возвращающая декоратор @@ -22,7 +22,7 @@ def wrapper(self, *args, **kwargs): # Декоратор возвращает обертку return wrapper - # Возаращаем сам декоратор + # Возвращаем сам декоратор return decorator # Функция, принимающая аргументы и возвращающая декоратор @@ -41,17 +41,20 @@ def wrapper(self, *args, **kwargs): # Декоратор возвращает обертку return wrapper - # Возаращаем сам декоратор + # Возвращаем сам декоратор return decorator - @_call_before(lambda self: self.result.append('+' + '-' * 10 + '+')) - @_call_after(lambda self: self.result.append('+' + '-' * 10 + '+'), lambda self: self.result.append('\n')) - def append(self, text: str) -> 'TextBuilder': + @_call_before(lambda self: self.result.append("+" + "-" * 10 + "+")) + @_call_after( + lambda self: self.result.append("+" + "-" * 10 + "+"), + lambda self: self.result.append("\n"), + ) + def append(self, text: str) -> "TextBuilder": self.result.append(text) return self def build(self): - return '\n'.join(self.result) + return "\n".join(self.result) builder = TextBuilder() diff --git a/Decorators__examples/decorator_method_class__with_shelve.py b/Decorators__examples/decorator_method_class__with_shelve.py new file mode 100644 index 000000000..5ff1f1429 --- /dev/null +++ b/Decorators__examples/decorator_method_class__with_shelve.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import shelve +import functools + +from pathlib import Path +from typing import Any + + +DIR: Path = Path(__file__).resolve().parent +DIR_DB: Path = DIR / "databases" +DB_FILE_NAME: Path = DIR_DB / "db.shelve" + + +DIR_DB.mkdir(parents=True, exist_ok=True) + + +class DB: + db_name: str = str(DB_FILE_NAME) + + def __init__(self) -> None: + self.db: shelve.Shelf | None = None + + def session(*decorator_args, **decorator_kwargs): + def actual_decorator(func): + @functools.wraps(func) + def wrapped(self, *args, **kwargs): + has_db: bool = self.db is not None + try: + if not has_db: + self.db = shelve.open(self.db_name, writeback=True) + return func(self, *args, **kwargs) + finally: + if not has_db and self.db is not None: + self.db.close() + self.db = None + + return wrapped + + return actual_decorator + + @session() + def get_value(self, name: str, default: Any = None) -> Any: + if not name: + return dict(self.db) + + if name not in self.db: + return default + return self.db.get(name) + + @session() + def set_value(self, name: str, value: Any) -> None: + self.db[name] = value + + def inc_value(self, name: str) -> int: + value = self.get_value(name, default=0) + value += 1 + self.set_value(name, value) + return value + + +if __name__ == "__main__": + db = DB() + print("name", db.get_value("name")) + + db.set_value("name", 123) + + users: dict[str, dict[str, Any]] = db.get_value("users", default=dict()) + print("users", users) + if not users: + users["Foo"] = dict(name="Foo", age=12) + users["Bar"] = dict(name="Bar", age=12) + db.set_value("users", users) + + counter: dict[str, int] = db.get_value("counter", default=dict()) + print("counter", counter) + if "value" not in counter: + counter["value"] = 0 + counter["value"] += 1 + db.set_value("counter", counter) + + print([db.inc_value("age") for _ in range(3)]) + + print(dict(db.get_value(""))) diff --git a/Decorators__examples/decorator_with_args_in_partial.py b/Decorators__examples/decorator_with_args_in_partial.py new file mode 100644 index 000000000..f7ab35865 --- /dev/null +++ b/Decorators__examples/decorator_with_args_in_partial.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from functools import partial +from combine_decorators__with_args import custom_tag + + +@custom_tag( + name="a", + href="https://example.com", + title="Hint!", +) +def hello(text): + return text + + +custom_tag_a = partial( + custom_tag, + name="a", + href="https://example.com", + title="Hint!", +) + + +@custom_tag_a() +def hello2(text): + return text + + +@custom_tag_a( + title="Other hint!", +) +def hello3(text): + return text + + +print(hello("Foo")) +# Foo + +print(hello2("Foo")) +# Foo + +print(hello3("Foo")) +# Foo diff --git a/Decorators__examples/example_1.py b/Decorators__examples/example_1.py index 511764c42..e7d123809 100644 --- a/Decorators__examples/example_1.py +++ b/Decorators__examples/example_1.py @@ -1,77 +1,90 @@ -__author__ = 'ipetrash' - - -if __name__ == '__main__': - def getprint(str="hello world!"): - print(str) - - def decor(func): - def wrapper(*args, **kwargs): - print("1 begin: " + func.__name__) - print("Args={} kwargs={}".format(args, kwargs)) - f = func(*args, **kwargs) - print("2 end: " + func.__name__ + "\n") - return f - return wrapper - - def predecor(w="W"): - print(w, end=': ') - - getprint() - getprint("Py!") - print() - f = decor(getprint) - f() - f("Py!") - - - def rgb2hex(get_rgb_func): - def wrapper(*args, **kwargs): - r, g, b = get_rgb_func(*args, **kwargs) - return '#{:02x}{:02x}{:02x}'.format(r, g, b) - return wrapper - - class RGB: - def __init__(self): - self._r = 0xff - self._g = 0xff - self._b = 0xff - - def getr(self): - return self._r - def setr(self, r): - self._r = r - r = property(getr, setr) - - def getg(self): - return self._g - def setg(self, g): - self._g = g - g = property(getg, setg) - - def getb(self): - return self._b - def setb(self, b): - self._b = b - b = property(getb, setb) - - def setrgb(self, r, g, b): - self.r, self.g, self.b = r, g, b - - @rgb2hex - def getrgb(self): - return (self.r, self.g, self.b) - - - rgb = RGB() - print('rgb.r={}'.format(rgb.r)) - rgb.setrgb(0xff, 0x1, 0xff) - print("rgb.getrgb(): %s" % rgb.getrgb()) - print() - - @decor - def foo(a, b): - print("{} ^ {} = {}".format(a, b, (a ** b))) - - foo(2, 3) - foo(b=3, a=2) \ No newline at end of file +__author__ = "ipetrash" + + +def getprint(str="hello world!") -> None: + print(str) + + +def decor(func): + def wrapper(*args, **kwargs): + print("1 begin: " + func.__name__) + print(f"Args={args} kwargs={kwargs}") + f = func(*args, **kwargs) + print("2 end: " + func.__name__ + "\n") + return f + + return wrapper + + +def predecor(w="W") -> None: + print(w, end=": ") + + +getprint() +getprint("Py!") +print() +f = decor(getprint) +f() +f("Py!") + + +def rgb2hex(get_rgb_func): + def wrapper(*args, **kwargs) -> str: + r, g, b = get_rgb_func(*args, **kwargs) + return f"#{r:02x}{g:02x}{b:02x}" + + return wrapper + + +class RGB: + def __init__(self) -> None: + self._r = 0xFF + self._g = 0xFF + self._b = 0xFF + + def getr(self): + return self._r + + def setr(self, r) -> None: + self._r = r + + r = property(getr, setr) + + def getg(self): + return self._g + + def setg(self, g) -> None: + self._g = g + + g = property(getg, setg) + + def getb(self): + return self._b + + def setb(self, b) -> None: + self._b = b + + b = property(getb, setb) + + def setrgb(self, r, g, b) -> None: + self.r, self.g, self.b = r, g, b + + @rgb2hex + def getrgb(self): + return (self.r, self.g, self.b) + + +rgb = RGB() +print(f"rgb.r={rgb.r}") +rgb.setrgb(0xFF, 0x1, 0xFF) +print(f"rgb.getrgb(): {rgb.getrgb()}") +print() + + +@decor +def foo(a, b) -> None: + print(f"{a} ^ {b} = {(a ** b)}") + + +foo(2, 3) +foo(b=3, a=2) diff --git a/Decorators__examples/hello_world.py b/Decorators__examples/hello_world.py index bcef31853..38ae2dfa6 100644 --- a/Decorators__examples/hello_world.py +++ b/Decorators__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import functools @@ -38,7 +38,7 @@ def hello(text): return text -print(hello('Hello World!')) +print(hello("Hello World!")) # HELLO WORLD! -assert hello('Hello World!') == 'HELLO WORLD!' +assert hello("Hello World!") == "HELLO WORLD!" diff --git a/Decorators__examples/hello_world__async.py b/Decorators__examples/hello_world__async.py new file mode 100644 index 000000000..69c05d92d --- /dev/null +++ b/Decorators__examples/hello_world__async.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import asyncio +import functools + + +def makebold(func): + @functools.wraps(func) + async def wrapped(*args, **kwargs): + return "" + await func(*args, **kwargs) + "" + + return wrapped + + +def makeitalic(func): + @functools.wraps(func) + async def wrapped(*args, **kwargs): + return "" + await func(*args, **kwargs) + "" + + return wrapped + + +def upper(func): + @functools.wraps(func) + async def wrapped(*args, **kwargs): + return (await func(*args, **kwargs)).upper() + + return wrapped + + +@makebold +@makeitalic +@upper +async def hello(text): + return text + + +loop = asyncio.new_event_loop() + +print(loop.run_until_complete(hello("Hello World!"))) +# HELLO WORLD! + +assert loop.run_until_complete(hello("Hello World!")) == "HELLO WORLD!" diff --git a/Decorators__examples/memoize_class.py b/Decorators__examples/memoize_class.py index 0122fb33c..3654b2225 100644 --- a/Decorators__examples/memoize_class.py +++ b/Decorators__examples/memoize_class.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Using memoization as decorator (decorator-class) class MemoizeClass: - def __init__(self, func): + def __init__(self, func) -> None: self.func = func self.memo = dict() diff --git a/Decorators__examples/memoize_func.py b/Decorators__examples/memoize_func.py index e66a3095b..d2494ede1 100644 --- a/Decorators__examples/memoize_func.py +++ b/Decorators__examples/memoize_func.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Using memoization as decorator (decorator-function) diff --git a/Decorators__examples/requests__append_attempts.py b/Decorators__examples/requests__append_attempts.py new file mode 100644 index 000000000..2919a37a8 --- /dev/null +++ b/Decorators__examples/requests__append_attempts.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import functools +import time + +import requests +from requests.exceptions import RequestException + + +def attempts( + max_number: int = 5, + sleep: int = 30, + ignored_exceptions: tuple[type(Exception)] = (Exception,), +): + def actual_decorator(func): + @functools.wraps(func) + def wrapped(*args, **kwargs): + number = 0 + while True: + try: + print("\nGO", args, kwargs) + return func(*args, **kwargs) + except Exception as e: + number += 1 + print(f"ERROR on {number}/{max_number}: {e}") + + if number >= max_number or not isinstance(e, ignored_exceptions): + raise e + + print(f"Sleep {sleep} seconds") + time.sleep(sleep) + + return wrapped + + return actual_decorator + + +@attempts( + max_number=3, + sleep=5, + ignored_exceptions=(RequestException,), +) +def do_get(url: str, *args, **kwargs) -> requests.Response: + rs = requests.get(url, *args, **kwargs) + rs.raise_for_status() + + return rs + + +print(do_get("https://google.com")) +""" +GO ('https://google.com',) {} + +""" + +print(do_get("http://sdfsdfs.dfsdf")) +""" +GO ('http://sdfsdfs.dfsdf',) {} +ERROR on 1/3: HTTPConnectionPool(host='sdfsdfs.dfsdf', port=80): Max retries exceeded with url: / (Caused by NewConnectionError(': Failed to establish a new connection: [Errno 11001] getaddrinfo failed')) +Sleep 5 seconds + +GO ('http://sdfsdfs.dfsdf',) {} +ERROR on 2/3: HTTPConnectionPool(host='sdfsdfs.dfsdf', port=80): Max retries exceeded with url: / (Caused by NewConnectionError(': Failed to establish a new connection: [Errno 11001] getaddrinfo failed')) +Sleep 5 seconds + +GO ('http://sdfsdfs.dfsdf',) {} +ERROR on 3/3: HTTPConnectionPool(host='sdfsdfs.dfsdf', port=80): Max retries exceeded with url: / (Caused by NewConnectionError(': Failed to establish a new connection: [Errno 11001] getaddrinfo failed')) +Traceback (most recent call last): + ... +socket.gaierror: [Errno 11001] getaddrinfo failed + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + ... +urllib3.exceptions.NewConnectionError: : Failed to establish a new connection: [Errno 11001] getaddrinfo failed + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + ... +urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='sdfsdfs.dfsdf', port=80): Max retries exceeded with url: / (Caused by NewConnectionError(': Failed to establish a new connection: [Errno 11001] getaddrinfo failed')) + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + ... + raise ConnectionError(e, request=request) +requests.exceptions.ConnectionError: HTTPConnectionPool(host='sdfsdfs.dfsdf', port=80): Max retries exceeded with url: / (Caused by NewConnectionError(': Failed to establish a new connection: [Errno 11001] getaddrinfo failed')) +""" diff --git a/Decorators__examples/thread.py b/Decorators__examples/thread.py index 6002a9718..70426ff29 100644 --- a/Decorators__examples/thread.py +++ b/Decorators__examples/thread.py @@ -1,32 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import time from threading import Thread def thread(my_func): - def wrapper(*args, **kwargs): + def wrapper(*args, **kwargs) -> None: my_thread = Thread(target=my_func, args=args, kwargs=kwargs) my_thread.start() + return wrapper -if __name__ == '__main__': +if __name__ == "__main__": @thread - def _print_and_sleep(timeout=2): - print('start. print_and_sleep') - import time + def _print_and_sleep(timeout=2) -> None: + print("start. print_and_sleep") + time.sleep(timeout) - print('finish. print_and_sleep') + print("finish. print_and_sleep") @thread - def _print_loop(name, max_num=10): - print('start. _print_loop') + def _print_loop(name, max_num=10) -> None: + print("start. _print_loop") - import time i = 0 while True: @@ -37,9 +38,9 @@ def _print_loop(name, max_num=10): if i == max_num: break - print('finish. _print_loop') + print("finish. _print_loop") _print_and_sleep() _print_and_sleep(4) _print_and_sleep() - _print_loop(' loop') + _print_loop(" loop") diff --git a/Decorators__examples/timer.py b/Decorators__examples/timer.py index 8e2642c7f..c441a32ad 100644 --- a/Decorators__examples/timer.py +++ b/Decorators__examples/timer.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import time @@ -13,12 +13,13 @@ def wrapper(*args, **kwargs): r = f(*args, **kwargs) print("Время выполнения функции: %f сек." % (time.time() - t)) return r + return wrapper -if __name__ == '__main__': +if __name__ == "__main__": @timer - def my_sleep(): + def my_sleep() -> None: print(123) time.sleep(0.3) print(456) @@ -29,6 +30,6 @@ def my_sleep(): @timer def my_foo(): - return [i for i in range(10 ** 7) if i % 2 == 0] + return [i for i in range(10**7) if i % 2 == 0] print(my_foo()[:20]) diff --git a/Decorators__examples/try_repeat.py b/Decorators__examples/try_repeat.py index ca2e834ca..a2bc6a507 100644 --- a/Decorators__examples/try_repeat.py +++ b/Decorators__examples/try_repeat.py @@ -1,11 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import random # http://ru.stackoverflow.com/a/491681/201445 + def try_repeat(func): def wrapper(*args, **kwargs): count = 10 @@ -14,7 +18,7 @@ def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: - print('Error:', e) + print("Error:", e) count -= 1 return wrapper @@ -22,9 +26,8 @@ def wrapper(*args, **kwargs): @try_repeat def exception_func(): - import random if random.randint(0, 1): - raise Exception('!!!') + raise Exception("!!!") exception_func() diff --git "a/Demons Souls/demonssouls_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/main.py" "b/Demons Souls/demonssouls_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/main.py" deleted file mode 100644 index 76f4d9771..000000000 --- "a/Demons Souls/demonssouls_fandom_com__ru__wiki__\320\221\320\276\321\201\321\201\321\213/main.py" +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from collections import defaultdict -import json -import os -from typing import Dict, List, NamedTuple -from urllib.parse import urljoin - -from bs4 import BeautifulSoup -import requests - - -class Boss(NamedTuple): - name: str - url: str - - -def get_bosses(url: str) -> Dict[str, List[Boss]]: - rs = requests.get(url) - root = BeautifulSoup(rs.content, 'html.parser') - - bosses_by_category = defaultdict(list) - - category_name = None - - for tr in root.select('table.article-table > tr'): - # Заголовок первым идет - th = tr.select_one('th') - if th: - category_name = th.text.strip().upper() - continue - - if not category_name: - continue - - td_list = [] - - for a in tr.select('td table a[href]'): - name = a.get_text(strip=True) - if not name: - continue - - url = urljoin(rs.url, a['href']) - boss = Boss(name, url) - - td_list.append(boss) - - bosses_by_category[category_name] += td_list - - return bosses_by_category - - -def print_bosses(url: str, bosses: Dict[str, List[Boss]]): - print('{} ({}):'.format(url, sum(len(i) for i in bosses.values()))) - - for category, bosses in bosses.items(): - print('{} ({}):'.format(category, len(bosses))) - - for i, boss in enumerate(bosses, 1): - print(' {}. "{}": {}'.format(i, boss.name, boss.url)) - - print() - - print() - - -def convert_bosses_to_only_name(bosses: Dict[str, List[Boss]]) -> Dict[str, List[str]]: - bosses_only_name = dict() - for category, bosses_list in bosses.items(): - bosses_only_name[category] = [boss.name for boss in bosses_list] - - return bosses_only_name - - -def export_to_json(file_name, bosses): - dir_name = os.path.dirname(file_name) - os.makedirs(dir_name, exist_ok=True) - - json.dump(bosses, open(file_name, 'w', encoding='utf-8'), ensure_ascii=False, indent=4) - - -def export_to_simple_text(file_name, bosses): - dir_name = os.path.dirname(file_name) - os.makedirs(dir_name, exist_ok=True) - - with open(file_name, 'w', encoding='utf-8') as f: - num = 0 - - for category, bosses_list in bosses.items(): - num += 1 - if num > 1: - f.write('\n') - - f.write(category + '\n') - - for boss in bosses_list: - f.write(f' {boss.name}\n') - - -if __name__ == '__main__': - url = 'https://demonssouls.fandom.com/ru/wiki/Боссы' - bosses = get_bosses(url) - print('Total bosses:', sum(len(i) for i in bosses.values())) - # Total bosses: 21 - - print() - - print_bosses(url, bosses) - # https://demonssouls.fandom.com/ru/wiki/Боссы (21): - # ОБЯЗАТЕЛЬНЫЕ БОССЫ (17): - # 1. "Фаланга": https://demonssouls.fandom.com/ru/wiki/%D0%A4%D0%B0%D0%BB%D0%B0%D0%BD%D0%B3%D0%B0 - # 2. "Рыцарь башни": https://demonssouls.fandom.com/ru/wiki/%D0%A0%D1%8B%D1%86%D0%B0%D1%80%D1%8C_%D0%B1%D0%B0%D1%88%D0%BD%D0%B8 - # 3. "Пронзающий": https://demonssouls.fandom.com/ru/wiki/%D0%9F%D1%80%D0%BE%D0%BD%D0%B7%D0%B0%D1%8E%D1%89%D0%B8%D0%B9 - # 4. "Старый король Аллант": https://demonssouls.fandom.com/ru/wiki/%D0%A1%D1%82%D0%B0%D1%80%D1%8B%D0%B9_%D0%BA%D0%BE%D1%80%D0%BE%D0%BB%D1%8C_%D0%90%D0%BB%D0%BB%D0%B0%D0%BD%D1%82 - # 5. "Стальной паук": https://demonssouls.fandom.com/ru/wiki/%D0%A1%D1%82%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B9_%D0%BF%D0%B0%D1%83%D0%BA - # 6. "Огненный Соглядатай": https://demonssouls.fandom.com/ru/wiki/%D0%9E%D0%B3%D0%BD%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9_%D0%A1%D0%BE%D0%B3%D0%BB%D1%8F%D0%B4%D0%B0%D1%82%D0%B0%D0%B9 - # 7. "Бог драконов": https://demonssouls.fandom.com/ru/wiki/%D0%91%D0%BE%D0%B3_%D0%B4%D1%80%D0%B0%D0%BA%D0%BE%D0%BD%D0%BE%D0%B2 - # 8. "Ложный Идол": https://demonssouls.fandom.com/ru/wiki/%D0%9B%D0%BE%D0%B6%D0%BD%D1%8B%D0%B9_%D0%98%D0%B4%D0%BE%D0%BB - # 9. "Людоед": https://demonssouls.fandom.com/ru/wiki/%D0%9B%D1%8E%D0%B4%D0%BE%D0%B5%D0%B4 - # 10. "Старый Монах": https://demonssouls.fandom.com/ru/wiki/%D0%A1%D1%82%D0%B0%D1%80%D1%8B%D0%B9_%D0%9C%D0%BE%D0%BD%D0%B0%D1%85 - # 11. "Судья": https://demonssouls.fandom.com/ru/wiki/%D0%A1%D1%83%D0%B4%D1%8C%D1%8F - # 12. "Старый герой": https://demonssouls.fandom.com/ru/wiki/%D0%A1%D1%82%D0%B0%D1%80%D1%8B%D0%B9_%D0%B3%D0%B5%D1%80%D0%BE%D0%B9 - # 13. "Властитель Бурь (босс)": https://demonssouls.fandom.com/ru/wiki/%D0%92%D0%BB%D0%B0%D1%81%D1%82%D0%B8%D1%82%D0%B5%D0%BB%D1%8C_%D0%91%D1%83%D1%80%D1%8C_(%D0%B1%D0%BE%D1%81%D1%81) - # 14. "Торговец пиявками": https://demonssouls.fandom.com/ru/wiki/%D0%A2%D0%BE%D1%80%D0%B3%D0%BE%D0%B2%D0%B5%D1%86_%D0%BF%D0%B8%D1%8F%D0%B2%D0%BA%D0%B0%D0%BC%D0%B8 - # 15. "Грязный Колосс": https://demonssouls.fandom.com/ru/wiki/%D0%93%D1%80%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9_%D0%9A%D0%BE%D0%BB%D0%BE%D1%81%D1%81 - # 16. "Дева Астрея": https://demonssouls.fandom.com/ru/wiki/%D0%94%D0%B5%D0%B2%D0%B0_%D0%90%D1%81%D1%82%D1%80%D0%B5%D1%8F - # 17. "Король Аллант": https://demonssouls.fandom.com/ru/wiki/%D0%9A%D0%BE%D1%80%D0%BE%D0%BB%D1%8C_%D0%90%D0%BB%D0%BB%D0%B0%D0%BD%D1%82 - # - # ОПЦИОНАЛЬНЫЕ БОССЫ (4): - # 1. "Авангард": https://demonssouls.fandom.com/ru/wiki/%D0%90%D0%B2%D0%B0%D0%BD%D0%B3%D0%B0%D1%80%D0%B4 - # 2. "Красный дракон": https://demonssouls.fandom.com/ru/wiki/%D0%9A%D1%80%D0%B0%D1%81%D0%BD%D1%8B%D0%B9_%D0%B4%D1%80%D0%B0%D0%BA%D0%BE%D0%BD - # 3. "Синий дракон": https://demonssouls.fandom.com/ru/wiki/%D0%A1%D0%B8%D0%BD%D0%B8%D0%B9_%D0%B4%D1%80%D0%B0%D0%BA%D0%BE%D0%BD - # 4. "Первобытный демон": https://demonssouls.fandom.com/ru/wiki/%D0%9F%D0%B5%D1%80%D0%B2%D0%BE%D0%B1%D1%8B%D1%82%D0%BD%D1%8B%D0%B9_%D0%B4%D0%B5%D0%BC%D0%BE%D0%BD - - export_to_json('dumps/bosses.json', bosses) - export_to_json('dumps/bosses__only_name.json', convert_bosses_to_only_name(bosses)) - export_to_simple_text('dumps/bosses__only_name.txt', bosses) diff --git "a/Demons Souls/demonssouls_fandom_com__ru__wiki__\320\232\320\273\320\260\321\201\321\201\321\213.py" "b/Demons Souls/demonssouls_fandom_com__ru__wiki__\320\232\320\273\320\260\321\201\321\201\321\213.py" deleted file mode 100644 index af3cf673a..000000000 --- "a/Demons Souls/demonssouls_fandom_com__ru__wiki__\320\232\320\273\320\260\321\201\321\201\321\213.py" +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import Dict - -from bs4 import BeautifulSoup -import requests - - -def get_class_by_stats() -> Dict[str, Dict[str, int]]: - url = 'https://demonssouls.fandom.com/ru/wiki/Классы' - - rs = requests.get(url) - root = BeautifulSoup(rs.content, 'html.parser') - - class_by_stats = dict() - - for tr in root.select('table > tr'): - td_list = [td.get_text(strip=True) for td in tr.select('td')] - if not td_list: - continue - - class_name = td_list[0] - stats = list(map(int, td_list[1:])) - - class_by_stats[class_name] = { - "Жизненная сила": stats[0], - "Интеллект": stats[1], - "Стойкость": stats[2], - "Сила": stats[3], - "Ловкость": stats[4], - "Магия": stats[5], - "Вера": stats[6], - "Удача": stats[7], - "Уровень души": stats[8], - } - - return class_by_stats - - -if __name__ == '__main__': - class_by_stats = get_class_by_stats() - print(class_by_stats) - - print() - - for class_name, stats in sorted(class_by_stats.items(), key=lambda x: x[1]["Уровень души"]): - total_stats = sum(list(stats.values())[:-1]) - print(f'{class_name} (level={stats["Уровень души"]}), total stats: {total_stats}') - # Дворянин (level=1), total stats: 81 - # Рыцарь (level=4), total stats: 84 - # Храмовник (level=4), total stats: 84 - # Солдат (level=6), total stats: 86 - # Охотник (level=6), total stats: 86 - # Жрец (level=6), total stats: 86 - # Маг (level=6), total stats: 86 - # Странник (level=6), total stats: 86 - # Варвар (level=9), total stats: 89 - # Вор (level=9), total stats: 89 - - print() - - for class_name, stats in class_by_stats.items(): - print(f'{class_name}: {stats}') - # Солдат: {'Жизненная сила': 14, 'Интеллект': 9, 'Стойкость': 12, 'Сила': 12, 'Ловкость': 11, 'Магия': 8, 'Вера': 10, 'Удача': 10, 'Уровень души': 6} - # Рыцарь: {'Жизненная сила': 10, 'Интеллект': 11, 'Стойкость': 11, 'Сила': 14, 'Ловкость': 10, 'Магия': 10, 'Вера': 11, 'Удача': 7, 'Уровень души': 4} - # Охотник: {'Жизненная сила': 12, 'Интеллект': 10, 'Стойкость': 13, 'Сила': 11, 'Ловкость': 12, 'Магия': 8, 'Вера': 8, 'Удача': 12, 'Уровень души': 6} - # Жрец: {'Жизненная сила': 13, 'Интеллект': 11, 'Стойкость': 12, 'Сила': 13, 'Ловкость': 8, 'Магия': 8, 'Вера': 13, 'Удача': 8, 'Уровень души': 6} - # Маг: {'Жизненная сила': 9, 'Интеллект': 15, 'Стойкость': 10, 'Сила': 9, 'Ловкость': 11, 'Магия': 15, 'Вера': 6, 'Удача': 11, 'Уровень души': 6} - # Странник: {'Жизненная сила': 10, 'Интеллект': 10, 'Стойкость': 11, 'Сила': 11, 'Ловкость': 15, 'Магия': 9, 'Вера': 7, 'Удача': 13, 'Уровень души': 6} - # Варвар: {'Жизненная сила': 15, 'Интеллект': 7, 'Стойкость': 13, 'Сила': 15, 'Ловкость': 9, 'Магия': 11, 'Вера': 8, 'Удача': 11, 'Уровень души': 9} - # Вор: {'Жизненная сила': 10, 'Интеллект': 13, 'Стойкость': 10, 'Сила': 9, 'Ловкость': 14, 'Магия': 10, 'Вера': 8, 'Удача': 15, 'Уровень души': 9} - # Храмовник: {'Жизненная сила': 11, 'Интеллект': 8, 'Стойкость': 13, 'Сила': 14, 'Ловкость': 12, 'Магия': 6, 'Вера': 13, 'Удача': 7, 'Уровень души': 4} - # Дворянин: {'Жизненная сила': 8, 'Интеллект': 12, 'Стойкость': 8, 'Сила': 9, 'Ловкость': 12, 'Магия': 13, 'Вера': 12, 'Удача': 7, 'Уровень души': 1} diff --git a/DownloadURL/DownloadURL.py b/DownloadURL/DownloadURL.py deleted file mode 100644 index 8b55f0b98..000000000 --- a/DownloadURL/DownloadURL.py +++ /dev/null @@ -1,18 +0,0 @@ -# coding=utf-8 - -import argparse -import urllib2 - -__author__ = 'ipetrash' - - -def create_parser(): - parser = argparse.ArgumentParser(description="Download content URL.") - return parser - -if __name__ == '__main__': - create_parser().parse_args() - url = raw_input("Input url: ") - file = urllib2.urlopen(url) - content = file.read() - print(content) \ No newline at end of file diff --git a/DownloadURL_CLI/DownloadURL.py b/DownloadURL_CLI/DownloadURL.py new file mode 100644 index 000000000..df6fb3943 --- /dev/null +++ b/DownloadURL_CLI/DownloadURL.py @@ -0,0 +1,20 @@ +# coding=utf-8 + +__author__ = "ipetrash" + + +import argparse +from urllib.request import urlopen + + +def create_parser(): + parser = argparse.ArgumentParser(description="Download content URL.") + return parser + + +if __name__ == "__main__": + create_parser().parse_args() + url = input("Input url: ") + file = urlopen(url) + content = file.read() + print(content) diff --git a/DownloadURL/README.md b/DownloadURL_CLI/README.md similarity index 100% rename from DownloadURL/README.md rename to DownloadURL_CLI/README.md diff --git a/ELEX__hacking_minigame.py b/ELEX__hacking_minigame.py deleted file mode 100644 index 36d620d4b..000000000 --- a/ELEX__hacking_minigame.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import random - - -# Этот шаблон допускает любые числа -_ = '*' -keys = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] -fixed_values = [_, _, _, _] -need_values = [] - -keys = [1, 2, 4, 5, 7, 8, 9] -keys = [0, 1, 6, 8] -keys = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - -eval_template = 'a == b and (a > c < d)' -eval_template = 'a > b > c < d' -eval_template = 'a > b < c > d' - -fixed_values = [_, _, _, _] -fixed_values = [_, 1, 8, _] - -need_values = [4] - -while True: - items = keys.copy() - fixed_items = fixed_values.copy() - need_items = need_values.copy() - - # Удаляем дублирующие значения - for fixed in fixed_items: - if fixed in items: - items.remove(fixed) - - if fixed in need_items: - need_items.remove(fixed) - - random.shuffle(items) - random.shuffle(need_items) - - free_idx = [i for i, x in enumerate(fixed_items) if x == _] - random.shuffle(free_idx) - - for i in free_idx: - # Если есть обязательные значения, то берем из них - if need_items: - fixed_items[i] = need_items.pop() - else: - fixed_items[i] = items.pop() - - a, b, c, d = fixed_items - - text = eval_template\ - .replace('a', str(a)).replace('b', str(b))\ - .replace('c', str(c)).replace('d', str(d)) - - ok = eval(text) - if ok: - print(text) - break diff --git a/Elden Ring/common.py b/Elden Ring/common.py deleted file mode 100644 index 237a793fc..000000000 --- a/Elden Ring/common.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import requests -from bs4 import BeautifulSoup - - -session = requests.Session() -session.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0' - - -def parse(url: str) -> tuple[requests.Response, BeautifulSoup]: - rs = session.get(url) - return rs, BeautifulSoup(rs.content, 'html.parser') - diff --git a/EncodeStringExample/EncodeStringExample.py b/EncodeStringExample/EncodeStringExample.py index 502e11169..75f52c63b 100644 --- a/EncodeStringExample/EncodeStringExample.py +++ b/EncodeStringExample/EncodeStringExample.py @@ -1,20 +1,27 @@ # coding=utf-8 import argparse -__author__ = 'ipetrash' +__author__ = "ipetrash" def create_parser(): - parse = argparse.ArgumentParser(description="Output a user-defined string in different encodings.") + parse = argparse.ArgumentParser( + description="Output a user-defined string in different encodings." + ) return parse -if __name__ == '__main__': +if __name__ == "__main__": create_parser().parse_args() - string = raw_input("text = ") - uni = unicode(string) - - list_encoding = {"windows-1251", "UTF-16", "UTF-16LE", "UTF-16BE", "ASCII", "Latin-1"} + string = input("text = ") + list_encoding = { + "windows-1251", + "UTF-16", + "UTF-16LE", + "UTF-16BE", + "ASCII", + "Latin-1", + } for encoding in list_encoding: - print(encoding + ": " + uni.encode(encoding)) \ No newline at end of file + print(f"{encoding}: {string.encode(encoding)}") diff --git a/Environment variables/EnvironmentVariables.py b/Environment variables/EnvironmentVariables.py index 07d2922b9..f4537ae9c 100644 --- a/Environment variables/EnvironmentVariables.py +++ b/Environment variables/EnvironmentVariables.py @@ -1,33 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os # Вывести все переменные окружения (environment variables) -print("Environment variables:\n{}".format(os.environ)) +print(f"Environment variables:\n{os.environ}") print("\nEnvironment variables:") for var, value in os.environ.items(): - print("'{}': '{}'".format(var, value)) + print(f"'{var}': '{value}'") # # Получение значения переменной окружения Path env_path = os.environ["Path"] # или os.environ.get("Path") # Вывести значение переменной окружения Path -print("\nPath: {}".format(env_path)) +print(f"\nPath: {env_path}") # Разделение строки с путями на список values_env_path = env_path.split(";") -print("Path: {}".format(values_env_path)) # Вывод списка +print(f"Path: {values_env_path}") # Вывод списка # Вывод списка путей print("Path:") for i, val in enumerate(values_env_path, 1): # Если элемент не пустой if val: - print("{}. '{}'".format(i, val) + (": not exist" if not os.path.exists(val) else "")) + print(f"{i}. '{val}'" + (": not exist" if not os.path.exists(val) else "")) diff --git a/Environment variables/expand_env__APPDATA.py b/Environment variables/expand_env__APPDATA.py index 372a7ab65..d8a39639c 100644 --- a/Environment variables/expand_env__APPDATA.py +++ b/Environment variables/expand_env__APPDATA.py @@ -1,19 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # https://ru.wikipedia.org/wiki/Переменная_среды_Windows import os -app_data = os.path.expandvars('%APPDATA%') -print(app_data) + +app_data = os.path.expandvars("%APPDATA%") +print(app_data) print() -print(os.path.expandvars('%OS%')) -print(os.path.expandvars('%COMPUTERNAME%')) -print(os.path.expandvars('%WINDIR%')) + +print(os.path.expandvars("%OS%")) +print(os.path.expandvars("%COMPUTERNAME%")) +print(os.path.expandvars("%WINDIR%")) print() -print(os.path.expandvars('%NUMBER_OF_PROCESSORS%')) -print(os.path.expandvars('%PROCESSOR_ARCHITECTURE%')) + +print(os.path.expandvars("%NUMBER_OF_PROCESSORS%")) +print(os.path.expandvars("%PROCESSOR_ARCHITECTURE%")) diff --git a/EscapePyPromptLineString/main.py b/EscapePyPromptLineString/main.py index 92e87e64d..4555e837e 100644 --- a/EscapePyPromptLineString/main.py +++ b/EscapePyPromptLineString/main.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import traceback @@ -23,12 +23,12 @@ from PySide.QtCore import * -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) @@ -36,10 +36,10 @@ def log_uncaught_exceptions(ex_cls, ex, tb): class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('EscapePyPromptLineString') + self.setWindowTitle("EscapePyPromptLineString") self.text_edit_input = QPlainTextEdit() @@ -51,9 +51,9 @@ def __init__(self): self.label_error.setTextInteractionFlags(Qt.TextSelectableByMouse) self.label_error.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.button_detail_error = QPushButton('...') + self.button_detail_error = QPushButton("...") self.button_detail_error.setFixedSize(20, 20) - self.button_detail_error.setToolTip('Detail error') + self.button_detail_error.setToolTip("Detail error") self.button_detail_error.clicked.connect(self.show_detail_error_massage) self.last_error_message = None @@ -77,11 +77,11 @@ def __init__(self): self.setLayout(layout) - def show_detail_error_massage(self): - message = self.last_error_message + '\n\n' + self.last_detail_error_message + def show_detail_error_massage(self) -> None: + message = self.last_error_message + "\n\n" + self.last_detail_error_message mb = QErrorMessage() - mb.setWindowTitle('Error') + mb.setWindowTitle("Error") # Сообщение ошибки содержит отступы, символы-переходы на следующую строку, # которые поломаются при вставке через QErrorMessage.showMessage, и нет возможности # выбрать тип текста, то делаем такой хак. @@ -89,7 +89,7 @@ def show_detail_error_massage(self): mb.exec_() - def input_text_changed(self): + def input_text_changed(self) -> None: self.label_error.clear() self.button_detail_error.hide() @@ -104,15 +104,15 @@ def input_text_changed(self): new_out_text = [] for line in out_text.splitlines(): - if line.startswith('>>> ') or line.startswith('... ') or line == '...': + if line.startswith(">>> ") or line.startswith("... ") or line == "...": line = line[4:] new_out_text.append(line) - out_text = '\n'.join(new_out_text) + out_text = "\n".join(new_out_text) self.text_edit_output.setPlainText(out_text) - print('Escape for {:.3f} secs'.format(time.perf_counter() - t)) + print(f"Escape for {time.perf_counter() - t:.3f} secs") except Exception as e: # Выводим ошибку в консоль @@ -125,15 +125,16 @@ def input_text_changed(self): self.last_detail_error_message = str(tb) self.button_detail_error.show() - self.label_error.setText('Error: ' + self.last_error_message) + self.label_error.setText("Error: " + self.last_error_message) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() mw.resize(650, 500) - mw.text_edit_input.setPlainText("""\ + mw.text_edit_input.setPlainText( + """\ >>> import bcrypt >>> password = b"super secret password" >>> # Hash a password for the first time, with a randomly-generated salt diff --git a/EscapeString/main.py b/EscapeString/main.py index 81c9add9b..c6b7903dd 100644 --- a/EscapeString/main.py +++ b/EscapeString/main.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import time @@ -23,12 +23,12 @@ from PySide.QtCore import * -def log_uncaught_exceptions(ex_cls, ex, tb): - text = f'{ex_cls.__name__}: {ex}:\n' - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) @@ -37,17 +37,17 @@ def log_uncaught_exceptions(ex_cls, ex, tb): class MainWindow(QWidget): ESCAPE_RULES = [ - ('"', '"', r'\"', True), - ("'", "'", r"\'", True), - (r'\n', "\n", r'\n', True), - (r'\t', "\t", r'\t', True), - (r'\b', "\b", r'\b', True), - (r'\f', "\f", r'\f', False), - ('\\', "\\", '\\\\', False), + ('"', '"', r"\"", True), + ("'", "'", r"\'", False), + (r"\n", "\n", r"\n", True), + (r"\t", "\t", r"\t", False), + (r"\b", "\b", r"\b", False), + (r"\f", "\f", r"\f", False), + ("\\", "\\", "\\\\", False), ] - TITLE = 'EscapeString' + TITLE = "EscapeString" - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self.setWindowTitle(self.TITLE) @@ -72,16 +72,21 @@ def __init__(self, parent=None): button_layout.addStretch() - self.cb_save_multiline = QCheckBox('Save multiline') + self.cb_save_multiline = QCheckBox("Save multiline") self.cb_save_multiline.setChecked(False) self.cb_save_multiline.clicked.connect(self.input_text_changed) button_layout.addWidget(self.cb_save_multiline) - self.cb_string_literal = QCheckBox('String literal') + self.cb_string_literal = QCheckBox("String literal") self.cb_string_literal.setChecked(True) self.cb_string_literal.clicked.connect(self.input_text_changed) button_layout.addWidget(self.cb_string_literal) + self.cb_semicolon = QCheckBox(";") + self.cb_semicolon.setChecked(True) + self.cb_semicolon.clicked.connect(self.input_text_changed) + button_layout.addWidget(self.cb_semicolon) + layout = QVBoxLayout() layout.addLayout(button_layout) @@ -95,9 +100,9 @@ def __init__(self, parent=None): self.label_error.setTextInteractionFlags(Qt.TextSelectableByMouse) self.label_error.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.button_detail_error = QPushButton('...') + self.button_detail_error = QPushButton("...") self.button_detail_error.setFixedSize(20, 20) - self.button_detail_error.setToolTip('Detail error') + self.button_detail_error.setToolTip("Detail error") self.button_detail_error.clicked.connect(self.show_detail_error_massage) self.button_detail_error.hide() @@ -124,14 +129,14 @@ def __init__(self, parent=None): self.setLayout(layout) - def show_detail_error_massage(self): + def show_detail_error_massage(self) -> None: if not self.last_error_message or not self.last_detail_error_message: return - message = self.last_error_message + '\n\n' + self.last_detail_error_message + message = self.last_error_message + "\n\n" + self.last_detail_error_message mb = QErrorMessage() - mb.setWindowTitle('Error') + mb.setWindowTitle("Error") # Сообщение ошибки содержит отступы, символы-переходы на следующую строку, # которые поломаются при вставке через QErrorMessage.showMessage, и нет возможности # выбрать тип текста, то делаем такой хак. @@ -139,22 +144,24 @@ def show_detail_error_massage(self): mb.exec_() - def _escape(self, in_text: str, ignored='') -> str: + def _escape(self, in_text: str, ignored="") -> str: out_text = [] for char in in_text: # Если char нет в словаре экранирования, или флаг экранирования для char выключен, # или char среди игнорируемых - if char not in self.escape_char_by_checkbox \ - or not self.escape_char_by_checkbox[char].isChecked()\ - or char in ignored: + if ( + char not in self.escape_char_by_checkbox + or not self.escape_char_by_checkbox[char].isChecked() + or char in ignored + ): out_text.append(char) continue out_text.append(self.char_by_escape[char]) - return ''.join(out_text) + return "".join(out_text) - def input_text_changed(self): + def input_text_changed(self) -> None: self.label_error.clear() self.button_detail_error.hide() @@ -168,11 +175,11 @@ def input_text_changed(self): if self.cb_save_multiline.isChecked(): lines = [] - for line in in_text.splitlines(): + for line in in_text.splitlines(keepends=True): line = self._escape(line) lines.append(f'"{line}"') - out_text = ' +\n'.join(lines) + out_text = " +\n".join(lines) else: out_text = self._escape(in_text) @@ -182,16 +189,18 @@ def input_text_changed(self): out_text = '"' + out_text if not out_text.endswith('"'): out_text += '"' - out_text += ';' + + if self.cb_semicolon.isChecked(): + out_text += ";" self.text_edit_output.setPlainText(out_text) self.setWindowTitle( - f'{self.TITLE} (number of characters: ' - f'{len(self.text_edit_input.toPlainText())} -> ' - f'{len(self.text_edit_output.toPlainText())})' + f"{self.TITLE} (number of characters: " + f"{len(self.text_edit_input.toPlainText())} -> " + f"{len(self.text_edit_output.toPlainText())})" ) - print('Escape for {:.6f} secs'.format(time.perf_counter() - t)) + print(f"Escape for {time.perf_counter() - t:.6f} secs") except Exception as e: # Выводим ошибку в консоль @@ -204,10 +213,10 @@ def input_text_changed(self): self.last_detail_error_message = str(tb) self.button_detail_error.show() - self.label_error.setText('Error: ' + self.last_error_message) + self.label_error.setText("Error: " + self.last_error_message) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/FFXIII-2_clock_puzzle_solver/main.py b/FFXIII-2_clock_puzzle_solver/main.py deleted file mode 100644 index a281afc3f..000000000 --- a/FFXIII-2_clock_puzzle_solver/main.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# FFXIII-2 clock puzzle solver - - -from copy import deepcopy - - -def is_win(clock_data: dict) -> bool: - return all(x == 0 for x in clock_data['items']) - - -def get_arrows_value(clock_data: dict) -> (int, int): - items = clock_data['items'] - arrow_1 = clock_data['left_arrow_index'] - arrow_2 = clock_data['right_arrow_index'] - - return items[arrow_1], items[arrow_2] - - -def is_fail(clock_data: dict) -> bool: - a, b = get_arrows_value(clock_data) - return a == 0 and b == 0 - - -def click_clock_item(clock_data: dict, selected_index: int): - items = clock_data['items'] - move_value = items[selected_index] - - clock_data['selected_indexes'].append(selected_index) - clock_data['selected_values'].append(move_value) - - # После клика кнопка теперь не активна - items[selected_index] = 0 - - # Остаток по модулю поможет получить индекс после вращения стрелки по часовой или против - left_arrow_index = (selected_index - move_value) % len(items) - right_arrow_index = (selected_index + move_value) % len(items) - - clock_data['left_arrow_index'] = left_arrow_index - clock_data['right_arrow_index'] = right_arrow_index - - # После клика, часы изменились, поэтому нужно сохранить это в истории - clock_data['history'].append(deepcopy(clock_data['items'])) - - -def solve_step(clock_data: dict, index: int, result_win: dict): - # Если текущий индекс указывает на уже активированную кнопку - if clock_data['items'][index] == 0: - return - - # Т.к. этот объект используется в рекурсии, нужно делать его копии - # чтобы изменения в одной функции рекурсии не повлияло на другую - clock_data = deepcopy(clock_data) - - click_clock_item(clock_data, index) - - if is_win(clock_data): - # Для удаления дубликатов выбранные индексы преобразуем в ключ для словаря - key = ','.join(map(str, clock_data['selected_indexes'])) - - result_win[key] = clock_data - return - - if is_fail(clock_data): - return - - arrow_1 = clock_data['left_arrow_index'] - solve_step(clock_data, arrow_1, result_win) - - arrow_2 = clock_data['right_arrow_index'] - solve_step(clock_data, arrow_2, result_win) - - -def solver(clock_items: list) -> list: - clock_data = { - 'items': deepcopy(clock_items), - 'history': [deepcopy(clock_items)], - 'left_arrow_index': 0, - 'right_arrow_index': 0, - 'selected_indexes': [], - 'selected_values': [], - } - - wins = dict() - - items = clock_data['items'] - for index in range(len(items)): - solve_step(clock_data, index, wins) - - return list(wins.values()) - - -def print_extended(clock_data: dict): - num_clock_items = len(clock_data['selected_indexes']) - - # Форматирование строки для красивого отображения двухзначных индексов - fmt_print = '{0} -> #{1:<%d} ({2})' % (2 if num_clock_items >= 11 else 1) - - # Совмещение элемента истории, выбранного индекса и значения - total_result = zip(clock_data['history'], clock_data['selected_indexes'], clock_data['selected_values']) - for item, index, value in total_result: - print(fmt_print.format(item, index, value)) - - -def print_simple(clock_data: dict): - simple_result = zip(clock_data['selected_indexes'], clock_data['selected_values']) - print(' -> '.join('{}({})'.format(index, value) for index, value in simple_result)) - - -if __name__ == '__main__': - # 2 - # 4 4 - # 1 1 - # 2 3 - # 5 4 - # 3 - # clock_items = [2, 4, 1, 3, 4, 3, 5, 2, 1, 4] - clock_items = list(map(int, '2413435214')) - - wins = solver(clock_items) - print('Winning results:', len(wins)) - - for i, clock_data in enumerate(wins, 1): - print(f'{i}.') - - print('Simple:') - print_simple(clock_data) - print() - - print('Extended:') - print_extended(clock_data) - print() diff --git a/Fibonacci number/6 ways.py b/Fibonacci number/6 ways.py index 2fbe6dc50..25cd58937 100644 --- a/Fibonacci number/6 ways.py +++ b/Fibonacci number/6 ways.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://technobeans.com/2012/04/16/5-ways-of-fibonacci-in-python/ NUMBER = 15 + # Example 1: Using looping technique def fib_loop(n): a, b = 1, 1 @@ -16,7 +17,7 @@ def fib_loop(n): return a -print('Example 1') +print("Example 1") print(fib_loop(NUMBER)) print() @@ -28,7 +29,7 @@ def fib_recursion(n): return fib_recursion(n - 1) + fib_recursion(n - 2) -print('Example 2') +print("Example 2") print(fib_recursion(NUMBER)) print() @@ -40,7 +41,8 @@ def fib_generator(): a, b = b, a + b yield a -print('Example 3') + +print("Example 3") f = fib_generator() for _ in range(NUMBER): print(next(f)) @@ -56,7 +58,7 @@ def memoize(fn, arg): # fib() as written in example 1. -print('Example 4') +print("Example 4") fibm = memoize(fib_loop, NUMBER) print(fibm) print() @@ -64,7 +66,7 @@ def memoize(fn, arg): # Example 5: Using memoization as decorator (decorator-class) class MemoizeClass: - def __init__(self, func): + def __init__(self, func) -> None: self.func = func self.memo = dict() @@ -82,7 +84,8 @@ def fib(n): a, b = b, a + b return a -print('Example 5') + +print("Example 5") print(fib(NUMBER)) print() @@ -98,6 +101,7 @@ def func(*args): return func + @memoize_func def fib(n): a, b = 1, 1 @@ -105,6 +109,7 @@ def fib(n): a, b = b, a + b return a -print('Example 6') + +print("Example 6") print(fib(NUMBER)) print() diff --git a/Fibonacci number/FibonacciNumber.py b/Fibonacci number/FibonacciNumber.py index 30d2dad3d..a164d783e 100644 --- a/Fibonacci number/FibonacciNumber.py +++ b/Fibonacci number/FibonacciNumber.py @@ -1,21 +1,22 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" -def fibo_1(n): + +def fibo_1(n) -> None: f = [0, 1] for i in range(2, n + 1): - f.append(f[i-1] + f[i-2]) + f.append(f[i - 1] + f[i - 2]) print(f) -def fibo_2(n): +def fibo_2(n) -> None: a, b = 0, 1 - print(a, b, end=' ') + print(a, b, end=" ") for i in range(2, n + 1): a, b = b, a + b - print(b, end=' ') + print(b, end=" ") -if __name__ == '__main__': +if __name__ == "__main__": # Fibo / Фибоначчи n = 10 fibo_1(n) diff --git a/Final_Fantasy_II__upgrade_skills_bot.py b/Final_Fantasy_II__upgrade_skills_bot.py deleted file mode 100644 index 5cb24901c..000000000 --- a/Final_Fantasy_II__upgrade_skills_bot.py +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Using ePSXe + setting in the joystick buttons "X" as "K" and "O" as "L". -""" - - -import os -import time - -import keyboard - -from press_release_keys__ScanCodes__for_games import write_key, DIK_L, DIK_K, DIK_S - - -def get_logger(name, file='log.txt', encoding='utf-8', log_stdout=True, log_file=True): - import logging - log = logging.getLogger(name) - log.setLevel(logging.DEBUG) - - formatter = logging.Formatter('[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s') - - if log_file: - from logging.handlers import RotatingFileHandler - fh = RotatingFileHandler(file, maxBytes=10000000, backupCount=5, encoding=encoding) - fh.setFormatter(formatter) - log.addHandler(fh) - - if log_stdout: - import sys - sh = logging.StreamHandler(stream=sys.stdout) - sh.setFormatter(formatter) - log.addHandler(sh) - - return log - - -def change_start(): - DATA['START'] = not DATA['START'] - if DATA['START']: - DATA['COUNTER'] = 0 - - log.debug('Change START: %s', DATA['START']) - - -def set_min_counter(): - DATA['MAX_COUNTER'] = 1 - log.debug('Change MAX_COUNTER: %s', DATA['MAX_COUNTER']) - - -def set_max_counter(): - DATA['MAX_COUNTER'] = MAX_COUNTER - log.debug('Change MAX_COUNTER: %s', DATA['MAX_COUNTER']) - - -def change_cancel_all_last(): - DATA['CANCEL_ALL_LAST'] = not DATA['CANCEL_ALL_LAST'] - log.debug('Change CANCEL_ALL_LAST: %s', DATA['CANCEL_ALL_LAST']) - - -RUN_COMBINATION = 'Ctrl+Shift+Space' -RUN_SET_MIN_COUNTER = 'Ctrl+Shift+1' -RUN_SET_MAX_COUNTER = 'Ctrl+Shift+2' -RUN_CHANGE_CANCEL_ALL_LAST = 'Ctrl+Shift+3' -QUIT_COMBINATION = 'Ctrl+Shift+Q' - -MAX_COUNTER = 100 - -DATA = { - 'START': False, - 'COUNTER': 0, - 'MAX_COUNTER': MAX_COUNTER, - 'CANCEL_ALL_LAST': True, -} - -log = get_logger(__file__, log_file=False) - -log.debug('Press "%s" for RUN / PAUSE', RUN_COMBINATION) -log.debug('Press "%s" for RUN_SET_MIN_COUNTER', RUN_SET_MIN_COUNTER) -log.debug('Press "%s" for RUN_SET_MAX_COUNTER', RUN_SET_MAX_COUNTER) -log.debug('Press "%s" for RUN_CHANGE_CANCEL_ALL_LAST', RUN_CHANGE_CANCEL_ALL_LAST) -log.debug('Press "%s" for QUIT', QUIT_COMBINATION) -log.debug("DATA: %s", DATA) - -keyboard.add_hotkey(RUN_COMBINATION, change_start) -keyboard.add_hotkey(RUN_SET_MIN_COUNTER, set_min_counter) -keyboard.add_hotkey(RUN_SET_MAX_COUNTER, set_max_counter) -keyboard.add_hotkey(RUN_CHANGE_CANCEL_ALL_LAST, change_cancel_all_last) -keyboard.add_hotkey(QUIT_COMBINATION, lambda: log.debug('Quit by Escape') or os._exit(0)) - - -while True: - if not DATA['START']: - time.sleep(0.01) - continue - - # Прокачка атаки - # # Симуляция атаки, кнопка K - # for _ in range(6): - # write_key(DIK_K, pause=0.3) - - number = 3 - if not DATA['CANCEL_ALL_LAST']: - number = 4 - - # Прокачка магии - # Симуляция выбора первого заклинания магии - for _ in range(number): - write_key(DIK_S, pause=0.3) - write_key(DIK_K, pause=0.3) - write_key(DIK_K, pause=0.3) - write_key(DIK_K, pause=0.3) - - if DATA['CANCEL_ALL_LAST']: - # Отмена атаки, кнопка L - # https://gist.github.com/tracend/912308#file-gistfile1-cpp-L42 - for _ in range(4): - write_key(DIK_L, pause=0.3) - - # 100 итераций должно хватить, чтобы докачать до 100% уровень оружия - DATA['COUNTER'] += 1 - if DATA['COUNTER'] >= DATA['MAX_COUNTER']: - DATA['START'] = False - log.debug('Change START: %s', DATA['START']) diff --git a/FindWordInString/FindWordInString.py b/FindWordInString/FindWordInString.py index 332ca14c8..21b9dbb7c 100644 --- a/FindWordInString/FindWordInString.py +++ b/FindWordInString/FindWordInString.py @@ -1,17 +1,22 @@ # coding=utf-8 -import argparse -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import argparse def create_parser(): - parser = argparse.ArgumentParser(description="Find the longest word in a string, separated by spaces.") + parser = argparse.ArgumentParser( + description="Find the longest word in a string, separated by spaces." + ) return parser -if __name__ == '__main__': + +if __name__ == "__main__": create_parser().parse_args() - string = str(raw_input("Input string: ")) + string = input("Input string: ") max_len = -1 max_len_word = "" for word in string.split(" "): @@ -20,4 +25,4 @@ def create_parser(): max_len = current_len max_len_word = word - print("Longest word: " + max_len_word) \ No newline at end of file + print("Longest word: " + max_len_word) diff --git a/FindWordInString/regexp.py b/FindWordInString/regexp.py index 5c2935268..ff9df6b75 100644 --- a/FindWordInString/regexp.py +++ b/FindWordInString/regexp.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -text = 'in comparison to dogs, cats have not undergone major changes during the domestication process.' - import re -words = re.findall(r'\b(\w+)\b', text) + + +text = "in comparison to dogs, cats have not undergone major changes during the domestication process." + +words = re.findall(r"\b(\w+)\b", text) print(words) diff --git a/Fingerprinting with Zero-Width Characters/main.py b/Fingerprinting with Zero-Width Characters/main.py index 49c4c19ae..898f98e8c 100644 --- a/Fingerprinting with Zero-Width Characters/main.py +++ b/Fingerprinting with Zero-Width Characters/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Fingerprinting with Zero-Width Characters @@ -23,10 +23,10 @@ # const zeroWidthNoBreakSpace = '\uFEFF'; // 65279 -ZERO_WIDTH_SPACE = '\u200B' # 8203 -ZERO_WIDTH_NON_JOINER = '\u200C' # 8204 -ZERO_WIDTH_JOINER = '\u200D' # 8205 -ZERO_WIDTH_NO_BREAK_SPACE = '\uFEFF' # 65279 +ZERO_WIDTH_SPACE = "\u200B" # 8203 +ZERO_WIDTH_NON_JOINER = "\u200C" # 8204 +ZERO_WIDTH_JOINER = "\u200D" # 8205 +ZERO_WIDTH_NO_BREAK_SPACE = "\uFEFF" # 65279 def to_binary(c: str) -> str: @@ -34,16 +34,16 @@ def to_binary(c: str) -> str: def text_to_binary(username: str) -> str: - return ' '.join(map(to_binary, username)) + return " ".join(map(to_binary, username)) def binary_to_zero_width(binary_username: str) -> str: zero_width_items = [] for c in binary_username: - if c == '1': + if c == "1": zero_width = ZERO_WIDTH_SPACE - elif c == '0': + elif c == "0": zero_width = ZERO_WIDTH_NON_JOINER else: zero_width = ZERO_WIDTH_JOINER @@ -69,23 +69,29 @@ def zero_width_to_binary(text: str) -> str: for c in text.split(ZERO_WIDTH_NO_BREAK_SPACE): if c == ZERO_WIDTH_SPACE: - binary.append('1') + binary.append("1") elif c == ZERO_WIDTH_NON_JOINER: - binary.append('0') + binary.append("0") else: - binary.append(' ') + binary.append(" ") - return ''.join(binary) + return "".join(binary) def binary_to_text(text: str) -> str: - return ''.join(chr(int(num, 2)) for num in text.split(' ')) + return "".join(chr(int(num, 2)) for num in text.split(" ")) def get_zero_width_from_text(text: str) -> str: - return ''.join( - c for c in text - if c in (ZERO_WIDTH_SPACE, ZERO_WIDTH_NON_JOINER, ZERO_WIDTH_JOINER, ZERO_WIDTH_NO_BREAK_SPACE) + return "".join( + c + for c in text + if c in ( + ZERO_WIDTH_SPACE, + ZERO_WIDTH_NON_JOINER, + ZERO_WIDTH_JOINER, + ZERO_WIDTH_NO_BREAK_SPACE, + ) ) @@ -102,10 +108,12 @@ def get_username_from_text(text: str) -> str: return text_username -if __name__ == '__main__': - text = "This is some confidential text that you really shouldn't be sharing anywhere else. " \ - "Это конфиденциальный текст, которым вы действительно не должны делиться." - username = 'hello world/привет мир' +if __name__ == "__main__": + text = ( + "This is some confidential text that you really shouldn't be sharing anywhere else. " + "Это конфиденциальный текст, которым вы действительно не должны делиться." + ) + username = "hello world/привет мир" print(len(text), text) diff --git a/FizzBuzz/FizzBuzz.py b/FizzBuzz/FizzBuzz.py index a3520b6b4..17f69b275 100644 --- a/FizzBuzz/FizzBuzz.py +++ b/FizzBuzz/FizzBuzz.py @@ -1,22 +1,23 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - # EN: - # Write a program that displays a number from 1 to 100. In this case, instead of numbers that are - # multiples of three, the program should display the word «Fizz», but instead of multiples of five - the - # word «Buzz». If the number is a multiple and 3 and 5, the program should display the word «FizzBuzz» +# EN: +# Write a program that displays a number from 1 to 100. In this case, instead of numbers that are +# multiples of three, the program should display the word «Fizz», but instead of multiples of five - the +# word «Buzz». If the number is a multiple and 3 and 5, the program should display the word «FizzBuzz» - # RU: - # Напишите программу, которая выводит на экран числа от 1 до 100. При этом вместо чисел, кратных трем, программа - # должна выводить слово «Fizz», а вместо чисел, кратных пяти — слово «Buzz». Если число кратно и 3, и 5, - # то программа должна выводить слово «FizzBuzz» - for num in range(1, 100 + 1): - if num % 15 is 0: - print("FizzBuzz") - elif num % 3 is 0: - print("Fizz") - elif num % 5 is 0: - print("Buzz") - else: - print(num) \ No newline at end of file +# RU: +# Напишите программу, которая выводит на экран числа от 1 до 100. При этом вместо чисел, кратных трем, программа +# должна выводить слово «Fizz», а вместо чисел, кратных пяти — слово «Buzz». Если число кратно и 3, и 5, +# то программа должна выводить слово «FizzBuzz» + + +for num in range(1, 100 + 1): + if num % 15 is 0: + print("FizzBuzz") + elif num % 3 is 0: + print("Fizz") + elif num % 5 is 0: + print("Buzz") + else: + print(num) diff --git a/Get MAC from Syslog.txt Router/main.py b/Get MAC from Syslog.txt Router/main.py index e635f5aee..0969c98d9 100644 --- a/Get MAC from Syslog.txt Router/main.py +++ b/Get MAC from Syslog.txt Router/main.py @@ -1,12 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +__author__ = "ipetrash" -__author__ = 'ipetrash' +import re -text = open('Syslog.txt').read() -import re -for mac in set(re.findall(r'[0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5}', text)): +with open("Syslog.txt") as f: + text = f.read() + +for mac in set(re.findall(r"[0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5}", text)): print(mac) +""" +51:FD:DE:2B:B6:DE +61:61:6D:3F:99:1F +03:B4:1D:40:9B:CD +AC:38:72:82:FF:C4 +""" diff --git a/Grab/empty_tags_stackoverflow.py b/Grab/empty_tags_stackoverflow.py index eb4bace36..43b5fa8e5 100644 --- a/Grab/empty_tags_stackoverflow.py +++ b/Grab/empty_tags_stackoverflow.py @@ -1,16 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import time +import traceback + from urllib.parse import quote import grab -if __name__ == '__main__': +if __name__ == "__main__": g = grab.Grab() tag_list = set() @@ -19,8 +21,8 @@ while True: try: - url = 'http://ru.stackoverflow.com/tags?page={}&tab=name'.format(page) - print('Go page (found tags: {}): {}'.format(len(tag_list), url)) + url = f"http://ru.stackoverflow.com/tags?page={page}&tab=name" + print(f"Go page (found tags: {len(tag_list)}): {url}") g.go(url) next_page = g.doc.select('//span[@class="page-numbers next"]') @@ -30,14 +32,19 @@ for a in g.doc.select('//a[@class="post-tag"]'): tag = a.text() - url = 'http://ru.stackoverflow.com/tags/{}/info'.format(quote(tag)) - print(' Go tag: {}'.format(url)) + url = f"http://ru.stackoverflow.com/tags/{quote(tag)}/info" + print(f" Go tag: {url}") tag_g = grab.Grab() tag_g.go(url) - has_not_ref_guide = "Для этой метки до сих пор нет руководства по использованию." in tag_g.response.body - has_not_description = "Для этой метки до сих пор нет описания." in tag_g.response.body + has_not_ref_guide = ( + "Для этой метки до сих пор нет руководства по использованию." + in tag_g.response.body + ) + has_not_description = ( + "Для этой метки до сих пор нет описания." in tag_g.response.body + ) if has_not_ref_guide or has_not_description: tag_info = ( @@ -47,23 +54,21 @@ ) tag_list.add(tag_info) - time.sleep(.3) + time.sleep(0.3) page += 1 time.sleep(1) except Exception as e: - import traceback - # Сохраняем в переменную tb = traceback.format_exc() - print('Error:\n{}'.format(tb)) - print('Wait 60 sec.') + print(f"Error:\n{tb}") + print("Wait 60 sec.") time.sleep(60) - print('Tags: {}.'.format(len(tag_list))) + print(f"Tags: {len(tag_list)}.") for tag in tag_list: print(*tag) diff --git a/Grab/find_pictures_on_request.py b/Grab/find_pictures_on_request.py index badf6bd20..215f08fff 100644 --- a/Grab/find_pictures_on_request.py +++ b/Grab/find_pictures_on_request.py @@ -1,17 +1,18 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Find images on request search engine -from grab import Grab -import urllib.parse import re +import urllib.parse + +from grab import Grab -if __name__ == '__main__': - url = 'http://yandex.ru/images/search?text=' - rq_text = 'husky puppies' +if __name__ == "__main__": + url = "http://yandex.ru/images/search?text=" + rq_text = "husky puppies" url += urllib.parse.quote(rq_text) @@ -19,8 +20,8 @@ g.go(url) images = g.doc.select('//a[@class="serp-item__link"]/@onmousedown') - print('Total: %s' % images.count()) + print(f"Total: {images.count()}") for im in images: im_href = re.search(r'"href":"(.+)"', im.text()).group(1) - print(im_href) \ No newline at end of file + print(im_href) diff --git a/Grab/get_boormarks_readmanga.py b/Grab/get_boormarks_readmanga.py index f53ebd0a4..d366f6773 100644 --- a/Grab/get_boormarks_readmanga.py +++ b/Grab/get_boormarks_readmanga.py @@ -3,14 +3,14 @@ манга, оставленных пользователем в закладках. """ -__author__ = 'ipetrash' +__author__ = "ipetrash" from grab import Grab import re -if __name__ == '__main__': +if __name__ == "__main__": username = input("Логин: ") password = input("Пароль: ") @@ -18,12 +18,12 @@ # Переходим на страницу входа print("...Перехожу на страницу входа...") - g.go('http://grouple.ru/internal/auth/login') + g.go("http://grouple.ru/internal/auth/login") # Заполняем формы логина и пароля print("...Заполняем формы логина и пароля...") - g.set_input('j_username', username) - g.set_input('j_password', password) + g.set_input("j_username", username) + g.set_input("j_password", password) # Отсылаю данные print("...Отсылаю данные...") @@ -35,15 +35,17 @@ # Переход на страницу Закладки print('...Перехожу на страницу "Закладки"...') - g.go('http://grouple.ru/private/bookmarks') + g.go("http://grouple.ru/private/bookmarks") - print("\nПользователь: {}".format(user)) + print(f"\nПользователь: {user}") # Запрос на получение всех закладок - TEMPLATE_BOOKMARKS = ('//div[@class="bookmarks-lists"]/table[starts-with(@class, "cTable bookmarks_")]' - '//tr[@class="bookmark-row"]') + TEMPLATE_BOOKMARKS = ( + '//div[@class="bookmarks-lists"]/table[starts-with(@class, "cTable bookmarks_")]' + '//tr[@class="bookmark-row"]' + ) # Общее количество заметок - print("\nЗакладки({}):".format(g.doc.select(TEMPLATE_BOOKMARKS).count())) + print(f"\nЗакладки({g.doc.select(TEMPLATE_BOOKMARKS).count()}):") # Запрос на получение всех типов закладок TEMPLATE_TYPE_BOOKMARK = '//div[@class="bookmarks-lists"]/table[starts-with(@class, "cTable bookmarks_")]' @@ -51,7 +53,7 @@ # Регулярка для вытаскивания из имени закладки нужное # Например, из "Пока бросил (1)" будет вытащено: "Пока бросил" - regexp_bookmark = re.compile("(.+) \(.+\)") + regexp_bookmark = re.compile(r"(.+) \(.+\)") # Перебор всех типов закладок for tb in type_bookmarks: @@ -61,15 +63,15 @@ # вытаскивается первая группа: # Например, из "Пока бросил (1)" будет вытащено: "Пока бросил" bookmark = regexp_bookmark.search(bookmark).group(1) - print(' Закладка "{}":'.format(bookmark)) + print(f' Закладка "{bookmark}":') # Получение всех закладок данного типа group_bookmarks = tb.select('tr[@class="bookmark-row"]') # Перебор всех закладок данного типа for bm in group_bookmarks: - href = bm.select("td/a").attr('href') # Ссылка на мангу + href = bm.select("td/a").attr("href") # Ссылка на мангу name = bm.select("td/a/text()").text() # Название манги - print(' "{}": {}'.format(name, href)) + print(f' "{name}": {href}') - print() \ No newline at end of file + print() diff --git a/Grab/get_example_from_de_brauwer.py b/Grab/get_example_from_de_brauwer.py index fe09d34fe..119653d05 100644 --- a/Grab/get_example_from_de_brauwer.py +++ b/Grab/get_example_from_de_brauwer.py @@ -1,7 +1,11 @@ -from grab import Grab + +__author__ = "ipetrash" + + import re +import os -__author__ = 'ipetrash' +from grab import Grab """Пример получения исходного кода с сайта www.de-brauwer.be.""" @@ -9,39 +13,35 @@ def get_example_from_de_brauwer(wakka): g = Grab() - g.go('http://www.de-brauwer.be/wiki/wikka.php?wakka=' + wakka) + g.go("http://www.de-brauwer.be/wiki/wikka.php?wakka=" + wakka) code_html = g.doc.select('//div[@class="code"]').html() - pattern = (r'(
)' - '|' - '|' - '|' - '|
') - source_code = re.sub(pattern, '', code_html) - source_code = re.sub('\xa0', ' ', source_code) # remove   + pattern = r'(
)' "|" "|" "|" "|
" + source_code = re.sub(pattern, "", code_html) + source_code = re.sub("\xa0", " ", source_code) # remove   return source_code -DIR_NAME = 'PyOpenGLExample' -import os + +DIR_NAME = "PyOpenGLExample" os.makedirs(DIR_NAME, exist_ok=True) examples = [ - 'PyOpenGLHelloWorld', - 'PyOpenGLSierpinski', - 'PyOpenGLSquares', - 'PyOpenGLCheckerBoard', - 'PyOpenGLMouse', - 'PyOpenGLScatter', - 'PyOpenGLGingerbread', - 'PyOpenGLMaze', - 'PyOpenGLReshape', - 'PyOpenGLTurtle', - 'PyOpenGLRosette', - 'PyOpenGLWireframe', + "PyOpenGLHelloWorld", + "PyOpenGLSierpinski", + "PyOpenGLSquares", + "PyOpenGLCheckerBoard", + "PyOpenGLMouse", + "PyOpenGLScatter", + "PyOpenGLGingerbread", + "PyOpenGLMaze", + "PyOpenGLReshape", + "PyOpenGLTurtle", + "PyOpenGLRosette", + "PyOpenGLWireframe", ] for e in examples: source_code = get_example_from_de_brauwer(e) - with open(os.path.join(DIR_NAME, e + '.py'), mode='w', encoding='utf-8') as f: - f.write(source_code) \ No newline at end of file + with open(os.path.join(DIR_NAME, e + ".py"), mode="w", encoding="utf-8") as f: + f.write(source_code) diff --git a/Grab/get_repo_github_info.py b/Grab/get_repo_github_info.py index 7d6f95b4b..53bf1b03d 100644 --- a/Grab/get_repo_github_info.py +++ b/Grab/get_repo_github_info.py @@ -3,13 +3,13 @@ репозиториев и некоторую информацию о них. """ -__author__ = 'ipetrash' +__author__ = "ipetrash" from grab import Grab -if __name__ == '__main__': +if __name__ == "__main__": login = input("Логин: ") password = input("Пароль: ") @@ -17,12 +17,12 @@ # Переходим на страницу входа print("...Перехожу на страницу входа...") - g.go('https://github.com/login') + g.go("https://github.com/login") # Заполняем формы логина и пароля print("...Заполняем формы логина и пароля...") - g.set_input('login', login) - g.set_input('password', password) + g.set_input("login", login) + g.set_input("password", password) # Отсылаю данные формы print("...Отсылаю данные формы...") @@ -30,21 +30,21 @@ # Переход на страницу с репозиториями print("...Перехожу на страницу с репозиториями...") - g.go("https://github.com/{}?tab=repositories".format(login)) + g.go(f"https://github.com/{login}?tab=repositories") # Получение списка репозиториев print("...Получаю список репозиториев...") # list_repo = g.doc.select('//ul[@class="repo-list js-repo-list"]/li/h3/a') list_repo = g.doc.select('//h3[@class="repo-list-name"]/a') - print("\nРепозитории({}):".format(len(list_repo))) + print(f"\nРепозитории({len(list_repo)}):") # Перебор всех репозиториев for i, repo in enumerate(list_repo, 1): - url = "https://github.com" + repo.attr('href') + url = "https://github.com" + repo.attr("href") grab_repo = Grab() grab_repo.go(url) # переход на страницу репозитория # получение количества коммитов данного репозитория count = grab_repo.doc.select('//span[@class="num text-emphasized"]').text() - print('{}. "{}" ({} commits): {}'.format(i, repo.text(), count, url)) \ No newline at end of file + print(f'{i}. "{repo.text()}" ({count} commits): {url}') diff --git a/Grab/get_translation_completed_manga_from_readmanga.py b/Grab/get_translation_completed_manga_from_readmanga.py index 1bcb7d10c..b44dcd10d 100644 --- a/Grab/get_translation_completed_manga_from_readmanga.py +++ b/Grab/get_translation_completed_manga_from_readmanga.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Вывод манг жанра 'game' с статусом 'Переведена'. @@ -22,52 +22,51 @@ from grab import Grab -LOGIN = '' -PASSWORD = '' +LOGIN = "" +PASSWORD = "" -if __name__ == '__main__': +if __name__ == "__main__": g = Grab() # Авторизация - g.go('http://grouple.ru/internal/auth/login') - g.set_input('j_username', LOGIN) - g.set_input('j_password', PASSWORD) + g.go("http://grouple.ru/internal/auth/login") + g.set_input("j_username", LOGIN) + g.set_input("j_password", PASSWORD) g.submit() - if g.response.url == 'http://grouple.ru/internal/auth/login?login_error=1': - print('Авторизация прошла неудачно :(') + if g.response.url == "http://grouple.ru/internal/auth/login?login_error=1": + print("Авторизация прошла неудачно :(") # Зайдем в закладки - g.go('http://grouple.ru/private/bookmarks') + g.go("http://grouple.ru/private/bookmarks") # Получим список всех url'ов манг в закладках get_href_manga_xpath = '//div[@class="bookmarks-lists"]/*/tr/td/a/@href' - all_bookmarks_href = [href.text() - for href in g.doc.select(get_href_manga_xpath)] + all_bookmarks_href = [href.text() for href in g.doc.select(get_href_manga_xpath)] # Заходим на страницу жанра манг "игра" - g.go('http://readmanga.me/list/genre/game') + g.go("http://readmanga.me/list/genre/game") # Ищем элементы div, у которых есть дети div/span у которого есть класс "mangaTranslationCompleted", # и если мы такой нашли, мы у него ищем div с классом "desc" xpath = '//div[div/span[@class="mangaTranslationCompleted"]]/div[@class="desc"]' - print('Список законченной и переведенной манги:') + print("Список законченной и переведенной манги:") # Переберем список найденных элементов for i, tile in enumerate(g.doc.select(xpath), 1): - title = tile.select('h3/a/@title').text() + title = tile.select("h3/a/@title").text() - alt_title = tile.select('h4/@title') + alt_title = tile.select("h4/@title") # Если альтернативный заголовок есть, добавим его if alt_title.count(): - title = '"{}" / "{}"'.format(title, alt_title.text()) + title = f'"{title}" / "{alt_title.text()}"' else: - title = '"{}"'.format(title) + title = f'"{title}"' - href = 'http://readmanga.me' + tile.select('h3/a/@href').text() + href = "http://readmanga.me" + tile.select("h3/a/@href").text() if href not in all_bookmarks_href: - print('{}. {}: {}'.format(i, title, href)) + print("{i}. {title}: {href}") else: - print('Уже есть в закладках {}: {}'.format(title, href)) \ No newline at end of file + print("Уже есть в закладках {title}: {href}") diff --git a/Grab/get_user_github_info.py b/Grab/get_user_github_info.py index f6637d14a..1bc3b6e7f 100644 --- a/Grab/get_user_github_info.py +++ b/Grab/get_user_github_info.py @@ -5,10 +5,10 @@ """ -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': +if __name__ == "__main__": login = input("Логин: ") password = input("Пароль: ") @@ -36,7 +36,9 @@ fullname = g.doc.select('//span[@itemprop="name"]').text() username = g.doc.select('//span[@itemprop="additionalName"]').text() - avatar = g.doc.select('//*[@class="vcard-avatar tooltipped tooltipped-s"]/*[@class="avatar"]').attr('src') + avatar = g.doc.select( + '//*[@class="vcard-avatar tooltipped tooltipped-s"]/*[@class="avatar"]' + ).attr("src") organization = g.doc.select('//li[@itemprop="worksFor"]').text() homeLocation = g.doc.select('//li[@itemprop="homeLocation"]').text() email = g.doc.select('//a[@class="email"]').text() @@ -53,31 +55,30 @@ print("home location:", homeLocation) print("email:", email) print("url:", url) - print('{} {} ({})'.format(join_label, join_title_time, join_datetime)) + print(f"{join_label} {join_title_time} ({join_datetime})") # Получение списка репозиториев print() - print('...Перехожу на вкладку репозиториев...') - g.go("https://github.com/" + login + '?tab=repositories') + print("...Перехожу на вкладку репозиториев...") + g.go("https://github.com/" + login + "?tab=repositories") print("...Получаю список репозиториев...") list_source_repo = g.doc.select('//li[@class="repo-list-item public source"]') print() print("Репозитории:") - print("Sources({}):".format(len(list_source_repo))) + print(f"Sources({len(list_source_repo)}):") for i, repo in enumerate(list_source_repo, 1): name = repo.select('*[@class="repo-list-name"]/a') - href = 'https://github.com' + name.attr('href') - print(' {}. {}: {}'.format(i, name.text(), href)) + href = "https://github.com" + name.attr("href") + print(f" {i}. {name.text()}: {href}") description = repo.select('*[@class="repo-list-description"]') if description.count(): - print(' "{}"'.format(description.text())) + print(f' "{description.text()}"') - stats = repo.select('*[@class="repo-list-stats"]').text().split(' ') + stats = repo.select('*[@class="repo-list-stats"]').text().split(" ") lang, stars, forks = stats - print(' lang: {}, stars: {}, forks: {}'.format(lang, stars, forks)) - print() \ No newline at end of file + print(f" lang: {lang}, stars: {stars}, forks: {forks}\n") diff --git a/Grab/parse_onlinemultfilmy.py b/Grab/parse_onlinemultfilmy.py index 71512d032..0c2eed163 100644 --- a/Grab/parse_onlinemultfilmy.py +++ b/Grab/parse_onlinemultfilmy.py @@ -1,20 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Поиск мультсериалов 16+ # Пример сериала: 'http://onlinemultfilmy.ru/bratya-ventura/' + import time from grab import Grab + g = Grab() +# TODO: магическое число нужно заменить # Перебор страниц с мультами for i in range(1, 82 + 1): - url_page = 'http://onlinemultfilmy.ru/multserialy/page/' + str(i) + url_page = f"http://onlinemultfilmy.ru/multserialy/page/{i}" print(url_page) # Загрузка страницы с мультами @@ -22,10 +25,10 @@ # Перебор и загрузка мультов на странице for url in g.doc.select('//div[@class="cat-post"]/a'): - g.go(url.attr('href')) + g.go(url.attr("href")) if g.doc.select('//*[@class="age_icon age_icon_16"]').count(): - print(' ', url.attr('title'), url.attr('href')) + print(" ", url.attr("title"), url.attr("href")) # Чтобы сервер не посчитал это дос атакой time.sleep(2) diff --git a/Guess the number/GuessTheNumber.py b/Guess the number/GuessTheNumber.py deleted file mode 100644 index 82bca49dd..000000000 --- a/Guess the number/GuessTheNumber.py +++ /dev/null @@ -1,32 +0,0 @@ -__author__ = 'ipetrash' - -# Guess the number / Угадай число -import random - -if __name__ == '__main__': - max = int(input("Input max: ")) - print("Random number (x): from %d to %d" % (1, max)) - number = random.randrange(1, max + 1) - user_choice = -1 - range_min = range_max = "?" - - while True: - print("\n%s < x < %s" % (range_min, range_max)) - user_choice = int(input("Input number: ")) - if number > user_choice: - if range_min == "?": - range_min = user_choice - if user_choice > range_min: - range_min = user_choice - print("x > %d" % user_choice) - - elif number < user_choice: - if range_max == "?": - range_max = user_choice - if user_choice < range_max: - range_max = user_choice - print("x < %d" % user_choice) - - elif number == user_choice: - print("Congratulations! You guessed it!") - break diff --git a/Hash/available.py b/Hash/available.py index e67d78ab3..6cf6ac4a9 100644 --- a/Hash/available.py +++ b/Hash/available.py @@ -1,29 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import hashlib import sys -if __name__ == '__main__': - algorithms = list(hashlib.algorithms_available) # get list algorithms - algorithms.sort() - print("Algorithms available: %s" % ", ".join(algorithms)) - - text = input("Text: ") - if not text: - print("Empty text!") - sys.exit(1) - - alg_name = input("Name algorithm: ") - if alg_name not in algorithms: # search in list - print("Algorithm not found!") - sys.exit(1) - - alg = hashlib.new(alg_name) # create hash function from name - alg.update(text.encode()) # set data in hash-function - print("Result:") - print(" hex: %s" % alg.hexdigest()) - print(" HEX: %s" % alg.hexdigest().upper()) + +algorithms = list(hashlib.algorithms_available) # get list algorithms +algorithms.sort() +print(f"Algorithms available: {', '.join(algorithms)}") + +text = input("Text: ") +if not text: + print("Empty text!") + sys.exit(1) + +alg_name = input("Name algorithm: ") +if alg_name not in algorithms: # search in list + print("Algorithm not found!") + sys.exit(1) + +alg = hashlib.new(alg_name) # create hash function from name +alg.update(text.encode()) # set data in hash-function +print("Result:") +print(f" hex: {alg.hexdigest()}") +print(f" HEX: {alg.hexdigest().upper()}") diff --git a/Hash/custom.py b/Hash/custom.py index dcd6dceff..b24dd957f 100644 --- a/Hash/custom.py +++ b/Hash/custom.py @@ -1,23 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import hashlib from datetime import datetime -import hashlib -if __name__ == '__main__': - ts = datetime.today().timestamp() - bts = str(ts).encode() +ts = datetime.today().timestamp() +bts = str(ts).encode() - # Long - md5 = hashlib.md5() - md5.update(bts) +# Long +md5 = hashlib.md5() +md5.update(bts) - print(md5.hexdigest()) - - # Short - print(hashlib.md5(bts).hexdigest()) +print(md5.hexdigest()) +# Short +print(hashlib.md5(bts).hexdigest()) diff --git a/Hash/encode_all_algo.py b/Hash/encode_all_algo.py index 221cb683c..516bc94ea 100644 --- a/Hash/encode_all_algo.py +++ b/Hash/encode_all_algo.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import hashlib -if __name__ == '__main__': - available = list(set(map(str.lower, hashlib.algorithms_available))) - available.sort() - text = 'hello world' +available = list(set(map(str.lower, hashlib.algorithms_available))) +available.sort() - for algo_name in available: - try: - alg = hashlib.new(algo_name) # create hash function from name - alg.update(text.encode()) # set data in hash-function - print("{}: {}".format(algo_name, alg.hexdigest().upper())) +text = "hello world" - except ValueError: - pass +for algo_name in available: + try: + alg = hashlib.new(algo_name) # create hash function from name + alg.update(text.encode()) # set data in hash-function + print(f"{algo_name}: {alg.hexdigest().upper()}") + + except ValueError: + pass diff --git a/Hash/to sha256.py b/Hash/to sha256.py index 1ad99dd18..62140261e 100644 --- a/Hash/to sha256.py +++ b/Hash/to sha256.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def to_sha256(text): - import hashlib +import hashlib + + +def to_sha256(text: str) -> str: return hashlib.sha256(text.encode()).hexdigest() -if __name__ == '__main__': - print(to_sha256('Hello World!')) +if __name__ == "__main__": + print(to_sha256("Hello World!")) diff --git a/HelloPython/HelloPython.py b/HelloPython/HelloPython.py index 0f82a6c8d..388f750d8 100644 --- a/HelloPython/HelloPython.py +++ b/HelloPython/HelloPython.py @@ -1,15 +1,14 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" import argparse +from datetime import datetime, time -def main(namespace): +def main(namespace) -> None: args = namespace.parse_args() args.user = "Илья" if args.user: - from datetime import datetime, time - welcome = "Привет" current = datetime.now().time() @@ -25,16 +24,17 @@ def main(namespace): elif time(0) <= current < time(6): welcome = "Доброй ночи" - print("{}, {}!".format(welcome, args.user)) + print(f"{welcome}, {args.user}!") else: print("Привет, Python!") -def create_parser(): - parser = argparse.ArgumentParser(description='Hello World Example!') - parser.add_argument('--user', type=str, help=' user name.') +def create_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Hello World Example!") + parser.add_argument("--user", type=str, help=" user name.") return parser -if __name__ == '__main__': + +if __name__ == "__main__": parser = create_parser() main(parser) diff --git a/HypotenuseOfTriangle/HypotenuseOfTriangle.py b/HypotenuseOfTriangle/HypotenuseOfTriangle.py index 82f6ca44f..a59faafbe 100644 --- a/HypotenuseOfTriangle/HypotenuseOfTriangle.py +++ b/HypotenuseOfTriangle/HypotenuseOfTriangle.py @@ -1,34 +1,40 @@ # coding=utf-8 -import math + + +__author__ = "ipetrash" + + import argparse +import math -__author__ = 'ipetrash' def createParser(): - parse = argparse.ArgumentParser(description=u"Программа высчитывает гипотенузу прямоугольного треугольника") - parse.add_argument("-a", type=int, help=u"Первый катет") - parse.add_argument("-b", type=int, help=u"второй катет") + parse = argparse.ArgumentParser( + description="Программа высчитывает гипотенузу прямоугольного треугольника" + ) + parse.add_argument("-a", type=int, help="Первый катет") + parse.add_argument("-b", type=int, help="второй катет") return parse def hypotenuse(a, b): - u""" + """ Функция высчитывает гипотенузу прямоугольного треугольника. :param a: Первый катет :param b: Второй катет :return: Гипотенуза """ - return math.sqrt(a ** 2 + b ** 2) + return math.sqrt(a**2 + b**2) -if __name__ == '__main__': +if __name__ == "__main__": parse = createParser() args = parse.parse_args() if args.a is not None and args.b is not None: a = args.a b = args.b - print(u"Катет а=%s, катет b=%s, гипотенуза с=%s" % (a, b, hypotenuse(a, b))) + print(f"Катет а={a}, катет b={b}, гипотенуза с={hypotenuse(a, b)}") else: - a = int(raw_input(u"Введи катет a: ")) - b = int(raw_input(u"Введи катет b: ")) - print(u"Гипотенуза с=%s" % hypotenuse(a, b)) \ No newline at end of file + a = int(input("Введи катет a: ")) + b = int(input("Введи катет b: ")) + print(f"Гипотенуза с={hypotenuse(a, b)}") diff --git a/LCM/LCM.py b/LCM/LCM.py index b59cc9fcf..802cb9f97 100644 --- a/LCM/LCM.py +++ b/LCM/LCM.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # EN: Least Common Multiple. @@ -15,9 +15,11 @@ def gcd(a, b): return b -if __name__ == '__main__': +if __name__ == "__main__": a, b = 7006652, 112307574 print("LCM: %s" % ((a * b) // gcd(a, b))) + # LCM: 637682405172 import math print("LCM: %s" % ((a * b) // math.gcd(a, b))) + # LCM: 637682405172 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..8e4fbe851 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Ilya Petrash (gil9red) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LuhnAlgorithm/LuhnAlgorithm.py b/LuhnAlgorithm/LuhnAlgorithm.py index 0fa9d6048..ed6fcbc45 100644 --- a/LuhnAlgorithm/LuhnAlgorithm.py +++ b/LuhnAlgorithm/LuhnAlgorithm.py @@ -1,13 +1,13 @@ # coding=utf-8 -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - number_card = raw_input("Number card: ") +if __name__ == "__main__": + number_card = input("Number card: ") sum = 0 digits = [int(x) for x in number_card if x.isdigit()] digits.reverse() - for i in xrange(len(digits)): + for i in range(len(digits)): p = digits[i] if i % 2 is not 0: p *= 2 @@ -15,9 +15,9 @@ p -= 9 sum += p - print("sum: %d" % sum) + print(f"sum: {sum}") sum = 10 - (sum % 10) sum = 0 if sum is 10 else sum - print("checksum: %s" % sum) + print(f"checksum: {sum}") print("card number is correct" if (sum % 10 is 0) else "card number is incorrect") diff --git a/MergeListOfLists/MergeListOfLists.py b/MergeListOfLists/MergeListOfLists.py index 965b53674..f198a5d5a 100644 --- a/MergeListOfLists/MergeListOfLists.py +++ b/MergeListOfLists/MergeListOfLists.py @@ -1,9 +1,16 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" -## EN: Ways to merge a list of lists -## RU: Cпособы слияния списка списков -# source: http://habrahabr.ru/post/63539/ -def listmerge1(lstlst): + +# EN: Ways to merge a list of lists +# RU: Cпособы слияния списка списков + + +import operator +from functools import reduce + + +# Source: http://habrahabr.ru/post/63539/ +def list_merge_1(lstlst): all = [] for lst in lstlst: for el in lst: @@ -11,43 +18,37 @@ def listmerge1(lstlst): return all -def listmerge2(lstlst): +def list_merge_2(lstlst): all = [] for lst in lstlst: all = all + lst return all -def listmerge3(lstlst): +def list_merge_3(lstlst): all = [] for lst in lstlst: all.extend(lst) return all -from functools import reduce - -listmerge4a = lambda ll: reduce(lambda a, b: a + b, ll, []) -listmerge4b = lambda ll: sum(ll, []) - +list_merge_4_a = lambda ll: reduce(lambda a, b: a + b, ll, []) +list_merge_4_b = lambda ll: sum(ll, []) -listmerge5 = lambda ll: [el for lst in ll for el in lst] +list_merge_5 = lambda ll: [el for lst in ll for el in lst] - -listmerge6a = lambda s: reduce(lambda d, el: d.extend(el) or d, s, []) - -import operator -listmerge6b = lambda s: reduce(operator.iadd, s, []) +list_merge_6_a = lambda s: reduce(lambda d, el: d.extend(el) or d, s, []) +list_merge_6_b = lambda s: reduce(operator.iadd, s, []) -lstlst = ([6, 6], [1, 2, 3], [4, 5], [6], [7, 8], [9]) -print("List: ", lstlst) +lst_lst = ([6, 6], [1, 2, 3], [4, 5], [6], [7, 8], [9]) +print("List: ", lst_lst) print("Result:") -print("1. ", listmerge1(lstlst)) -print("2. ", listmerge2(lstlst)) -print("3. ", listmerge3(lstlst)) -print("4a. ", listmerge4a(lstlst)) -print("4b. ", listmerge4b(lstlst)) -print("5. ", listmerge5(lstlst)) -print("6a. ", listmerge6a(lstlst)) -print("6b. ", listmerge6b(lstlst)) \ No newline at end of file +print("1. ", list_merge_1(lst_lst)) +print("2. ", list_merge_2(lst_lst)) +print("3. ", list_merge_3(lst_lst)) +print("4a. ", list_merge_4_a(lst_lst)) +print("4b. ", list_merge_4_b(lst_lst)) +print("5. ", list_merge_5(lst_lst)) +print("6a. ", list_merge_6_a(lst_lst)) +print("6b. ", list_merge_6_b(lst_lst)) diff --git a/Moodle_examples/get_token.py b/Moodle_examples/get_token.py index 19bde3153..69047a1cf 100644 --- a/Moodle_examples/get_token.py +++ b/Moodle_examples/get_token.py @@ -1,32 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import requests def get_token(username, password): params = { - 'username': username, - 'password': password, - 'service': 'moodle_mobile_app', + "username": username, + "password": password, + "service": "moodle_mobile_app", } - import requests - rs = requests.get('http://newlms.magtu.ru/login/token.php', params=params) + + rs = requests.get("http://newlms.magtu.ru/login/token.php", params=params) print(rs) json_data = rs.json() - print('Response: {}'.format(json_data)) + print(f"Response: {json_data}") - if 'errorcode' in json_data: - print(json_data['errorcode']) + if "errorcode" in json_data: + print(json_data["errorcode"]) return - return json_data['token'] + return json_data["token"] -USERNAME = '' -PASSWORD = '' +USERNAME = "" +PASSWORD = "" -if __name__ == '__main__': +if __name__ == "__main__": token = get_token(USERNAME, PASSWORD) - print('token: {}'.format(token)) + print(f"token: {token}") diff --git a/Moodle_examples/search_current_user.py b/Moodle_examples/search_current_user.py index 3e3f9ae6e..aca5fced5 100644 --- a/Moodle_examples/search_current_user.py +++ b/Moodle_examples/search_current_user.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -9,36 +9,36 @@ from get_token import get_token -USERNAME = '' -PASSWORD = '' +USERNAME = "" +PASSWORD = "" token = get_token(USERNAME, PASSWORD) params = { - 'wstoken': token, - 'wsfunction': 'core_user_get_users_by_field', - 'moodlewsrestformat': 'json', - 'field': 'username', - 'values[0]': USERNAME, + "wstoken": token, + "wsfunction": "core_user_get_users_by_field", + "moodlewsrestformat": "json", + "field": "username", + "values[0]": USERNAME, } -rs = requests.get('http://newlms.magtu.ru/webservice/rest/server.php', params=params) +rs = requests.get("http://newlms.magtu.ru/webservice/rest/server.php", params=params) print(rs) json_data = rs.json() -print('Response: {}'.format(json_data)) +print(f"Response: {json_data}") -if 'errorcode' in json_data: - print(json_data['errorcode']) +if "errorcode" in json_data: + print(json_data["errorcode"]) sys.exit() if not json_data: - print('Not found user "{}"'.format(USERNAME)) + print(f'Not found user "{USERNAME}"') sys.exit() info = json_data[0] -print(info['username']) -print(info['fullname']) -print(info['email']) -print(info['idnumber']) +print(info["username"]) +print(info["fullname"]) +print(info["email"]) +print(info["idnumber"]) diff --git a/Morse code with Python unary + and - operators.py b/Morse code with Python unary + and - operators.py index b5d5dfb2d..e0ae258e3 100644 --- a/Morse code with Python unary + and - operators.py +++ b/Morse code with Python unary + and - operators.py @@ -58,7 +58,7 @@ "(": "-.--.-", ")": "-.--.-", "'": ".----.", - "\"": ".-..-.", + '"': ".-..-.", "-": "-....-", "/": "-..-.", "?": "..--..", @@ -122,5 +122,5 @@ def morsify(s): _, ___ = Morse(), MorseWithSpace() print(+--+_ + -+_ + +_ + --_ + _ - _ + -+-+-___ - -+_ + +_ - _ + +++_ + -_ - +++_ - -++--_) - print(morsify('ПРИВЕТ ХАБР!')) # +--+_+-+_++_+--_+_-___++++_+-_-+++_+-+_--++--_ - print(+--+_+-+_++_+--_+_-___++++_+-_-+++_+-+_--++--_) + print(morsify("ПРИВЕТ ХАБР!")) # +--+_+-+_++_+--_+_-___++++_+-_-+++_+-+_--++--_ + print(+--+_ + -+_ + +_ + --_ + _ - ___ + +++_ + -_ - +++_ + -+_ - -++--_) diff --git a/MultiplicationTableByX/MultiplicationTableByX.py b/MultiplicationTableByX/MultiplicationTableByX.py index bf732c808..368fffdf8 100644 --- a/MultiplicationTableByX/MultiplicationTableByX.py +++ b/MultiplicationTableByX/MultiplicationTableByX.py @@ -1,8 +1,12 @@ -# coding=utf-8 +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + # Реализовать функцию-генератор строки с таблицей умножения на число Х. -__author__ = 'ipetrash' -if __name__ == '__main__': - x = int(raw_input("Input x: ")) - print(' '.join(['%d' % (i * x) for i in xrange(1, 10 + 1)])) \ No newline at end of file +if __name__ == "__main__": + x = int(input("Input x: ")) + print(" ".join(f"{i * x}" for i in range(1, 10 + 1))) diff --git a/MultiplicationTablesByM/MultiplicationTablesByM.py b/MultiplicationTablesByM/MultiplicationTablesByM.py index 3050071e6..68874ceaf 100644 --- a/MultiplicationTablesByM/MultiplicationTablesByM.py +++ b/MultiplicationTablesByM/MultiplicationTablesByM.py @@ -1,20 +1,25 @@ -# coding=utf-8 -import argparse +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" -__author__ = 'ipetrash' + +import argparse def create_parser(): - parser = argparse.ArgumentParser(description="The table of multiplication by M. Table compiled from M * a, to M * b, " - "where M, a, b are requested from the user.") + parser = argparse.ArgumentParser( + description="The table of multiplication by M. Table compiled from M * a, to M * b, " + "where M, a, b are requested from the user." + ) return parser -if __name__ == '__main__': +if __name__ == "__main__": create_parser().parse_args() - M = int(raw_input("M=")) - a = int(raw_input("a=")) - b = int(raw_input("b=")) + M = int(input("M=")) + a = int(input("a=")) + b = int(input("b=")) for x in range(a, b): - print("%d * %d = %d" % (M, x, M * x)) + print(f"{M:d} * {x:d} = {M * x:d}") diff --git a/No choice line edit.py b/No choice line edit.py index 28d567f2f..c8db94aac 100644 --- a/No choice line edit.py +++ b/No choice line edit.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -14,31 +14,31 @@ class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('No choice') + self.setWindowTitle("No choice") self.line_edit_no_choice = QLineEdit() self.line_edit_no_choice.textEdited.connect(self.on_text_edited_no_choice) - self.line_edit_source = QLineEdit('Я придурок') + self.line_edit_source = QLineEdit("Я придурок") self.line_edit_source.textEdited.connect(self.line_edit_no_choice.clear) layout = QFormLayout() - layout.addRow('Скажи:', self.line_edit_source) + layout.addRow("Скажи:", self.line_edit_source) layout.addWidget(self.line_edit_no_choice) self.setLayout(layout) self.line_edit_no_choice.setFocus() - def on_text_edited_no_choice(self, text): - text = self.line_edit_source.text()[:len(text)] + def on_text_edited_no_choice(self, text) -> None: + text = self.line_edit_source.text()[: len(text)] self.line_edit_no_choice.setText(text) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() diff --git a/ObjectWithArrayAccess.py b/ObjectWithArrayAccess.py index 8de66be6b..1886745e0 100644 --- a/ObjectWithArrayAccess.py +++ b/ObjectWithArrayAccess.py @@ -1,29 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" class ObjectWithArrayAccess: - def __init__(self): + def __init__(self) -> None: self._fields = dict() def __getitem__(self, item): return self._fields.get(item) - def __setitem__(self, key, value): + def __setitem__(self, key, value) -> None: self._fields[key] = value - def __len__(self): + def __len__(self) -> int: return len(self._fields) -if __name__ == '__main__': +if __name__ == "__main__": obj = ObjectWithArrayAccess() - print(bool(obj)) # False - obj[0] = 'Hello' - obj[1] = 'world' - print(obj._fields) # {0: 'Hello', 1: 'world'} - print(len(obj)) # 2 - print(bool(obj)) # True + print(bool(obj)) # False + obj[0] = "Hello" + obj[1] = "world" + print(obj._fields) # {0: 'Hello', 1: 'world'} + print(len(obj)) # 2 + print(bool(obj)) # True print(obj[0], obj[1]) # Hello world diff --git a/OpenSSL_example/generate_selfsigned_cert.py b/OpenSSL_example/generate_selfsigned_cert.py index f86eaff25..c71bd75f5 100644 --- a/OpenSSL_example/generate_selfsigned_cert.py +++ b/OpenSSL_example/generate_selfsigned_cert.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/60804101/5909792 @@ -21,8 +21,8 @@ def cert_gen( serial_number=0, validity_end_in_seconds=10 * 365 * 24 * 60 * 60, key_file="key.pem", - cert_file="cert.pem" -): + cert_file="cert.pem", +) -> None: # Create a key pair k = crypto.PKey() k.generate_key(crypto.TYPE_RSA, 4096) @@ -41,7 +41,7 @@ def cert_gen( cert.gmtime_adj_notAfter(validity_end_in_seconds) cert.set_issuer(cert.get_subject()) cert.set_pubkey(k) - cert.sign(k, 'sha512') + cert.sign(k, "sha512") with open(cert_file, "wb") as f: f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) @@ -50,5 +50,5 @@ def cert_gen( f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k)) -if __name__ == '__main__': +if __name__ == "__main__": cert_gen() diff --git a/OpenSSL_example/p12_to_pem.py b/OpenSSL_example/p12_to_pem.py index b35bd1f90..8c419fd66 100644 --- a/OpenSSL_example/p12_to_pem.py +++ b/OpenSSL_example/p12_to_pem.py @@ -1,24 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os.path from OpenSSL import crypto -def save_pem(p12_file_name, p12_password, pem_file_name=None): +def save_pem(p12_file_name, p12_password, pem_file_name=None) -> None: """Функция из p12 вытаскивает pem. Если pem_file_name не указан, сохраняется в той же папке, что и p12_file_name.""" if pem_file_name is None: - pem_file_name = os.path.splitext(p12_file_name)[0] + '.pem' + pem_file_name = os.path.splitext(p12_file_name)[0] + ".pem" # May require "" for empty password depending on version - p12 = crypto.load_pkcs12(open(p12_file_name, 'rb').read(), p12_password) + p12 = crypto.load_pkcs12(open(p12_file_name, "rb").read(), p12_password) - with open(pem_file_name, mode='wb') as f: + with open(pem_file_name, mode="wb") as f: # PEM formatted private key f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, p12.get_privatekey())) @@ -26,7 +26,7 @@ def save_pem(p12_file_name, p12_password, pem_file_name=None): f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, p12.get_certificate())) -if __name__ == '__main__': - p12_file_name = '' - p12_password = '' +if __name__ == "__main__": + p12_file_name = "" + p12_password = "" save_pem(p12_file_name, p12_password) diff --git a/Parse Teslagrad. Get and show scrolls/main.py b/Parse Teslagrad. Get and show scrolls/main.py deleted file mode 100644 index e993c68c7..000000000 --- a/Parse Teslagrad. Get and show scrolls/main.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -__author__ = 'ipetrash' - - -# Teslagrad. Прохождение игры на 100%. Карта расположения и изображения свитков (Сайт GamesisArt.ru) - -import os -from urllib.parse import urljoin -import requests - -# Cache -if not os.path.exists('scrolls.html'): - rs = requests.get('http://gamesisart.ru/guide/Teslagrad_Prohozhdenie_4.html#Scrolls') - html = rs.content - - with open('scrolls.html', 'wb') as f: - f.write(html) - -else: - html = open('scrolls.html', 'rb').read() - - -URL = 'http://gamesisart.ru/guide/Teslagrad_Prohozhdenie_4.html#Scrolls' -DIR_SCROLLS = 'scrolls' - -from bs4 import BeautifulSoup -root = BeautifulSoup(html, 'html.parser') - -img_urls = [img['src'] for img in root.select('img[src]')] -img_urls = [urljoin(URL, url_img) for url_img in img_urls if '/Teslagrad_Scroll_' in url_img] -print(len(img_urls), img_urls) - -if not os.path.exists(DIR_SCROLLS): - os.mkdir(DIR_SCROLLS) - -# Save images -for url in img_urls: - rs = requests.get(url) - img_data = rs.content - - file_name = DIR_SCROLLS + '/' + os.path.basename(url) - - with open(file_name, 'wb') as f: - f.write(img_data) - -# Merge all image into one -IMAGE_WIDTH = 200 -IMAGE_HEIGHT = 376 -ROWS = 9 -COLS = 4 - -SCROOLS_WIDTH = IMAGE_WIDTH * COLS -SCROOLS_HEIGHT = IMAGE_HEIGHT * ROWS - -from PIL import Image -image = Image.new('RGB', (SCROOLS_WIDTH, SCROOLS_HEIGHT)) - -import glob -file_names = glob.glob('scrolls/*.jpg') - -# Sort by : Teslagrad_Scroll_.jpg' -file_names.sort(key=lambda x: int(x.split('.')[0].split('_')[-1])) -it = iter(file_names) - -for y in range(0, SCROOLS_HEIGHT, IMAGE_HEIGHT): - for x in range(0, SCROOLS_WIDTH, IMAGE_WIDTH): - file_name = next(it) - img = Image.open(file_name) - - image.paste(img, (x, y)) - -image.save('scrolls.jpg') -image.show() diff --git a/Pascal Triangle fact.py b/Pascal Triangle fact.py index 34e3206eb..b131d2a92 100644 --- a/Pascal Triangle fact.py +++ b/Pascal Triangle fact.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import math @@ -9,4 +9,10 @@ N = 8 for n in range(N): - print(n, [math.factorial(n) // (math.factorial(m) * math.factorial(n - m)) for m in range(n+1)]) + print( + n, + [ + math.factorial(n) // (math.factorial(m) * math.factorial(n - m)) + for m in range(n + 1) + ], + ) diff --git a/Pascal Triangle pow11.py b/Pascal Triangle pow11.py index 9f93a114b..964cfe19e 100644 --- a/Pascal Triangle pow11.py +++ b/Pascal Triangle pow11.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://www.delftstack.com/howto/python/python-pascal-triangle/#print-pascal-s-triangle-by-computing-the-power-of-11-in-python @@ -13,4 +13,4 @@ N = 5 # NOTE: MAXIMUM for n in range(N): - print(list(map(int, str(11 ** n)))) + print(list(map(int, str(11**n)))) diff --git a/Pipe/Pipe.py b/Pipe/Pipe.py index 96048b125..17cf75506 100644 --- a/Pipe/Pipe.py +++ b/Pipe/Pipe.py @@ -1,20 +1,22 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## Example of using pipe module + # https://github.com/JulienPalard/Pipe # ru: http://habrahabr.ru/post/117679/ import pipe -if __name__ == '__main__': + +if __name__ == "__main__": print((i for i in range(10)) | pipe.as_list) # tuple to list - print([i for i in range(10)] | pipe.as_tuple) # list to tuple - print(((1,1), ('a', 2), (3, 'd')) | pipe.as_dict) # tuple to dict + print([i for i in range(10)] | pipe.as_tuple) # list to tuple + print(((1, 1), ("a", 2), (3, "d")) | pipe.as_dict) # tuple to dict print() # list of even numbers l = (i for i in range(10)) | pipe.where(lambda x: x % 2 is 0) | pipe.as_list c = l | pipe.count # count elements - print("List: {}, count: {}".format(l, c)) + print(f"List: {l}, count: {c}") print() # custom pipe: @@ -22,4 +24,4 @@ def custom_add(x): return sum(x) - print([1,2,3,4] | custom_add) # = 10 \ No newline at end of file + print([1, 2, 3, 4] | custom_add) # = 10 diff --git a/Printing all instances of a class/use__garbage_collector__gc.py b/Printing all instances of a class/use__garbage_collector__gc.py index 7016ddd00..d407f84e8 100644 --- a/Printing all instances of a class/use__garbage_collector__gc.py +++ b/Printing all instances of a class/use__garbage_collector__gc.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/328882/5909792 @@ -15,7 +15,7 @@ def get_instances(class_: type) -> [type]: class X: - def __init__(self, name): + def __init__(self, name) -> None: self.name = name diff --git a/Printing all instances of a class/use__mixin_and_weakrefs.py b/Printing all instances of a class/use__mixin_and_weakrefs.py index 92bced1c7..3cca80828 100644 --- a/Printing all instances of a class/use__mixin_and_weakrefs.py +++ b/Printing all instances of a class/use__mixin_and_weakrefs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/328882/5909792 @@ -14,7 +14,7 @@ class KeepRefs: __refs__ = defaultdict(list) - def __init__(self): + def __init__(self) -> None: self.__refs__[self.__class__].append(weakref.ref(self)) @classmethod @@ -26,7 +26,7 @@ def get_instances(cls): class X(KeepRefs): - def __init__(self, name): + def __init__(self, name) -> None: super().__init__() self.name = name diff --git a/PyGithub_examples/config.py b/PyGithub_examples/config.py index 940a77379..88e342a0e 100644 --- a/PyGithub_examples/config.py +++ b/PyGithub_examples/config.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os @@ -9,13 +9,12 @@ DIR = Path(__file__).resolve().parent -TOKEN_FILE_NAME = DIR / 'TOKEN.txt' +TOKEN_FILE_NAME = DIR / "TOKEN.txt" -TOKEN = os.environ.get('TOKEN') or TOKEN_FILE_NAME.read_text('utf-8').strip() +TOKEN = os.environ.get("TOKEN") or TOKEN_FILE_NAME.read_text("utf-8").strip() # http://user:password@proxy_host:proxy_port PROXY = None if PROXY: - import os - os.environ['http_proxy'] = PROXY + os.environ["http_proxy"] = PROXY diff --git a/PyGithub_examples/create_repo.py b/PyGithub_examples/create_repo.py index 181d18239..205493b1c 100644 --- a/PyGithub_examples/create_repo.py +++ b/PyGithub_examples/create_repo.py @@ -1,16 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pygithub from github import Github + from config import LOGIN, PASSWORD + # NOTE: delete repo: Settings -> -> # or repo.delete() -NEW_REPO = 'Test-Repo' +NEW_REPO = "Test-Repo" gh = Github(LOGIN, PASSWORD) diff --git a/PyGithub_examples/download_gist_files.py b/PyGithub_examples/download_gist_files.py index 5537844d5..bff993a31 100644 --- a/PyGithub_examples/download_gist_files.py +++ b/PyGithub_examples/download_gist_files.py @@ -1,33 +1,38 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import os + # pip install pygithub from github import Github + from config import LOGIN, PASSWORD + gh = Github(LOGIN, PASSWORD) -gist = gh.get_gist('2f80a34fb601cd685353') +gist = gh.get_gist("2f80a34fb601cd685353") # Filter by commit version history_list = [ - x for x in gist.history if x.version in [ - 'd4572ecd5b66ac06a64973cd35ef71936ebf84cd', - '8040c03f7ee008b0d3ce315a34e4c364f164d939', - '798e7a89e02cac541e1866ac16278ce9a068cf68', - '758e49da06e0bb5b6b455e9035a47047ece714d0', + x + for x in gist.history + if x.version in [ + "d4572ecd5b66ac06a64973cd35ef71936ebf84cd", + "8040c03f7ee008b0d3ce315a34e4c364f164d939", + "798e7a89e02cac541e1866ac16278ce9a068cf68", + "758e49da06e0bb5b6b455e9035a47047ece714d0", ] ] -import os -if not os.path.exists('gist_file'): - os.mkdir('gist_file') +if not os.path.exists("gist_file"): + os.mkdir("gist_file") for history in history_list: for gist_file_name, gist_file in history.files.items(): - file_name = 'gist_file/{}_{}.txt'.format(history.version, gist_file_name) + file_name = f"gist_file/{history.version}_{gist_file_name}.txt" - with open(file_name, mode='w', encoding='utf-8') as f: + with open(file_name, mode="w", encoding="utf-8") as f: f.write(gist_file.content) diff --git a/PyGithub_examples/get_rate_limit_info.py b/PyGithub_examples/get_rate_limit_info.py index 5750b88e6..c90249a1c 100644 --- a/PyGithub_examples/get_rate_limit_info.py +++ b/PyGithub_examples/get_rate_limit_info.py @@ -1,22 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pygithub from github import Github + from config import LOGIN, PASSWORD + gh = Github(LOGIN, PASSWORD) -print('With auth:') -print(' rate_limiting:', gh.rate_limiting) -print(' rate_limiting_resettime:', gh.rate_limiting_resettime) -print(' gh.get_rate_limit():', gh.get_rate_limit()) +print("With auth:") +print(" rate_limiting:", gh.rate_limiting) +print(" rate_limiting_resettime:", gh.rate_limiting_resettime) +print(" gh.get_rate_limit():", gh.get_rate_limit()) print() gh = Github() -print('Without auth:') -print(' rate_limiting:', gh.rate_limiting) -print(' rate_limiting_resettime:', gh.rate_limiting_resettime) -print(' gh.get_rate_limit():', gh.get_rate_limit()) +print("Without auth:") +print(" rate_limiting:", gh.rate_limiting) +print(" rate_limiting_resettime:", gh.rate_limiting_resettime) +print(" gh.get_rate_limit():", gh.get_rate_limit()) diff --git a/PyGithub_examples/gist_history_to_sqlite_db.py b/PyGithub_examples/gist_history_to_sqlite_db.py index 4374ed063..8cd7ca839 100644 --- a/PyGithub_examples/gist_history_to_sqlite_db.py +++ b/PyGithub_examples/gist_history_to_sqlite_db.py @@ -1,21 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import sqlite3 from config import TOKEN def create_connect(): - import sqlite3 - return sqlite3.connect('gist_commits.sqlite') + return sqlite3.connect("gist_commits.sqlite") -def init_db(): +def init_db() -> None: # Создание базы и таблицы with create_connect() as connect: - connect.execute(''' + connect.execute( + """ CREATE TABLE IF NOT EXISTS GistFile ( id INTEGER PRIMARY KEY, commit_hash TEXT NOT NULL, @@ -25,16 +26,20 @@ def init_db(): CONSTRAINT raw_url_unique UNIQUE (commit_hash, raw_url) ); - ''') + """ + ) connect.commit() -if __name__ == '__main__': - init_db() +if __name__ == "__main__": + import traceback # pip install pygithub from github import Github + + init_db() + gh = Github(TOKEN) # # # # OR: @@ -43,31 +48,30 @@ def init_db(): # # documentation for more details.)", 'documentation_url': 'https://developer.github.com/v3/#rate-limiting'}" # gh = Github() - gist = gh.get_gist('2f80a34fb601cd685353') + gist = gh.get_gist("2f80a34fb601cd685353") print(gist) - print('History ({}):'.format(len(gist.history))) + print(f"History ({len(gist.history)}):") with create_connect() as connect: try: for history in reversed(gist.history): - print(' committed_at: {}, version: {}, files: {}'.format( - history.committed_at, history.version, history.files) + print( + f" committed_at: {history.committed_at}, version: {history.version}, files: {history.files}" ) - if 'gistfile1.txt' not in history.files: + if "gistfile1.txt" not in history.files: print(' Not found file "gistfile1.txt"!') continue - file = history.files['gistfile1.txt'] + file = history.files["gistfile1.txt"] # print(' url: {}'.format(file.raw_url)) # print(' [{}]: {}'.format(len(file.content), repr(file.content)[:150])) # print() connect.execute( - 'INSERT OR IGNORE INTO GistFile (commit_hash, committed_at, raw_url, content) VALUES (?, ?, ?, ?)', - (history.version, history.committed_at, file.raw_url, file.content) + "INSERT OR IGNORE INTO GistFile (commit_hash, committed_at, raw_url, content) VALUES (?, ?, ?, ?)", + (history.version, history.committed_at, file.raw_url, file.content), ) except Exception as e: - import traceback - print('ERROR: {}:\n\n{}'.format(e, traceback.format_exc())) + print(f"ERROR: {e}:\n\n{traceback.format_exc()}") diff --git a/PyGithub_examples/print_gist_history.py b/PyGithub_examples/print_gist_history.py index e7bad839c..e9839fcec 100644 --- a/PyGithub_examples/print_gist_history.py +++ b/PyGithub_examples/print_gist_history.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pygithub from github import Github + from config import LOGIN, PASSWORD + gh = Github(LOGIN, PASSWORD) # # # # OR: @@ -16,15 +18,15 @@ # # documentation for more details.)", 'documentation_url': 'https://developer.github.com/v3/#rate-limiting'}" # gh = Github() -gist = gh.get_gist('2f80a34fb601cd685353') +gist = gh.get_gist("2f80a34fb601cd685353") print(gist) -print('created_at: {}, updated_at: {}'.format(gist.created_at, gist.updated_at)) +print(f"created_at: {gist.created_at}, updated_at: {gist.updated_at}") print() -print('files ({}): {}'.format(len(gist.files), gist.files)) +print(f"files ({len(gist.files)}): {gist.files}") print() -print('history ({}):'.format(len(gist.history))) +print(f"history ({len(gist.history)}):") # From new to old: # for history in gist.history: @@ -34,11 +36,11 @@ # # First 10: for history in list(reversed(gist.history))[:10]: - print(' committed_at: {}, version: {}, files: {}'.format( - history.committed_at, history.version, history.files) + print( + f" committed_at: {history.committed_at}, version: {history.version}, files: {history.files}" ) - file = history.files['gistfile1.txt'] - print(' url: {}'.format(file.raw_url)) - print(' [{}]: {}'.format(len(file.content), repr(file.content)[:150])) + file = history.files["gistfile1.txt"] + print(f" url: {file.raw_url}") + print(f" [{len(file.content)}]: {repr(file.content)[:150]}") print() diff --git a/PyGithub_examples/print_user_gists.py b/PyGithub_examples/print_user_gists.py index 17e220213..456960f6e 100644 --- a/PyGithub_examples/print_user_gists.py +++ b/PyGithub_examples/print_user_gists.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pygithub from github import Github + from config import LOGIN, PASSWORD + # TODO: сделать версию с gui gh = Github(LOGIN, PASSWORD) @@ -19,6 +21,6 @@ print(i, gist.description, gist.html_url) for file, gist_file in gist.files.items(): - print(' ', file, gist_file.raw_url) + print(" ", file, gist_file.raw_url) print() diff --git a/PyGithub_examples/print_user_repo.py b/PyGithub_examples/print_user_repo.py index ba777502c..c01d1add6 100644 --- a/PyGithub_examples/print_user_repo.py +++ b/PyGithub_examples/print_user_repo.py @@ -1,17 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +from collections import Counter, defaultdict + # pip install pygithub from github import Github + from config import LOGIN, PASSWORD + # TODO: сделать версию с gui # Пользователь, чьи репозитории собираемся вывести в консоль -user = 'gil9red' +user = "gil9red" gh = Github(LOGIN, PASSWORD) @@ -19,14 +23,10 @@ repo_by_name_dict = {repo.name: repo for repo in gh.get_user(user).get_repos()} # Подсчет количества репозиториев по языкам -from collections import Counter - counter_langs = Counter(repo.language for _, repo in repo_by_name_dict.items()) counter_langs = sorted(counter_langs.items(), key=lambda x: x[1], reverse=True) # Словарь, у которого ключом является язык репозитория, значением -- объект репозитория -from collections import defaultdict - repo_by_lang_dict = defaultdict(list) for _, repo in repo_by_name_dict.items(): @@ -38,10 +38,11 @@ # Вывод репозиториев в соответствии их языку и популярности for lang, number in counter_langs: - print("{} ({}):".format(lang, number)) + print(f"{lang} ({number}):") for i, repo in enumerate(repo_by_lang_dict[lang], 1): - print(" {}. {}: '{}' ({} star): {}".format(i, repo.name, repo.description, - repo.stargazers_count, repo.html_url)) + print( + f" {i}. {repo.name}: '{repo.description}' ({repo.stargazers_count} star): {repo.html_url}" + ) print() diff --git a/PyGithub_examples/search_by_code.py b/PyGithub_examples/search_by_code.py index 4b3ce1c1a..fc99b7b89 100644 --- a/PyGithub_examples/search_by_code.py +++ b/PyGithub_examples/search_by_code.py @@ -1,17 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +from base64 import b64decode as base64_to_text + # pip install pygithub from github import Github + from config import LOGIN, PASSWORD + gh = Github(LOGIN, PASSWORD) # print(list(gh.search_code('requests auth github filename:.py language:python')[:5])) -search_query = 'requests auth github filename:.py language:python' +search_query = "requests auth github filename:.py language:python" # print(gh.search_code(search_query).totalCount) # The Search API has a custom rate limit. For requests using Basic Authentication, OAuth, or client ID and @@ -35,12 +39,12 @@ print(dir(data[0])) print(data[0].url) print(data[0].content) -from base64 import b64decode as base64_to_text + print(base64_to_text(data[0].content.encode()).decode()) print(data[0].html_url) # get user from repo url -user = data[0].html_url.split('/')[3] +user = data[0].html_url.split("/")[3] print(user) # i = 1 diff --git a/PyMsgBox__examples/alert.py b/PyMsgBox__examples/alert.py index 55574341f..7fd767637 100644 --- a/PyMsgBox__examples/alert.py +++ b/PyMsgBox__examples/alert.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install PyMsgBox from pymsgbox import alert -button = alert(text='My Text', title='My Title', button='OK') + + +button = alert(text="My Text", title="My Title", button="OK") print(button) diff --git a/PyMsgBox__examples/confirm.py b/PyMsgBox__examples/confirm.py index 939955a0f..67bddc287 100644 --- a/PyMsgBox__examples/confirm.py +++ b/PyMsgBox__examples/confirm.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install PyMsgBox from pymsgbox import confirm -button = confirm(text='My Text', title='My Title', buttons=['OK', 'Cancel']) + + +button = confirm(text="My Text", title="My Title", buttons=["OK", "Cancel"]) print(button) diff --git a/PyMsgBox__examples/password.py b/PyMsgBox__examples/password.py index 27af73228..2b2d0bd8e 100644 --- a/PyMsgBox__examples/password.py +++ b/PyMsgBox__examples/password.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install PyMsgBox from pymsgbox import password -result = password(text='My Text', title='My Title', default='Default Text', mask='*') + + +result = password(text="My Text", title="My Title", default="Default Text", mask="*") print(result) -result = password(text='My Text', title='My Title', default='Default Text', mask='?') +result = password(text="My Text", title="My Title", default="Default Text", mask="?") print(result) diff --git a/PyMsgBox__examples/prompt.py b/PyMsgBox__examples/prompt.py index 15fe0f43b..a1d3a57f4 100644 --- a/PyMsgBox__examples/prompt.py +++ b/PyMsgBox__examples/prompt.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install PyMsgBox from pymsgbox import prompt -result = prompt(text='My Text', title='My Title', default='Default Text') + + +result = prompt(text="My Text", title="My Title", default="Default Text") print(result) diff --git a/PyOpenGLExample/checkerboard.py b/PyOpenGLExample/checkerboard.py index e37968c4b..c95783a60 100644 --- a/PyOpenGLExample/checkerboard.py +++ b/PyOpenGLExample/checkerboard.py @@ -10,7 +10,7 @@ """ -def initFun(): +def initFun() -> None: glClearColor(1.0, 1.0, 1.0, 0.0) glColor3f(0.0, 0.0, 0.0) glMatrixMode(GL_PROJECTION) @@ -18,7 +18,7 @@ def initFun(): gluOrtho2D(0.0, 640.0, 0.0, 480.0) -def displayFun(): +def displayFun() -> None: CELL_SIZE = 40 glClear(GL_COLOR_BUFFER_BIT) @@ -32,7 +32,7 @@ def displayFun(): y2 = y1 + CELL_SIZE if (i % 2 and not j % 2) or (not i % 2 and j % 2): - glColor3f(0xff, 0xff, 0xff) # white + glColor3f(0xFF, 0xFF, 0xFF) # white else: glColor3f(0x0, 0x0, 0x0) # black @@ -41,11 +41,11 @@ def displayFun(): glFlush() -if __name__ == '__main__': +if __name__ == "__main__": glutInit() glutInitWindowSize(640, 480) glutCreateWindow(b"DrawSquares") glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB) glutDisplayFunc(displayFun) initFun() - glutMainLoop() \ No newline at end of file + glutMainLoop() diff --git a/PyOpenGLExample/gingerbread.py b/PyOpenGLExample/gingerbread.py index 955e9d203..3a788504c 100644 --- a/PyOpenGLExample/gingerbread.py +++ b/PyOpenGLExample/gingerbread.py @@ -18,7 +18,7 @@ y = 121 -def initFun(): +def initFun() -> None: glClearColor(1.0, 1.0, 1.0, 0.0) glColor3f(0.0, 0.0, 0.0) glPointSize(4.0) @@ -27,7 +27,7 @@ def initFun(): gluOrtho2D(0.0, 640.0, 0.0, 480.0) -def mouseFun(button, state, xIn, yIn): +def mouseFun(button, state, xIn, yIn) -> None: global x global y if button == GLUT_LEFT_BUTTON and state == GLUT_DOWN: @@ -37,7 +37,7 @@ def mouseFun(button, state, xIn, yIn): glutPostRedisplay() -def displayFun(): +def displayFun() -> None: global x global y glClear(GL_COLOR_BUFFER_BIT) @@ -54,7 +54,7 @@ def displayFun(): glFlush() -if __name__ == '__main__': +if __name__ == "__main__": glutInit() glutInitWindowSize(640, 480) glutCreateWindow(b"Gingerbread") @@ -62,4 +62,4 @@ def displayFun(): glutDisplayFunc(displayFun) glutMouseFunc(mouseFun) initFun() - glutMainLoop() \ No newline at end of file + glutMainLoop() diff --git a/PyOpenGLExample/helloworld.py b/PyOpenGLExample/helloworld.py index 6a6fbb4cc..eaaa108d3 100644 --- a/PyOpenGLExample/helloworld.py +++ b/PyOpenGLExample/helloworld.py @@ -14,7 +14,7 @@ """ -def initFun(): +def initFun() -> None: glClearColor(1.0, 1.0, 1.0, 0.0) glColor3f(0.0, 0.0, 0.0) glPointSize(4.0) @@ -23,7 +23,7 @@ def initFun(): gluOrtho2D(0.0, 640.0, 0.0, 480.0) -def displayFun(): +def displayFun() -> None: glClear(GL_COLOR_BUFFER_BIT) glBegin(GL_POINTS) glVertex2i(100, 50) @@ -33,11 +33,11 @@ def displayFun(): glFlush() -if __name__ == '__main__': +if __name__ == "__main__": glutInit() glutInitWindowSize(640, 480) glutCreateWindow(b"Drawdots") glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB) glutDisplayFunc(displayFun) initFun() - glutMainLoop() \ No newline at end of file + glutMainLoop() diff --git a/PyOpenGLExample/maze.py b/PyOpenGLExample/maze.py index 6ecb74b0b..0ed046b6c 100644 --- a/PyOpenGLExample/maze.py +++ b/PyOpenGLExample/maze.py @@ -38,7 +38,7 @@ class MazeCell: - def __init__(self, x, y): + def __init__(self, x, y) -> None: self.x = x self.y = y self.wall_north = True @@ -49,7 +49,7 @@ def __init__(self, x, y): class Maze: - def __init__(self, dimx, dimy): + def __init__(self, dimx, dimy) -> None: self.numx = dimx self.numy = dimy @@ -71,32 +71,61 @@ def __init__(self, dimx, dimy): cell.visited = True # If a cell is not connected, see if we can connected it to the path - if cell.wall_north and cell.wall_east and cell.wall_south and cell.wall_north: - if cell.x > 0 and self.cells[(cell.x - 1) + (cell.y + 0) * dimx].visited: + if ( + cell.wall_north + and cell.wall_east + and cell.wall_south + and cell.wall_north + ): + if ( + cell.x > 0 + and self.cells[(cell.x - 1) + (cell.y + 0) * dimx].visited + ): cell.wall_west = False self.cells[(cell.x - 1) + (cell.y + 0) * dimx].wall_east = False - elif cell.x < (dimx - 2) and self.cells[(cell.x + 1) + (cell.y + 0) * dimx].visited: + elif ( + cell.x < (dimx - 2) + and self.cells[(cell.x + 1) + (cell.y + 0) * dimx].visited + ): cell.wall_east = False self.cells[(cell.x + 1) + (cell.y + 0) * dimx].wall_west = False - elif cell.y > 0 and self.cells[(cell.x + 0) + (cell.y - 1) * dimx].visited: + elif ( + cell.y > 0 + and self.cells[(cell.x + 0) + (cell.y - 1) * dimx].visited + ): cell.wall_south = False self.cells[(cell.x + 0) + (cell.y - 1) * dimx].wall_north = False - elif cell.y < (dimy - 2) and self.cells[(cell.x + 0) + (cell.y + 1) * dimx].visited: + elif ( + cell.y < (dimy - 2) + and self.cells[(cell.x + 0) + (cell.y + 1) * dimx].visited + ): cell.wall_north = False self.cells[(cell.x + 0) + (cell.y + 1) * dimx].wall_south = False # Append neighbors if they are not yet in the path. num = 0 - if cell.x > 0 and not self.cells[(cell.x - 1) + (cell.y + 0) * dimx].visited: + if ( + cell.x > 0 + and not self.cells[(cell.x - 1) + (cell.y + 0) * dimx].visited + ): cell_list.append(self.cells[(cell.x - 1) + (cell.y + 0) * dimx]) num += 1 - if cell.x < (dimx - 1) and not self.cells[(cell.x + 1) + (cell.y + 0) * dimx].visited: + if ( + cell.x < (dimx - 1) + and not self.cells[(cell.x + 1) + (cell.y + 0) * dimx].visited + ): cell_list.append(self.cells[(cell.x + 1) + (cell.y + 0) * dimx]) num += 1 - if cell.y < (dimy - 1) and not self.cells[(cell.x + 0) + (cell.y + 1) * dimx].visited: + if ( + cell.y < (dimy - 1) + and not self.cells[(cell.x + 0) + (cell.y + 1) * dimx].visited + ): cell_list.append(self.cells[(cell.x + 0) + (cell.y + 1) * dimx]) num += 1 - if cell.y > 0 and not self.cells[(cell.x + 0) + (cell.y - 1) * dimx].visited: + if ( + cell.y > 0 + and not self.cells[(cell.x + 0) + (cell.y - 1) * dimx].visited + ): cell_list.append(self.cells[(cell.x + 0) + (cell.y - 1) * dimx]) num += 1 @@ -123,11 +152,11 @@ def __init__(self, dimx, dimy): cell.wall_east = False conn.wall_west = False - # Push it back on the list since this is our next node + # Push it back on the list since this is our next node cell_list.append(conn) - def draw(self): - """ Draws the field """ + def draw(self) -> None: + """Draws the field""" glBegin(GL_LINES) for i in range(0, self.numx * self.numy): x = i % self.numx @@ -155,7 +184,7 @@ def draw(self): maze = Maze(MAZECOLS, MAZEROWS) -def init_fun(): +def init_fun() -> None: glClearColor(1.0, 1.0, 1.0, 0.0) glColor3f(0.0, 0.0, 0.0) glMatrixMode(GL_PROJECTION) @@ -164,17 +193,17 @@ def init_fun(): gluOrtho2D(-1.0, WIDTH, -1.0, HEIGHT) -def display_fun(): +def display_fun() -> None: glClear(GL_COLOR_BUFFER_BIT) maze.draw() glFlush() -if __name__ == '__main__': +if __name__ == "__main__": glutInit() glutInitWindowSize(WIDTH, HEIGHT) glutCreateWindow(b"Maze") glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB) glutDisplayFunc(display_fun) init_fun() - glutMainLoop() \ No newline at end of file + glutMainLoop() diff --git a/PyOpenGLExample/mouse.py b/PyOpenGLExample/mouse.py index e8b8ec2df..77a6f7bbb 100644 --- a/PyOpenGLExample/mouse.py +++ b/PyOpenGLExample/mouse.py @@ -18,7 +18,7 @@ class Point: - def __init__(self, x, y): + def __init__(self, x, y) -> None: self.x = x self.y = y @@ -26,7 +26,7 @@ def __init__(self, x, y): points = [] -def initFun(): +def initFun() -> None: glClearColor(1.0, 1.0, 1.0, 0.0) glColor3f(0.0, 0.0, 0.0) glMatrixMode(GL_PROJECTION) @@ -34,7 +34,7 @@ def initFun(): gluOrtho2D(0.0, 640.0, 0.0, 480.0) -def displayFun(): +def displayFun() -> None: global points glClear(GL_COLOR_BUFFER_BIT) glBegin(GL_LINE_STRIP) @@ -45,7 +45,7 @@ def displayFun(): glFlush() -def mouseFun(button, state, x, y): +def mouseFun(button, state, x, y) -> None: global points if button == GLUT_LEFT_BUTTON and state == GLUT_DOWN: p = Point(x, 480 - y) @@ -56,7 +56,7 @@ def mouseFun(button, state, x, y): glutPostRedisplay() -if __name__ == '__main__': +if __name__ == "__main__": glutInit() glutInitWindowSize(640, 480) glutCreateWindow(b"Polyline") @@ -64,4 +64,4 @@ def mouseFun(button, state, x, y): glutDisplayFunc(displayFun) glutMouseFunc(mouseFun) initFun() - glutMainLoop() \ No newline at end of file + glutMainLoop() diff --git a/PyOpenGLExample/print_text.py b/PyOpenGLExample/print_text.py index c943cc6a3..1a6c62e32 100644 --- a/PyOpenGLExample/print_text.py +++ b/PyOpenGLExample/print_text.py @@ -9,7 +9,7 @@ height = 0 -def glSetup(w, h): +def glSetup(w, h) -> None: global width, height width = w height = h @@ -31,7 +31,7 @@ def glSetup(w, h): glMatrixMode(GL_MODELVIEW) -def glResize(w, h): +def glResize(w, h) -> None: global width, height width = w height = h @@ -43,18 +43,18 @@ def glResize(w, h): glMatrixMode(GL_MODELVIEW) -def glSetupCam(): +def glSetupCam() -> None: gluPerspective(45.0, float(width) / float(height), 0.1, 100.0) gluLookAt(0, 0, 5, 0, 0, 0, 0, 1, 0) -def glDraw(): +def glDraw() -> None: glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glLoadIdentity() # render objects here - # print text after rendering world so it is on top + # print text after rendering world so it is on top glut_print(10, 15, GLUT_BITMAP_9_BY_15, "Hello World", 1.0, 1.0, 1.0, 1.0) @@ -62,7 +62,7 @@ def glDraw(): # glut_print adapted from http://stackoverflow.com/questions/12837747/print-text-with-glut-and-python -def glut_print(x, y, font, text, r, g, b, a): +def glut_print(x, y, font, text, r, g, b, a) -> None: glMatrixMode(GL_PROJECTION) glLoadIdentity() gluOrtho2D(0.0, width, height, 0.0) @@ -80,13 +80,13 @@ def glut_print(x, y, font, text, r, g, b, a): glDisable(GL_BLEND) -def keyPressed(*args): - ESCAPE = b'\x1b' +def keyPressed(*args) -> None: + ESCAPE = b"\x1b" if args[0] == ESCAPE: sys.exit() -if __name__ == '__main__': +if __name__ == "__main__": glutInit() glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH) @@ -100,8 +100,8 @@ def keyPressed(*args): glutReshapeFunc(glResize) glutKeyboardFunc(keyPressed) - #setup things up + # setup things up glSetup(640, 480) - #start rendering - glutMainLoop() \ No newline at end of file + # start rendering + glutMainLoop() diff --git a/PyOpenGLExample/reshape.py b/PyOpenGLExample/reshape.py index b781e2899..d6db419d7 100644 --- a/PyOpenGLExample/reshape.py +++ b/PyOpenGLExample/reshape.py @@ -19,7 +19,7 @@ """ -def init_fun(): +def init_fun() -> None: glClearColor(1.0, 1.0, 1.0, 0.0) glColor3f(0.0, 0.0, 0.0) glMatrixMode(GL_PROJECTION) @@ -27,7 +27,7 @@ def init_fun(): gluOrtho2D(-100, 100, -100, 100) -def reshape_fun(w, h): +def reshape_fun(w, h) -> None: glViewport(0, 0, w, h) # if w > h: # glViewport((w - h) / 2, 0, h, h) @@ -35,7 +35,7 @@ def reshape_fun(w, h): # glViewport(0, (h - w) / 2, w, w) -def display_fun(): +def display_fun() -> None: glClear(GL_COLOR_BUFFER_BIT) glColor3f(0.0, 0.0, 1.0) glutWireTeapot(40) @@ -50,7 +50,7 @@ def display_fun(): glFlush() -if __name__ == '__main__': +if __name__ == "__main__": glutInit() glutInitWindowSize(640, 480) glutCreateWindow(b"Viewport") @@ -58,4 +58,4 @@ def display_fun(): glutDisplayFunc(display_fun) glutReshapeFunc(reshape_fun) init_fun() - glutMainLoop() \ No newline at end of file + glutMainLoop() diff --git a/PyOpenGLExample/rosette.py b/PyOpenGLExample/rosette.py index a3125f16b..1668d4bc2 100644 --- a/PyOpenGLExample/rosette.py +++ b/PyOpenGLExample/rosette.py @@ -23,7 +23,7 @@ RADIUS = 95 -def init_fun(): +def init_fun() -> None: glClearColor(1.0, 1.0, 1.0, 0.0) glColor3f(0.0, 0.0, 0.0) glMatrixMode(GL_PROJECTION) @@ -31,7 +31,7 @@ def init_fun(): gluOrtho2D(-100, 100, -100, 100) -def reshape_fun(w, h): +def reshape_fun(w, h) -> None: glViewport(0, 0, w, h) # if w > h: @@ -40,7 +40,7 @@ def reshape_fun(w, h): # glViewport(0, (h - w) / 2, w, w) -def display_fun(): +def display_fun() -> None: glClear(GL_COLOR_BUFFER_BIT) glColor3f(0.0, 0.0, 1.0) xpts = [] @@ -58,7 +58,7 @@ def display_fun(): glFlush() -if __name__ == '__main__': +if __name__ == "__main__": glutInit() glutInitWindowSize(400, 400) glutCreateWindow(b"Rosette") @@ -66,4 +66,4 @@ def display_fun(): glutDisplayFunc(display_fun) glutReshapeFunc(reshape_fun) init_fun() - glutMainLoop() \ No newline at end of file + glutMainLoop() diff --git a/PyOpenGLExample/scatter.py b/PyOpenGLExample/scatter.py index 10d81e11b..85ec9bbf4 100644 --- a/PyOpenGLExample/scatter.py +++ b/PyOpenGLExample/scatter.py @@ -19,7 +19,7 @@ """ -def initFun(): +def initFun() -> None: glClearColor(1.0, 1.0, 1.0, 0.0) glColor3f(0.0, 0.0, 0.0) glPointSize(1.0) @@ -29,7 +29,7 @@ def initFun(): def getRandom(mode): - """ + """ Wrapper around several RNG's. If mode is 0 random.randint() is used, if mode is one a guassian distribution is used. """ @@ -39,7 +39,7 @@ def getRandom(mode): return random.gauss(200, 40) % 400 -def displayFun(): +def displayFun() -> None: glClear(GL_COLOR_BUFFER_BIT) glBegin(GL_POINTS) @@ -54,11 +54,11 @@ def displayFun(): glFlush() -if __name__ == '__main__': +if __name__ == "__main__": glutInit() glutInitWindowSize(400, 400) glutCreateWindow(b"Scatter") glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB) glutDisplayFunc(displayFun) initFun() - glutMainLoop() \ No newline at end of file + glutMainLoop() diff --git a/PyOpenGLExample/sierpinski.py b/PyOpenGLExample/sierpinski.py index bb1ca0d67..3e10e1897 100644 --- a/PyOpenGLExample/sierpinski.py +++ b/PyOpenGLExample/sierpinski.py @@ -21,7 +21,7 @@ """ -def initFun(): +def initFun() -> None: glClearColor(1.0, 1.0, 1.0, 0.0) glColor3f(0.0, 0.0, 0.0) glPointSize(1.0) @@ -30,7 +30,7 @@ def initFun(): gluOrtho2D(0.0, 640.0, 0.0, 480.0) -def displayFun(): +def displayFun() -> None: glClear(GL_COLOR_BUFFER_BIT) glBegin(GL_POINTS) @@ -49,7 +49,7 @@ def displayFun(): glFlush() -if __name__ == '__main__': +if __name__ == "__main__": # This is the current implementation, note that all points are recalculated on # a window update, for any other purpose than showing off with how simply it is # to create this eyecode you'd probably want to precalculate the dots and put them @@ -61,4 +61,4 @@ def displayFun(): glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB) glutDisplayFunc(displayFun) initFun() - glutMainLoop() \ No newline at end of file + glutMainLoop() diff --git a/PyOpenGLExample/squares.py b/PyOpenGLExample/squares.py index 364ba43c4..b75d7d0a2 100644 --- a/PyOpenGLExample/squares.py +++ b/PyOpenGLExample/squares.py @@ -16,7 +16,7 @@ """ -def initFun(): +def initFun() -> None: glClearColor(1.0, 1.0, 1.0, 0.0) glColor3f(0.0, 0.0, 0.0) glMatrixMode(GL_PROJECTION) @@ -24,21 +24,25 @@ def initFun(): gluOrtho2D(0.0, 640.0, 0.0, 480.0) -def displayFun(): +def displayFun() -> None: glClear(GL_COLOR_BUFFER_BIT) for i in range(0, 25): - gray = idx = random.randint(0, 25) / 25.0 + gray = random.randint(0, 25) / 25.0 glColor3f(gray, gray, gray) - glRecti(random.randint(0, 640), random.randint(0, 480), - random.randint(0, 640), random.randint(0, 480)) + glRecti( + random.randint(0, 640), + random.randint(0, 480), + random.randint(0, 640), + random.randint(0, 480), + ) glFlush() -if __name__ == '__main__': +if __name__ == "__main__": glutInit() glutInitWindowSize(640, 480) glutCreateWindow(b"DrawSquares") glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB) glutDisplayFunc(displayFun) initFun() - glutMainLoop() \ No newline at end of file + glutMainLoop() diff --git a/PyOpenGLExample/turtle.py b/PyOpenGLExample/turtle.py index 486b7ac49..4b8e94969 100644 --- a/PyOpenGLExample/turtle.py +++ b/PyOpenGLExample/turtle.py @@ -23,8 +23,8 @@ angle = 0.0 -def reset(): - """ Reset the position to the origin """ +def reset() -> None: + """Reset the position to the origin""" global curX global curY global angle @@ -34,20 +34,20 @@ def reset(): angle = 0.0 -def turnTo(deg): - """ Turn to a certain angle """ +def turnTo(deg) -> None: + """Turn to a certain angle""" global angle angle = deg -def turn(deg): - """ Turn a certain number of degrees """ +def turn(deg) -> None: + """Turn a certain number of degrees""" global angle angle += deg -def forw(len, visible): - """ Move forward over a certain distance """ +def forw(len, visible) -> None: + """Move forward over a certain distance""" global curX global curY tmpX = curX @@ -61,7 +61,7 @@ def forw(len, visible): glEnd() -def initFun(): +def initFun() -> None: glClearColor(1.0, 1.0, 1.0, 0.0) glColor3f(0.0, 0.0, 0.0) glMatrixMode(GL_PROJECTION) @@ -69,7 +69,7 @@ def initFun(): gluOrtho2D(-100, 100, -100, 100) -def reshapeFun(w, h): +def reshapeFun(w, h) -> None: glViewport(0, 0, w, h) # if w > h: # glViewport((w-h)/2,0,h,h) @@ -77,7 +77,7 @@ def reshapeFun(w, h): # glViewport(0,(h-w)/2,w,w) -def turtle_1(): +def turtle_1() -> None: glClear(GL_COLOR_BUFFER_BIT) reset() glColor3f(0.0, 0.0, 1.0) @@ -93,7 +93,7 @@ def turtle_1(): glFlush() -def turtle_2(): +def turtle_2() -> None: glClear(GL_COLOR_BUFFER_BIT) glColor3f(0.0, 0.0, 1.0) reset() @@ -106,7 +106,7 @@ def turtle_2(): glFlush() -def turtle_3(): +def turtle_3() -> None: glClear(GL_COLOR_BUFFER_BIT) glColor3f(0.0, 0.0, 1.0) reset() @@ -119,7 +119,7 @@ def turtle_3(): glFlush() -def turtle_4(): +def turtle_4() -> None: glClear(GL_COLOR_BUFFER_BIT) glColor3f(0.0, 0.0, 1.0) reset() @@ -132,7 +132,7 @@ def turtle_4(): glFlush() -def turtle_5(): +def turtle_5() -> None: glClear(GL_COLOR_BUFFER_BIT) glColor3f(0.0, 0.0, 1.0) reset() @@ -145,7 +145,7 @@ def turtle_5(): glFlush() -def turtle_6(): +def turtle_6() -> None: glClear(GL_COLOR_BUFFER_BIT) glColor3f(0.0, 0.0, 1.0) reset() @@ -159,7 +159,7 @@ def turtle_6(): glFlush() -def turtle_7(): +def turtle_7() -> None: glClear(GL_COLOR_BUFFER_BIT) glColor3f(0.0, 0.0, 1.0) reset() @@ -172,7 +172,7 @@ def turtle_7(): glFlush() -def turtle_8(): +def turtle_8() -> None: glClear(GL_COLOR_BUFFER_BIT) glColor3f(0.0, 0.0, 1.0) reset() @@ -192,7 +192,7 @@ def turtle_8(): glFlush() -def turtle_9(): +def turtle_9() -> None: glClear(GL_COLOR_BUFFER_BIT) glColor3f(0.0, 0.0, 1.0) reset() @@ -208,7 +208,7 @@ def turtle_9(): glFlush() -def turtle_10(): +def turtle_10() -> None: glClear(GL_COLOR_BUFFER_BIT) glColor3f(0.0, 0.0, 1.0) reset() @@ -226,7 +226,7 @@ def turtle_10(): glFlush() -if __name__ == '__main__': +if __name__ == "__main__": glutInit() glutInitWindowSize(400, 400) glutCreateWindow(b"Turtle") @@ -245,4 +245,4 @@ def turtle_10(): glutReshapeFunc(reshapeFun) initFun() - glutMainLoop() \ No newline at end of file + glutMainLoop() diff --git a/PyOpenGLExample/wireframe.py b/PyOpenGLExample/wireframe.py index e07ef2658..1d16c92d7 100644 --- a/PyOpenGLExample/wireframe.py +++ b/PyOpenGLExample/wireframe.py @@ -12,8 +12,8 @@ """ -def axis(length): - """ Draws an axis (basicly a line with a cone on top) """ +def axis(length) -> None: + """Draws an axis (basicly a line with a cone on top)""" glPushMatrix() glBegin(GL_LINES) glVertex3d(0, 0, 0) @@ -24,8 +24,8 @@ def axis(length): glPopMatrix() -def three_axis(length): - """ Draws an X, Y and Z-axis """ +def three_axis(length) -> None: + """Draws an X, Y and Z-axis""" glPushMatrix() # Z-axis @@ -42,7 +42,7 @@ def three_axis(length): glPopMatrix() -def display_fun(): +def display_fun() -> None: glMatrixMode(GL_PROJECTION) glLoadIdentity() glOrtho(-2.0 * 64 / 48.0, 2.0 * 64 / 48.0, -1.5, 1.5, 0.1, 100) @@ -56,7 +56,7 @@ def display_fun(): glColor3f(0.0, 0.0, 0.0) glPushMatrix() - glTranslated(0.5, 0.5, 0.5); + glTranslated(0.5, 0.5, 0.5) glutWireCube(1.0) glPopMatrix() @@ -106,11 +106,11 @@ def display_fun(): glFlush() -if __name__ == '__main__': +if __name__ == "__main__": glutInit() glutInitWindowSize(640, 480) glutCreateWindow(b"3D") glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB) glClearColor(1.0, 1.0, 1.0, 0.0) glutDisplayFunc(display_fun) - glutMainLoop() \ No newline at end of file + glutMainLoop() diff --git a/PySvn_examples/analys_commits.py b/PySvn_examples/analys_commits.py index 246f23353..1e32fbc42 100644 --- a/PySvn_examples/analys_commits.py +++ b/PySvn_examples/analys_commits.py @@ -1,18 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import config from common import get_log_list, get_log_list_by_author -import config + url = config.SVN_FILE_NAME log_list = get_log_list(url) -print('Total commits ({}):'.format(len(log_list))) +print(f"Total commits ({len(log_list)}):") author_by_log = get_log_list_by_author(url, log_list) -for author, logs in sorted(author_by_log.items(), key=lambda item: len(item[1]), reverse=True): - print(' {}: {}'.format(author, len(logs))) +for author, logs in sorted( + author_by_log.items(), key=lambda item: len(item[1]), reverse=True +): + print(f" {author}: {len(logs)}") diff --git a/PySvn_examples/commit_watcher.py b/PySvn_examples/commit_watcher.py index 07881a8ed..c5319e731 100644 --- a/PySvn_examples/commit_watcher.py +++ b/PySvn_examples/commit_watcher.py @@ -1,21 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import time + import config +from common import get_log_list + + url = config.URL_SVN -from common import get_log_list last_log = get_log_list(url, limit=1)[0] last_revision = last_log.revision -print('Start from commit rev{} by {}: "{}" in {}'.format( - last_revision, - last_log.author, - repr(last_log.msg), - last_log.date -)) +print( + f'Start from commit rev{last_revision} by {last_log.author}: "{repr(last_log.msg)}" in {last_log.date}' +) while True: log_list = get_log_list(url, revision_from=last_revision) @@ -26,17 +27,12 @@ last_log = log_list[-1] if last_revision != last_log.revision: - print('commits +{} from: rev{} by {}: "{}" in {}....{}\n'.format( - len(log_list), - last_revision, - last_log.author, - repr(last_log.msg), - last_log.date, - log_list - )) + print( + f'commits +{len(log_list)} from: rev{last_revision} by {last_log.author}: ' + f'"{repr(last_log.msg)}" in {last_log.date}....{log_list}\n' + ) last_revision = last_log.revision # Every 10 minutes - import time time.sleep(60 * 10) diff --git a/PySvn_examples/commits__draw_plot__by_all_authors.py b/PySvn_examples/commits__draw_plot__by_all_authors.py index 326f028c1..29a61d77e 100644 --- a/PySvn_examples/commits__draw_plot__by_all_authors.py +++ b/PySvn_examples/commits__draw_plot__by_all_authors.py @@ -1,41 +1,50 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import pandas as pd +import os from datetime import datetime +import pandas as pd +import matplotlib.pyplot as plt + +import config +from common import get_log_list_by_author + + # Название текущего файла без '.py' -import os plot_title = os.path.basename(__file__)[:-3] -import matplotlib.pyplot as plt fig = plt.figure(1) -fig.suptitle(plot_title, fontsize=14, fontweight='bold') +fig.suptitle(plot_title, fontsize=14, fontweight="bold") ax = fig.add_subplot(111) -import config -from common import get_log_list_by_author author_by_log = get_log_list_by_author(config.SVN_FILE_NAME) -list_top_author_by_log = sorted(author_by_log.items(), key=lambda item: len(item[1]), reverse=True) +list_top_author_by_log = sorted( + author_by_log.items(), key=lambda item: len(item[1]), reverse=True +) # Добавление на график информации о авторах for author, logs in list_top_author_by_log: # Сбор коммитов за месяц/год records = [datetime(log.date.year, log.date.month, 1) for log in logs] - df = pd.DataFrame(data=records, columns=['year_month']) - df_month = pd.DataFrame({'count': df.groupby("year_month").size()}).reset_index() - ax.plot(df_month['year_month'], df_month['count'], label='{} ({})'.format(author, len(logs))) + df = pd.DataFrame(data=records, columns=["year_month"]) + df_month = pd.DataFrame({"count": df.groupby("year_month").size()}).reset_index() + ax.plot( + df_month["year_month"], + df_month["count"], + label=f"{author} ({len(logs)})", + ) ax.legend() ax.grid() -ax.set_title('Commits') -ax.set_xlabel('Date') -ax.set_ylabel('Count') +ax.set_title("Commits") +ax.set_xlabel("Date") +ax.set_ylabel("Count") plt.gcf().autofmt_xdate() diff --git a/PySvn_examples/commits__draw_plot__by_single_author.py b/PySvn_examples/commits__draw_plot__by_single_author.py index a42ee0e2d..ca0edb58a 100644 --- a/PySvn_examples/commits__draw_plot__by_single_author.py +++ b/PySvn_examples/commits__draw_plot__by_single_author.py @@ -1,43 +1,51 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -AUTHOR = 'ipetrash' +import os +from datetime import datetime + +import pandas as pd +import matplotlib.pyplot as plt import config from common import get_log_list_by_author + + +AUTHOR = 'ipetrash' + author_by_log = get_log_list_by_author(config.SVN_FILE_NAME) # Сбор коммитов за месяц/год -from datetime import datetime records = [datetime(log.date.year, log.date.month, 1) for log in author_by_log[AUTHOR]] -import pandas as pd -df = pd.DataFrame(data=records, columns=['year_month']) +df = pd.DataFrame(data=records, columns=["year_month"]) print(df) -print('Total rows:', len(df)) +print("Total rows:", len(df)) -df_month = pd.DataFrame({'count': df.groupby("year_month").size()}).reset_index() +df_month = pd.DataFrame({"count": df.groupby("year_month").size()}).reset_index() print(df_month) print() # Название текущего файла без '.py' -import os plot_title = os.path.basename(__file__)[:-3] -import matplotlib.pyplot as plt fig = plt.figure(1) -fig.suptitle(plot_title, fontsize=14, fontweight='bold') +fig.suptitle(plot_title, fontsize=14, fontweight="bold") ax = fig.add_subplot(111) -ax.plot(df_month['year_month'], df_month['count'], label='{} ({})'.format(AUTHOR, len(author_by_log[AUTHOR]))) +ax.plot( + df_month["year_month"], + df_month["count"], + label=f"{AUTHOR} ({len(author_by_log[AUTHOR])})", +) ax.legend() ax.grid() -ax.set_title('Commits') -ax.set_xlabel('Date') -ax.set_ylabel('Count') +ax.set_title("Commits") +ax.set_xlabel("Date") +ax.set_ylabel("Count") plt.gcf().autofmt_xdate() diff --git a/PySvn_examples/commits__draw_plot__by_top5_authors.py b/PySvn_examples/commits__draw_plot__by_top5_authors.py index bb44fb9bb..31e034348 100644 --- a/PySvn_examples/commits__draw_plot__by_top5_authors.py +++ b/PySvn_examples/commits__draw_plot__by_top5_authors.py @@ -1,41 +1,50 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import pandas as pd +import os from datetime import datetime +import pandas as pd +import matplotlib.pyplot as plt + +import config +from common import get_log_list_by_author + + # Название текущего файла без '.py' -import os plot_title = os.path.basename(__file__)[:-3] -import matplotlib.pyplot as plt fig = plt.figure(1) -fig.suptitle(plot_title, fontsize=14, fontweight='bold') +fig.suptitle(plot_title, fontsize=14, fontweight="bold") ax = fig.add_subplot(111) -import config -from common import get_log_list_by_author author_by_log = get_log_list_by_author(config.SVN_FILE_NAME) -list_top5_author_by_log = sorted(author_by_log.items(), key=lambda item: len(item[1]), reverse=True)[:5] +list_top5_author_by_log = sorted( + author_by_log.items(), key=lambda item: len(item[1]), reverse=True +)[:5] # Добавление на график информации о авторах for author, logs in list_top5_author_by_log: # Сбор коммитов за месяц/год records = [datetime(log.date.year, log.date.month, 1) for log in logs] - df = pd.DataFrame(data=records, columns=['year_month']) - df_month = pd.DataFrame({'count': df.groupby("year_month").size()}).reset_index() - ax.plot(df_month['year_month'], df_month['count'], label='{} ({})'.format(author, len(logs))) + df = pd.DataFrame(data=records, columns=["year_month"]) + df_month = pd.DataFrame({"count": df.groupby("year_month").size()}).reset_index() + ax.plot( + df_month["year_month"], + df_month["count"], + label=f"{author} ({len(logs)})", + ) ax.legend() ax.grid() -ax.set_title('Commits') -ax.set_xlabel('Date') -ax.set_ylabel('Count') +ax.set_title("Commits") +ax.set_xlabel("Date") +ax.set_ylabel("Count") plt.gcf().autofmt_xdate() diff --git a/PySvn_examples/common.py b/PySvn_examples/common.py index 2d6bf871d..dc845bc8e 100644 --- a/PySvn_examples/common.py +++ b/PySvn_examples/common.py @@ -1,18 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/dsoprea/PySvn + +import os +from collections import defaultdict + +import svn.local +import svn.remote + + def get_log_list(url__or__file_name: str, revision_from=None, limit=None) -> list: - import os if os.path.exists(url__or__file_name): - import svn.local repo = svn.local.LocalClient(url__or__file_name) else: - import svn.remote repo = svn.remote.RemoteClient(url__or__file_name) return list(repo.log_default(revision_from=revision_from, limit=limit)) @@ -22,9 +27,7 @@ def get_log_list_by_author(url__or__file_name: str, log_list: list = None) -> di if not log_list: log_list = get_log_list(url__or__file_name) - from collections import defaultdict author_by_log = defaultdict(list) - for log in log_list: author_by_log[log.author].append(log) diff --git a/PySvn_examples/config.py b/PySvn_examples/config.py index 0f3e72989..f714e9e1d 100644 --- a/PySvn_examples/config.py +++ b/PySvn_examples/config.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -URL_SVN = 'svn+cplus://svn2.compassplus.ru/twrbs/csm/optt/dev/trunk' -SVN_FILE_NAME = 'E:/OPTT/optt_trunk' +URL_SVN = "svn+cplus://svn2.compassplus.ru/twrbs/csm/optt/dev/trunk" +SVN_FILE_NAME = "E:/OPTT/optt_trunk" diff --git a/PySvn_examples/filter_log_by_message__print_release_version.py b/PySvn_examples/filter_log_by_message__print_release_version.py index 22f0eef65..b1a1aada3 100644 --- a/PySvn_examples/filter_log_by_message__print_release_version.py +++ b/PySvn_examples/filter_log_by_message__print_release_version.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import svn.local import config -import svn.local + repo = svn.local.LocalClient(config.SVN_FILE_NAME) # OR: @@ -15,11 +16,15 @@ log_list = [log for log in repo.log_default()] -print('Total commits:', len(log_list)) +print("Total commits:", len(log_list)) print() -release_version_log_list = [log for log in log_list if log.msg and 'Release version' in log.msg] -print('Release version log ({}):'.format(len(release_version_log_list))) +release_version_log_list = [ + log for log in log_list if log.msg and "Release version" in log.msg +] +print(f"Release version log ({len(release_version_log_list)}):") for i, log in enumerate(release_version_log_list, 1): - print(' {:6}. [rev {}] {} {:15} "{}"'.format(i, log.revision, log.date, log.author, log.msg)) + print( + f' {i:6}. [rev {log.revision}] {log.date} {log.author:15} "{log.msg}"' + ) diff --git a/PySvn_examples/get_commit_by_revision.py b/PySvn_examples/get_commit_by_revision.py index 2505d13a6..40cac5fc1 100644 --- a/PySvn_examples/get_commit_by_revision.py +++ b/PySvn_examples/get_commit_by_revision.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import svn.remote import config -import svn.remote + repo = svn.remote.RemoteClient(config.URL_SVN) print(list(repo.log_default(revision_from=159807, limit=1))) diff --git a/PySvn_examples/print_jira_by_date.py b/PySvn_examples/print_jira_by_date.py index 29ff024af..29f21d50d 100644 --- a/PySvn_examples/print_jira_by_date.py +++ b/PySvn_examples/print_jira_by_date.py @@ -1,11 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re -PATTERN_GET_JIRA = re.compile(r'\[.*?\] \((\w+?-\d+?)\)') + +import config +import common + + +PATTERN_GET_JIRA = re.compile(r"\[.*?\] \((\w+?-\d+?)\)") def get_jira(msg): @@ -22,26 +27,21 @@ def get_jira(msg): return match.group(1) -import config -import common - log_list = common.get_log_list(config.SVN_FILE_NAME) # OR: # log_list = common.get_log_list(config.URL_SVN) -print('Total commits:', len(log_list)) +print("Total commits:", len(log_list)) -from collections import OrderedDict -month_by_jira_info = OrderedDict() - +month_by_jira_info = dict() for log in reversed(log_list): jira = get_jira(log.msg) if not jira: continue - key = log.date.strftime('%Y/%m') + key = log.date.strftime("%Y/%m") if key not in month_by_jira_info: month_by_jira_info[key] = set() @@ -50,7 +50,8 @@ def get_jira(msg): for month, jira_items in month_by_jira_info.items(): # Хитрая сортировка по двум параметрами: имя проекта и номер джиры - jira_items = sorted(jira_items, key=lambda x: (x.split('-')[0], int(x.split('-')[1]))) - - print('{}: ({})\t{}'.format(month, len(jira_items), jira_items)) + jira_items = sorted( + jira_items, key=lambda x: (x.split("-")[0], int(x.split("-")[1])) + ) + print(f"{month}: ({len(jira_items)})\t{jira_items}") diff --git a/QuadraticEquation/QuadraticEquation.py b/QuadraticEquation/QuadraticEquation.py index 10fd8a2a8..3a49e4bb0 100644 --- a/QuadraticEquation/QuadraticEquation.py +++ b/QuadraticEquation/QuadraticEquation.py @@ -1,33 +1,37 @@ # coding=utf-8 + +__author__ = "ipetrash" + + import argparse import math -__author__ = 'ipetrash' - def create_parser(): - return argparse.ArgumentParser(description="Finding the roots of a quadratic equation.") + return argparse.ArgumentParser( + description="Finding the roots of a quadratic equation." + ) def calculate_D(a, b, c): - return (b ** 2) - 4 * a * c + return (b**2) - 4 * a * c + - def calculate_Roots(a, b, D): sqrt_D = math.sqrt(D) x1 = (-b - sqrt_D) / (2 * a) x2 = (-b + sqrt_D) / (2 * a) return x1, x2 - -if __name__ == '__main__': + +if __name__ == "__main__": parse = create_parser() parse.parse_args() - a = int(raw_input("a=")) - b = int(raw_input("b=")) - c = int(raw_input("c=")) + a = int(input("a=")) + b = int(input("b=")) + c = int(input("c=")) D = calculate_D(a, b, c) print("D=%s" % D) if D > 0: @@ -35,4 +39,4 @@ def calculate_Roots(a, b, D): elif D is 0: print("Root x=%s" % (-b / (2 * a))) elif D < 0: - print("No roots.") \ No newline at end of file + print("No roots.") diff --git a/QuickChart__examples/common.py b/QuickChart__examples/common.py new file mode 100644 index 000000000..98ecabbd6 --- /dev/null +++ b/QuickChart__examples/common.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install quickchart.io +from quickchart import QuickChart + + +def get_chart() -> QuickChart: + qc = QuickChart() + qc.width = 500 + qc.height = 300 + qc.version = "2" + qc.background_color = "" + qc.config = { + "type": "doughnut", + "data": { + "datasets": [ + { + "data": [43, 21], + "backgroundColor": ["rgb(173 225 232)", "rgb(33 170 184)"], + "borderWidth": 0, + }, + ], + }, + "options": { + "cutoutPercentage": 80, + "legend": { + "display": "false", + }, + "plugins": { + "datalabels": { + "display": "false", + }, + }, + }, + } + return qc diff --git a/QuickChart__examples/draw_chart_on_other_image/input.jpg b/QuickChart__examples/draw_chart_on_other_image/input.jpg new file mode 100644 index 000000000..da7126dfb Binary files /dev/null and b/QuickChart__examples/draw_chart_on_other_image/input.jpg differ diff --git a/QuickChart__examples/draw_chart_on_other_image/main.py b/QuickChart__examples/draw_chart_on_other_image/main.py new file mode 100644 index 000000000..12c012ee5 --- /dev/null +++ b/QuickChart__examples/draw_chart_on_other_image/main.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys + +from io import BytesIO +from pathlib import Path + +# pip install requests +import requests + +# pip install pillow +from PIL import Image + +DIR = Path(__file__).resolve().absolute().parent +sys.path.append(str(DIR.parent)) +from common import get_chart + + +qc = get_chart() + +# Variant 1 +f = BytesIO(qc.get_bytes()) +img_chart = Image.open(f) + +img = Image.open("input.jpg") +Image.Image.paste(img, img_chart, (10, 10), mask=img_chart) +img.save("input_with_chart_v1.png") + +# Variant 2 +url = qc.get_url() +raw = requests.get(url, stream=True).raw +img_chart = Image.open(raw) + +img = Image.open("input.jpg") +Image.Image.paste(img, img_chart, (10, 10), mask=img_chart) +img.save("input_with_chart_v2.png") diff --git a/QuickChart__examples/get_bytes.py b/QuickChart__examples/get_bytes.py new file mode 100644 index 000000000..5ce63436e --- /dev/null +++ b/QuickChart__examples/get_bytes.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import get_chart + + +qc = get_chart() +data = qc.get_bytes() +print(data[:6]) +# b'\x89PNG\r\n' + +print(len(data)) +# 13061 diff --git a/QuickChart__examples/get_url__get_short_url.py b/QuickChart__examples/get_url__get_short_url.py new file mode 100644 index 000000000..be6fa2777 --- /dev/null +++ b/QuickChart__examples/get_url__get_short_url.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import get_chart + + +qc = get_chart() +print(qc.get_url()) +# https://quickchart.io/chart?c=%7B%22type%22%3A%22doughnut%22%2C%22data%22%3A%7B%22datasets%22%3A%5B%7B%22data%22%3A%5B43%2C21%5D%2C%22backgroundColor%22%3A%5B%22rgb%28173+225+232%29%22%2C%22rgb%2833+170+184%29%22%5D%2C%22borderWidth%22%3A0%7D%5D%7D%2C%22options%22%3A%7B%22cutoutPercentage%22%3A80%2C%22legend%22%3A%7B%22display%22%3A%22false%22%7D%2C%22plugins%22%3A%7B%22datalabels%22%3A%7B%22display%22%3A%22false%22%7D%7D%7D%7D&w=500&h=300&bkg=&devicePixelRatio=1.0&f=png&v=2 + +print(qc.get_short_url()) +# https://quickchart.io/chart/render/sf-9237a1bc-f7ce-4156-ac58-2452a5729e9e diff --git a/QuickChart__examples/to_file.py b/QuickChart__examples/to_file.py new file mode 100644 index 000000000..e2add1ba9 --- /dev/null +++ b/QuickChart__examples/to_file.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from common import get_chart + + +PATH = Path(__file__).resolve().absolute() +FILE_NAME = Path(f"{PATH}.png") + + +qc = get_chart() +qc.to_file(FILE_NAME) diff --git a/RLE_encode.py b/RLE_encode.py index 272e5bd40..feb2978b4 100644 --- a/RLE_encode.py +++ b/RLE_encode.py @@ -1,13 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.wikipedia.org/wiki/Кодирование_длин_серий # SOURCE: https://en.wikipedia.org/wiki/Run-length_encoding +import itertools + + def compress(text): """ Функция для сжатия текста, с повторяющимися последовательностями символов @@ -20,15 +23,14 @@ def compress(text): chars = list() - import itertools for char, same in itertools.groupby(text): count = len(tuple(same)) # number of repetitions chars.append(char if count == 1 else str(count) + char) - return ''.join(chars) + return "".join(chars) -if __name__ == '__main__': +if __name__ == "__main__": print(compress("aaaa")) print(compress("aaaabbbbb")) print(compress("aabcDDfaaaa")) diff --git a/RSSbashorg/RSSbashorg.py b/RSSbashorg/RSSbashorg.py index 11473978d..c1591337b 100644 --- a/RSSbashorg/RSSbashorg.py +++ b/RSSbashorg/RSSbashorg.py @@ -1,16 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import html import feedparser -rss = feedparser.parse('http://bash.im/rss/') + + +rss = feedparser.parse("http://bash.im/rss/") print(rss.feed.subtitle) for entry in rss.entries: - import html quote = html.unescape(entry.summary) - quote = quote.replace('
', '\n') + quote = quote.replace("
", "\n") - print('{0.title}: {0.link}\n{1}\n\n'.format(entry, quote)) + print(f"{entry.title}: {entry.link}\n{quote}\n\n") diff --git a/Random_example/main.py b/Random_example/main.py index 3a26e4f69..495d3dc2b 100644 --- a/Random_example/main.py +++ b/Random_example/main.py @@ -1,19 +1,22 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" + import random -if __name__ == '__main__': - direction = ["up", 'down', 'left', 'right'] - for i in range(5): - print("Directions: %s, random direction = %s" % (direction, random.choice(direction))) - print("random: %s" % random.random()) # rand float number - print("randrange(5): %s" % random.randrange(5)) # rand int number - print("randrange(0, 10): %s" % random.randrange(0, 10)) # rand int number - print("uniform(1, 10): %s" % random.uniform(1, 10)) # rand float number +direction = ["up", "down", "left", "right"] +for i in range(5): + print( + f"Directions: {direction}, random direction = {random.choice(direction)}" + ) + +print(f"random: {random.random()}") # rand float number +print(f"randrange(5): {random.randrange(5)}") # rand int number +print(f"randrange(0, 10): {random.randrange(0, 10)}") # rand int number +print(f"uniform(1, 10): {random.uniform(1, 10)}") # rand float number - print("Directions: %s" % direction) - random.shuffle(direction) # shuffle list - print("Shuffle directions: %s" % direction) +print(f"Directions: {direction}") +random.shuffle(direction) # shuffle list +print(f"Shuffle directions: {direction}") - print(random.sample(direction, 2)) # select two element \ No newline at end of file +print(random.sample(direction, 2)) # select two element diff --git a/ReadURLsFromFile/ReadURLsFromFile.py b/ReadURLsFromFile/ReadURLsFromFile.py index 8863c831d..4dbc56e31 100644 --- a/ReadURLsFromFile/ReadURLsFromFile.py +++ b/ReadURLsFromFile/ReadURLsFromFile.py @@ -1,18 +1,19 @@ # coding=utf-8 -import argparse +__author__ = "ipetrash" + -__author__ = 'ipetrash' +import argparse def create_parser(): parser = argparse.ArgumentParser(description="Read URLs from file with URL.") return parser -if __name__ == '__main__': + +if __name__ == "__main__": create_parser().parse_args() - file_path = raw_input("Input file path: ") - file = open(file_path, "r") - for line in file.readlines(): - print(line) - file.close() \ No newline at end of file + file_path = input("Input file path: ") + with open(file_path) as f: + for line in f.readlines(): + print(line) diff --git a/Reading_the_target_of_a_lnk_file/using__winshell.py b/Reading_the_target_of_a_lnk_file/using__winshell.py index 51f77a85e..2fa0a7ae2 100644 --- a/Reading_the_target_of_a_lnk_file/using__winshell.py +++ b/Reading_the_target_of_a_lnk_file/using__winshell.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from glob import glob import os +from glob import glob # pip install winshell import winshell -path_desktop_lnk = os.path.expanduser(r'~\Desktop\**\*.lnk') + +path_desktop_lnk = os.path.expanduser(r"~\Desktop\**\*.lnk") file_name = glob(path_desktop_lnk, recursive=True)[0] shortcut = winshell.shortcut(file_name) diff --git a/Recursive search exe/via glob.py b/Recursive search exe/via glob.py index 90abef127..bc49042d4 100644 --- a/Recursive search exe/via glob.py +++ b/Recursive search exe/via glob.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import glob -for file_name in glob.iglob('C://**/*.exe', recursive=True): + + +for file_name in glob.iglob("C://**/*.exe", recursive=True): print(file_name) diff --git a/Recursive search exe/via os.walk.py b/Recursive search exe/via os.walk.py index 1eadda79a..baca5f3cd 100644 --- a/Recursive search exe/via os.walk.py +++ b/Recursive search exe/via os.walk.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os -for root, dirs, files in os.walk('C://'): + + +for root, dirs, files in os.walk("C://"): for file in files: base_name, ext = os.path.splitext(file) - if ext.lower() == '.exe': + if ext.lower() == ".exe": file_name = os.path.join(root, file) file_name = os.path.normpath(file_name) print(file_name) diff --git a/Recursive search exe/via pathlib.py b/Recursive search exe/via pathlib.py new file mode 100644 index 000000000..e1f5196e5 --- /dev/null +++ b/Recursive search exe/via pathlib.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + + +for file_name in Path("C://").rglob("*.exe"): + print(file_name) diff --git a/RemovingDuplicatesFromList/RemovingDuplicatesFromList.py b/RemovingDuplicatesFromList/RemovingDuplicatesFromList.py index 1538fec61..af828a316 100644 --- a/RemovingDuplicatesFromList/RemovingDuplicatesFromList.py +++ b/RemovingDuplicatesFromList/RemovingDuplicatesFromList.py @@ -1,8 +1,9 @@ # coding=utf-8 -import argparse +__author__ = "ipetrash" + -__author__ = 'ipetrash' +import argparse def create_parser(): @@ -10,10 +11,10 @@ def create_parser(): return parser -if __name__ == '__main__': +if __name__ == "__main__": create_parser().parse_args() - list = raw_input("Enter a list of elements separated by a space: ").split(" ") - list = filter(lambda x: x is not "", list) + list = input("Enter a list of elements separated by a space: ").split(" ") + list = list(filter(None, list)) print("Source list: %s" % list) list.sort() print("Sorted list: %s" % list) @@ -21,4 +22,4 @@ def create_parser(): if list.count(x) > 1: list.remove(x) print("Result: %s" % list) - print("List: " + ", ".join(list)) \ No newline at end of file + print("List: " + ", ".join(list)) diff --git a/SNMP__pysnmp__examples/get_table__get_CPU.py b/SNMP__pysnmp__examples/get_table__get_CPU.py index 00b52408c..942ca665a 100644 --- a/SNMP__pysnmp__examples/get_table__get_CPU.py +++ b/SNMP__pysnmp__examples/get_table__get_CPU.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://www.oidview.com/mibs/0/SNMPv2-MIB.html @@ -10,11 +10,19 @@ from typing import Iterator # pip install pysnmp -from pysnmp.hlapi import nextCmd, SnmpEngine, CommunityData, UdpTransportTarget, ContextData, ObjectType, ObjectIdentity +from pysnmp.hlapi import ( + nextCmd, + SnmpEngine, + CommunityData, + UdpTransportTarget, + ContextData, + ObjectType, + ObjectIdentity, +) -def get_iterator(host: str, port: int = 161, community: str = '') -> Iterator: - hrProcessorLoad = '1.3.6.1.2.1.25.3.3.1.2' +def get_iterator(host: str, port: int = 161, community: str = "") -> Iterator: + hrProcessorLoad = "1.3.6.1.2.1.25.3.3.1.2" return nextCmd( SnmpEngine(), @@ -26,23 +34,23 @@ def get_iterator(host: str, port: int = 161, community: str = '') -> Iterator: ) -iterator = get_iterator('localhost', 161, 'public') +iterator = get_iterator("localhost", 161, "public") items = list(iterator) print(f"Number CPU cores: {len(items)}\n") for i, (error_indication, error_status, error_index, var_binds) in enumerate(items, 1): if error_indication: - print(f'Error indication: {error_indication}') + print(f"Error indication: {error_indication}") elif error_status: - at = '?' + at = "?" if error_index: at = var_binds[int(error_index) - 1][0] - print(f'Error status {error_status.prettyPrint()!r} at {at}') + print(f"Error status {error_status.prettyPrint()!r} at {at}") else: for var, value in var_binds: - print(f'{var} = {value}') - print(f'{var.getOid()} = {value}') - print(f'{var.prettyPrint()} = {value.prettyPrint()}') + print(f"{var} = {value}") + print(f"{var.getOid()} = {value}") + print(f"{var.prettyPrint()} = {value.prettyPrint()}") print() diff --git a/SNMP__pysnmp__examples/hello_world.py b/SNMP__pysnmp__examples/hello_world.py index 97345d8ff..202531c7e 100644 --- a/SNMP__pysnmp__examples/hello_world.py +++ b/SNMP__pysnmp__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://www.oidview.com/mibs/0/SNMPv2-MIB.html @@ -13,28 +13,28 @@ iterator = getCmd( SnmpEngine(), - CommunityData('public', mpModel=0), - UdpTransportTarget(('localhost', 161)), + CommunityData("public", mpModel=0), + UdpTransportTarget(("localhost", 161)), ContextData(), - ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysDescr', 0)), - ObjectType(ObjectIdentity('1.3.6.1.2.1.1.1.0')), # OID, same as above - ObjectType(ObjectIdentity('.1.3.6.1.2.1.1.1.0')), # OID, same as above + ObjectType(ObjectIdentity("SNMPv2-MIB", "sysDescr", 0)), + ObjectType(ObjectIdentity("1.3.6.1.2.1.1.1.0")), # OID, same as above + ObjectType(ObjectIdentity(".1.3.6.1.2.1.1.1.0")), # OID, same as above ) error_indication, error_status, error_index, var_binds = next(iterator) if error_indication: - print(f'Error indication: {error_indication}') + print(f"Error indication: {error_indication}") elif error_status: - at = '?' + at = "?" if error_index: at = var_binds[int(error_index) - 1][0] - print(f'Error status {error_status.prettyPrint()!r} at {at}') + print(f"Error status {error_status.prettyPrint()!r} at {at}") else: for var, value in var_binds: - print(f'{var} = {value}') - print(f'{var.getOid()} = {value}') - print(f'{var.prettyPrint()} = {value.prettyPrint()}') + print(f"{var} = {value}") + print(f"{var.getOid()} = {value}") + print(f"{var.prettyPrint()} = {value.prettyPrint()}") print() diff --git a/SNMP__pysnmp__examples/hello_world__fix_imports.py b/SNMP__pysnmp__examples/hello_world__fix_imports.py index fc8230727..61507386d 100644 --- a/SNMP__pysnmp__examples/hello_world__fix_imports.py +++ b/SNMP__pysnmp__examples/hello_world__fix_imports.py @@ -1,40 +1,48 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://www.oidview.com/mibs/0/SNMPv2-MIB.html # pip install pysnmp -from pysnmp.hlapi import getCmd, SnmpEngine, CommunityData, UdpTransportTarget, ContextData, ObjectType, ObjectIdentity +from pysnmp.hlapi import ( + getCmd, + SnmpEngine, + CommunityData, + UdpTransportTarget, + ContextData, + ObjectType, + ObjectIdentity, +) iterator = getCmd( SnmpEngine(), - CommunityData('public', mpModel=0), - UdpTransportTarget(('localhost', 161)), + CommunityData("public", mpModel=0), + UdpTransportTarget(("localhost", 161)), ContextData(), - ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysDescr', 0)), - ObjectType(ObjectIdentity('1.3.6.1.2.1.1.1.0')), # OID, same as above - ObjectType(ObjectIdentity('.1.3.6.1.2.1.1.1.0')), # OID, same as above + ObjectType(ObjectIdentity("SNMPv2-MIB", "sysDescr", 0)), + ObjectType(ObjectIdentity("1.3.6.1.2.1.1.1.0")), # OID, same as above + ObjectType(ObjectIdentity(".1.3.6.1.2.1.1.1.0")), # OID, same as above ) error_indication, error_status, error_index, var_binds = next(iterator) if error_indication: - print(f'Error indication: {error_indication}') + print(f"Error indication: {error_indication}") elif error_status: - at = '?' + at = "?" if error_index: at = var_binds[int(error_index) - 1][0] - print(f'Error status {error_status.prettyPrint()!r} at {at}') + print(f"Error status {error_status.prettyPrint()!r} at {at}") else: for var, value in var_binds: - print(f'{var} = {value}') - print(f'{var.getOid()} = {value}') - print(f'{var.prettyPrint()} = {value.prettyPrint()}') + print(f"{var} = {value}") + print(f"{var.getOid()} = {value}") + print(f"{var.prettyPrint()} = {value.prettyPrint()}") print() diff --git a/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/export_to_json.py b/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/export_to_json.py deleted file mode 100644 index 4c08e4e0e..000000000 --- a/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/export_to_json.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import os -import json - -from utils import get_bosses, convert_bosses_to_only_name - - -def export_to_json_str(bosses) -> str: - return json.dumps(bosses, ensure_ascii=False, indent=4) - - -def export_to_json(file_name, bosses): - dir_name = os.path.dirname(file_name) - os.makedirs(dir_name, exist_ok=True) - - json.dump(bosses, open(file_name, 'w', encoding='utf-8'), ensure_ascii=False, indent=4) - - -if __name__ == '__main__': - bosses = get_bosses() - bosses_only_names = convert_bosses_to_only_name(bosses) - - print(export_to_json_str(bosses_only_names)) - # { - # "Обязательные боссы": [ - # "Обезумевший рыцарь", - # "Краекан циклоп", - # "Безумный алхимик", - # "Фальшивый шут", - # "Краекан вирм", - # "Нетронутый инквизитор", - # "Третий агнец", - # "Иссушенный король", - # "Ведьма озера", - # "Бескожий и Архитектор", - # "Краекан дракон Скоурж", - # "Безымянный бог" - # ], - # "Опциональные боссы": [ - # "Немая бездна", - # "Королева улыбок", - # "Древо людей", - # "Выпотрошенная оболочка", - # "Отвратительный смрад", - # "Кран Ронин", - # "Мёрдиела Мол", - # "Бескровный принц", - # "Жаждущий", - # "Карсджоу Жестокий", - # "Забытый король" - # ] - # } - - export_to_json('dumps/bosses.json', bosses) - export_to_json('dumps/bosses__only_name.json', bosses_only_names) diff --git a/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/print_bosses.py b/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/print_bosses.py deleted file mode 100644 index 87324fc6a..000000000 --- a/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/print_bosses.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import Dict, List -from utils import get_bosses, Boss - - -def print_bosses(bosses: Dict[str, List[Boss]], only_names=False): - print('Salt and Sanctuary ({}):'.format(sum(len(i) for i in bosses.values()))) - - for category, bosses in bosses.items(): - print(' {} ({}):'.format(category, len(bosses))) - - for i, boss in enumerate(bosses, 1): - if only_names: - print(' {}. "{}"'.format(i, boss.name)) - else: - print(' {}. "{}": {}'.format(i, boss.name, boss.url)) - - print() - - print() - - -if __name__ == '__main__': - bosses_by_category = get_bosses() - print_bosses(bosses_by_category) - # Salt and Sanctuary (23): - # Обязательные боссы (12): - # 1. "Обезумевший рыцарь": https://saltandsanctuary.fandom.com/ru/wiki/%D0%9E%D0%B1%D0%B5%D0%B7%D1%83%D0%BC%D0%B5%D0%B2%D1%88%D0%B8%D0%B9_%D1%80%D1%8B%D1%86%D0%B0%D1%80%D1%8C - # 2. "Краекан циклоп": https://saltandsanctuary.fandom.com/ru/wiki/%D0%9A%D1%80%D0%B0%D0%B5%D0%BA%D0%B0%D0%BD_%D1%86%D0%B8%D0%BA%D0%BB%D0%BE%D0%BF - # 3. "Безумный алхимик": https://saltandsanctuary.fandom.com/ru/wiki/%D0%91%D0%B5%D0%B7%D1%83%D0%BC%D0%BD%D1%8B%D0%B9_%D0%B0%D0%BB%D1%85%D0%B8%D0%BC%D0%B8%D0%BA - # 4. "Фальшивый шут": https://saltandsanctuary.fandom.com/ru/wiki/%D0%A4%D0%B0%D0%BB%D1%8C%D1%88%D0%B8%D0%B2%D1%8B%D0%B9_%D1%88%D1%83%D1%82 - # 5. "Краекан вирм": https://saltandsanctuary.fandom.com/ru/wiki/%D0%9A%D1%80%D0%B0%D0%B5%D0%BA%D0%B0%D0%BD_%D0%B2%D0%B8%D1%80%D0%BC - # 6. "Нетронутый инквизитор": https://saltandsanctuary.fandom.com/ru/wiki/%D0%9D%D0%B5%D1%82%D1%80%D0%BE%D0%BD%D1%83%D1%82%D1%8B%D0%B9_%D0%B8%D0%BD%D0%BA%D0%B2%D0%B8%D0%B7%D0%B8%D1%82%D0%BE%D1%80 - # 7. "Третий агнец": https://saltandsanctuary.fandom.com/ru/wiki/%D0%A2%D1%80%D0%B5%D1%82%D0%B8%D0%B9_%D0%B0%D0%B3%D0%BD%D0%B5%D1%86 - # 8. "Иссушенный король": https://saltandsanctuary.fandom.com/ru/wiki/%D0%98%D1%81%D1%81%D1%83%D1%88%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9_%D0%BA%D0%BE%D1%80%D0%BE%D0%BB%D1%8C - # 9. "Ведьма озера": https://saltandsanctuary.fandom.com/ru/wiki/%D0%92%D0%B5%D0%B4%D1%8C%D0%BC%D0%B0_%D0%BE%D0%B7%D0%B5%D1%80%D0%B0 - # 10. "Бескожий и Архитектор": https://saltandsanctuary.fandom.com/ru/wiki/%D0%91%D0%B5%D1%81%D0%BA%D0%BE%D0%B6%D0%B8%D0%B9_%D0%B8_%D0%90%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D0%BE%D1%80 - # 11. "Краекан дракон Скоурж": https://saltandsanctuary.fandom.com/ru/wiki/%D0%9A%D1%80%D0%B0%D0%B5%D0%BA%D0%B0%D0%BD_%D0%B4%D1%80%D0%B0%D0%BA%D0%BE%D0%BD_%D0%A1%D0%BA%D0%BE%D1%83%D1%80%D0%B6 - # 12. "Безымянный бог": https://saltandsanctuary.fandom.com/ru/wiki/%D0%91%D0%B5%D0%B7%D1%8B%D0%BC%D1%8F%D0%BD%D0%BD%D1%8B%D0%B9_%D0%B1%D0%BE%D0%B3 - # - # Опциональные боссы (11): - # 1. "Немая бездна": https://saltandsanctuary.fandom.com/ru/wiki/%D0%9D%D0%B5%D0%BC%D0%B0%D1%8F_%D0%B1%D0%B5%D0%B7%D0%B4%D0%BD%D0%B0 - # 2. "Королева улыбок": https://saltandsanctuary.fandom.com/ru/wiki/%D0%9A%D0%BE%D1%80%D0%BE%D0%BB%D0%B5%D0%B2%D0%B0_%D1%83%D0%BB%D1%8B%D0%B1%D0%BE%D0%BA - # 3. "Древо людей": https://saltandsanctuary.fandom.com/ru/wiki/%D0%94%D1%80%D0%B5%D0%B2%D0%BE_%D0%BB%D1%8E%D0%B4%D0%B5%D0%B9 - # 4. "Выпотрошенная оболочка": https://saltandsanctuary.fandom.com/ru/wiki/%D0%92%D1%8B%D0%BF%D0%BE%D1%82%D1%80%D0%BE%D1%88%D0%B5%D0%BD%D0%BD%D0%B0%D1%8F_%D0%BE%D0%B1%D0%BE%D0%BB%D0%BE%D1%87%D0%BA%D0%B0 - # 5. "Отвратительный смрад": https://saltandsanctuary.fandom.com/ru/wiki/%D0%9E%D1%82%D0%B2%D1%80%D0%B0%D1%82%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D1%81%D0%BC%D1%80%D0%B0%D0%B4 - # 6. "Кран Ронин": https://saltandsanctuary.fandom.com/ru/wiki/%D0%9A%D1%80%D0%B0%D0%BD_%D0%A0%D0%BE%D0%BD%D0%B8%D0%BD - # 7. "Мёрдиела Мол": https://saltandsanctuary.fandom.com/ru/wiki/%D0%9C%D1%91%D1%80%D0%B4%D0%B8%D0%B5%D0%BB%D0%B0_%D0%9C%D0%BE%D0%BB - # 8. "Бескровный принц": https://saltandsanctuary.fandom.com/ru/wiki/%D0%91%D0%B5%D1%81%D0%BA%D1%80%D0%BE%D0%B2%D0%BD%D1%8B%D0%B9_%D0%BF%D1%80%D0%B8%D0%BD%D1%86 - # 9. "Жаждущий": https://saltandsanctuary.fandom.com/ru/wiki/%D0%96%D0%B0%D0%B6%D0%B4%D1%83%D1%89%D0%B8%D0%B9_(%D0%B1%D0%BE%D1%81%D1%81) - # 10. "Карсджоу Жестокий": https://saltandsanctuary.fandom.com/ru/wiki/%D0%9A%D0%B0%D1%80%D1%81%D0%B4%D0%B6%D0%BE%D1%83_%D0%96%D0%B5%D1%81%D1%82%D0%BE%D0%BA%D0%B8%D0%B9 - # 11. "Забытый король": https://saltandsanctuary.fandom.com/ru/wiki/%D0%97%D0%B0%D0%B1%D1%8B%D1%82%D1%8B%D0%B9_%D0%BA%D0%BE%D1%80%D0%BE%D0%BB%D1%8C - - print_bosses(bosses_by_category, only_names=True) - # Salt and Sanctuary (23): - # Обязательные боссы (12): - # 1. "Обезумевший рыцарь" - # 2. "Краекан циклоп" - # 3. "Безумный алхимик" - # 4. "Фальшивый шут" - # 5. "Краекан вирм" - # 6. "Нетронутый инквизитор" - # 7. "Третий агнец" - # 8. "Иссушенный король" - # 9. "Ведьма озера" - # 10. "Бескожий и Архитектор" - # 11. "Краекан дракон Скоурж" - # 12. "Безымянный бог" - # - # Опциональные боссы (11): - # 1. "Немая бездна" - # 2. "Королева улыбок" - # 3. "Древо людей" - # 4. "Выпотрошенная оболочка" - # 5. "Отвратительный смрад" - # 6. "Кран Ронин" - # 7. "Мёрдиела Мол" - # 8. "Бескровный принц" - # 9. "Жаждущий" - # 10. "Карсджоу Жестокий" - # 11. "Забытый король" diff --git a/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/utils.py b/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/utils.py deleted file mode 100644 index 8bc862e0c..000000000 --- a/Salt and Sanctuary parse bosses saltandsanctuary.fandom.com/utils.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from collections import OrderedDict -from typing import Dict, List, NamedTuple -from urllib.parse import urljoin - -from bs4 import BeautifulSoup -import requests - - -class Boss(NamedTuple): - name: str - url: str - - -def get_bosses() -> Dict[str, List[Boss]]: - URL = 'https://saltandsanctuary.fandom.com/ru/wiki/Боссы' - - rs = requests.get(URL) - root = BeautifulSoup(rs.content, 'html.parser') - - bosses_by_category = OrderedDict() - - for h2 in root.select('h2'): - category = h2.select_one('.mw-headline') - if not category: - continue - - table = h2.find_next_sibling('table') - if not table: - continue - - category_name = category.get_text(strip=True) - bosses = [] - - for item in table.select('td > b > a'): - boss_url = urljoin(rs.url, item['href']) - boss_name = item.get_text(strip=True) - - boss = Boss(boss_name, boss_url) - bosses.append(boss) - - bosses_by_category[category_name] = bosses - - return bosses_by_category - - -def convert_bosses_to_only_name(bosses: Dict[str, List[Boss]]) -> Dict[str, List[str]]: - bosses_only_name = OrderedDict() - for category, bosses_list in bosses.items(): - bosses_only_name[category] = [boss.name for boss in bosses_list] - - return bosses_only_name - - -if __name__ == '__main__': - bosses_by_category = get_bosses() - for category, bosses in bosses_by_category.items(): - print(category) - for boss in bosses: - print(f' "{boss.name}": {boss.url}') - print() - print() - # ... - - bosses_by_category = convert_bosses_to_only_name(bosses_by_category) - for category, boss_names in bosses_by_category.items(): - print(category) - for name in boss_names: - print(' ' + name) - print() - # Обязательные боссы - # Обезумевший рыцарь - # Краекан циклоп - # Безумный алхимик - # Фальшивый шут - # Краекан вирм - # Нетронутый инквизитор - # Третий агнец - # Иссушенный король - # Ведьма озера - # Бескожий и Архитектор - # Краекан дракон Скоурж - # Безымянный бог - # - # Опциональные боссы - # Немая бездна - # Королева улыбок - # Древо людей - # Выпотрошенная оболочка - # Отвратительный смрад - # Кран Ронин - # Мёрдиела Мол - # Бескровный принц - # Жаждущий - # Карсджоу Жестокий - # Забытый король diff --git a/Serialize with pickle/SerializeWithPickle.py b/Serialize with pickle/SerializeWithPickle.py index 7b45c23c4..2ad97101d 100644 --- a/Serialize with pickle/SerializeWithPickle.py +++ b/Serialize with pickle/SerializeWithPickle.py @@ -1,106 +1,106 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Serialization / Сериализация + # Example of using pickle module # https://docs.python.org/3.4/library/pickle.html import pickle -if __name__ == '__main__': - data = { - 'foo': [1, 2, 3], - 'bar': ('Hello', 'world!'), - 'baz': True, - "fiz": { - 1: "first", - 6: "six", - "seven": 7, - }, - } - - ## Simple example - # write the serialized data in the file - with open('data.pkl', 'wb') as f: - pickle.dump(data, f) - - # open and read from a file - with open('data.pkl', 'rb') as f: - data = pickle.load(f) # stored in the variable - print(data) - - - ## Maybe add a few data - # write the serialized data in the file - with open('data_1.pkl', 'wb') as f: - pickle.dump([1, 1, 2, 3], f) - pickle.dump("Man", f) - pickle.dump((666, ), f) - - with open('data_1.pkl', 'rb') as f: - # Need to preserve the order of reading - fi = pickle.load(f) - tw = pickle.load(f) - th = pickle.load(f) - print(fi) # [1, 1, 2, 3] - print(tw) # Man - print(th) # (666,) - - - ## Serialization of custom classes - class Monster: - """Monster class!""" - def __init__(self, health, power, name, level): - self.health = health - self.power = power - self.name = name - self.level = level - self.abilities = ["Eater"] - - def __str__(self): - return ("Name: '{}' lv {}, health: {}, power: {}, abilities: {}: {}" - .format(self.name, self.level, self.health, self.power, self.abilities, hex(id(self)))) - - def say(self): - print("I'm %s" % self.name) - - - zombi = Monster(name="Zombi", health=100, power=10, level=2) - zombi.abilities.append("Undead") - zombi.abilities.append("Insensitivity to pain") - - goblin = Monster(name="Goblin", health=50, power=8, level=1) - ork = Monster(name="Ork", health=250, power=25, level=4) +data = { + "foo": [1, 2, 3], + "bar": ("Hello", "world!"), + "baz": True, + "fiz": { + 1: "first", + 6: "six", + "seven": 7, + }, +} + +## Simple example +# write the serialized data in the file +with open("data.pkl", "wb") as f: + pickle.dump(data, f) + +# open and read from a file +with open("data.pkl", "rb") as f: + data = pickle.load(f) # stored in the variable + print(data) + +## Maybe add a few data +# write the serialized data in the file +with open("data_1.pkl", "wb") as f: + pickle.dump([1, 1, 2, 3], f) + pickle.dump("Man", f) + pickle.dump((666,), f) + +with open("data_1.pkl", "rb") as f: + # Need to preserve the order of reading + fi = pickle.load(f) + tw = pickle.load(f) + th = pickle.load(f) + print(fi) # [1, 1, 2, 3] + print(tw) # Man + print(th) # (666,) + +## Serialization of custom classes +class Monster: + """Monster class!""" + + def __init__(self, health, power, name, level) -> None: + self.health = health + self.power = power + self.name = name + self.level = level + self.abilities = ["Eater"] + + def __str__(self) -> str: + return ( + f"Name: '{self.name}' lv {self.level}, health: {self.health}, " + f"power: {self.power}, abilities: {self.abilities}: {hex(id(self))}" + ) + + def say(self) -> None: + print("I'm %s" % self.name) + +zombi = Monster(name="Zombi", health=100, power=10, level=2) +zombi.abilities.append("Undead") +zombi.abilities.append("Insensitivity to pain") + +goblin = Monster(name="Goblin", health=50, power=8, level=1) +ork = Monster(name="Ork", health=250, power=25, level=4) + +print() +print(Monster) +print(zombi) +print(goblin) +print(ork) + +# write the serialized data in the file +with open("monster.pkl", "wb") as f: + pickle.dump(Monster, f) + pickle.dump(zombi, f) + pickle.dump(goblin, f) + pickle.dump(ork, f) + +with open("monster.pkl", "rb") as f: + m = pickle.load(f) # get Monster + z = pickle.load(f) # get Zombi + g = pickle.load(f) # get Goblin + o = pickle.load(f) # get Ork print() - print(Monster) - print(zombi) - print(goblin) - print(ork) - - # write the serialized data in the file - with open('monster.pkl', 'wb') as f: - pickle.dump(Monster, f) - pickle.dump(zombi, f) - pickle.dump(goblin, f) - pickle.dump(ork, f) - - with open('monster.pkl', 'rb') as f: - m = pickle.load(f) # get Monster - z = pickle.load(f) # get Zombi - g = pickle.load(f) # get Goblin - o = pickle.load(f) # get Ork - print() - print(m) - print(z) - print(g) - print(o) - - - # Serialize: object -> string - temp_1 = pickle.dumps(zombi) - print() - print(temp_1) - - # Serialize: string ->object - temp_2 = pickle.loads(temp_1) # create new object - print(temp_2) - temp_2.say() \ No newline at end of file + print(m) + print(z) + print(g) + print(o) + +# Serialize: object -> string +temp_1 = pickle.dumps(zombi) +print() +print(temp_1) + +# Serialize: string ->object +temp_2 = pickle.loads(temp_1) # create new object +print(temp_2) +temp_2.say() diff --git a/Sorting algorithm list/comparison_of_sorting.py b/Sorting algorithm list/comparison_of_sorting.py index ec8c021f0..afb189f32 100644 --- a/Sorting algorithm list/comparison_of_sorting.py +++ b/Sorting algorithm list/comparison_of_sorting.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/master/timeout_block/main.py @@ -15,17 +15,17 @@ class Kill(Exception): class KThread(Thread): - def __init__(self, *args, **keywords): + def __init__(self, *args, **keywords) -> None: Thread.__init__(self, *args, **keywords) self.killed = False - def start(self): + def start(self) -> None: """Start the thread.""" self.__run_backup = self.run self.run = self.__run # Force the Thread to install our trace. Thread.start(self) - def __run(self): + def __run(self) -> None: """Hacked run function, which installs the trace.""" sys.settrace(self.globaltrace) @@ -37,18 +37,18 @@ def __run(self): self.run = self.__run_backup def globaltrace(self, frame, why, arg): - if why == 'call': + if why == "call": return self.localtrace else: return None def localtrace(self, frame, why, arg): if self.killed: - if why == 'line': + if why == "line": raise Kill() return self.localtrace - def kill(self): + def kill(self) -> None: self.killed = True @@ -69,42 +69,44 @@ def the_wrapper_around_the_original_function(*args, **kwargs): return wrapper + ############################################################################################################ -if __name__ == '__main__': - items = list(range(10 ** 3)) +if __name__ == "__main__": import random - random.shuffle(items) + import sorts - import time + from timeit import default_timer + + items = list(range(10**3)) + random.shuffle(items) time_by_algo_name = dict() - import sorts for name, algo in sorted(sorts.ALGO_LIST.items(), key=lambda x: x[0]): print(name) @timeout(seconds=10, raise_timeout=True) - def run(): + def run() -> None: new_items = list(items) algo(new_items) - t = time.clock() + t = default_timer() try: run() except TimeoutError: - print(' timeout!') + print(" timeout!") except Exception as e: - print(' Error: {}: sort: {}'.format(e, name)) + print(f" Error: {e}: sort: {name}") - t = time.clock() - t + t = default_timer() - t time_by_algo_name[t] = name # print(' duration: {:.3f} secs'.format(t)) print() - print('Sorted by time:') + print("Sorted by time:") for t, name in sorted(time_by_algo_name.items(), key=lambda x: x[0]): - print('{}: {:.3f} secs'.format(name, t)) + print(f"{name}: {t:.3f} secs") diff --git a/Sorting algorithm list/sorts/__init__.py b/Sorting algorithm list/sorts/__init__.py index 2e44ba1f7..a60c6f788 100644 --- a/Sorting algorithm list/sorts/__init__.py +++ b/Sorting algorithm list/sorts/__init__.py @@ -1,16 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from sorts import ( bogosort, - # TODO: ImportError: No module named 'P26_InsertionSort' # bubble_sort, # bucket_sort, - cocktail_shaker_sort, gnome_sort, heap_sort, @@ -18,10 +16,8 @@ merge_sort, quick_sort, radix_sort, - # TODO: remove this # random_normaldistribution_quicksort, - selection_sort, shell_sort, topological_sort, @@ -32,16 +28,15 @@ # 'bogosort': bogosort.bogosort, # 'bubble_sort': bubble_sort.bubble_sort, # 'bucket_sort': bucket_sort.bucketSort, - - 'cocktail_shaker_sort': cocktail_shaker_sort.cocktail_shaker_sort, - 'gnome_sort': gnome_sort.gnome_sort, - 'heap_sort': heap_sort.heap_sort, - 'insertion_sort': insertion_sort.insertion_sort, - 'merge_sort': merge_sort.merge_sort, - 'quick_sort': quick_sort.quick_sort, - 'radix_sort': radix_sort.radixsort, + "cocktail_shaker_sort": cocktail_shaker_sort.cocktail_shaker_sort, + "gnome_sort": gnome_sort.gnome_sort, + "heap_sort": heap_sort.heap_sort, + "insertion_sort": insertion_sort.insertion_sort, + "merge_sort": merge_sort.merge_sort, + "quick_sort": quick_sort.quick_sort, + "radix_sort": radix_sort.radixsort, # 'random_normaldistribution_quicksort': random_normaldistribution_quicksort.random_normaldistribution_quicksort, - 'selection_sort': selection_sort.selection_sort, - 'shell_sort': shell_sort.shell_sort, + "selection_sort": selection_sort.selection_sort, + "shell_sort": shell_sort.shell_sort, # 'topological_sort': topological_sort.topological_sort, } diff --git a/Sorting algorithm list/sorts/bogosort.py b/Sorting algorithm list/sorts/bogosort.py index 2512dab51..84b3c9d9a 100644 --- a/Sorting algorithm list/sorts/bogosort.py +++ b/Sorting algorithm list/sorts/bogosort.py @@ -26,7 +26,7 @@ def bogosort(collection): [-45, -5, -2] """ - def isSorted(collection): + def isSorted(collection) -> bool: if len(collection) < 2: return True for i in range(len(collection) - 1): @@ -38,7 +38,8 @@ def isSorted(collection): random.shuffle(collection) return collection -if __name__ == '__main__': + +if __name__ == "__main__": import sys # For python 2.x and 3.x compatibility: 3.x has not raw_input builtin @@ -48,6 +49,6 @@ def isSorted(collection): else: input_function = input - user_input = input_function('Enter numbers separated by a comma:\n') - unsorted = [int(item) for item in user_input.split(',')] + user_input = input_function("Enter numbers separated by a comma:\n") + unsorted = [int(item) for item in user_input.split(",")] print(bogosort(unsorted)) diff --git a/Sorting algorithm list/sorts/bubble_sort.py b/Sorting algorithm list/sorts/bubble_sort.py index 54d69e5ba..0e69f38d7 100644 --- a/Sorting algorithm list/sorts/bubble_sort.py +++ b/Sorting algorithm list/sorts/bubble_sort.py @@ -31,16 +31,17 @@ def bubble_sort(collection): [-45, -5, -2] """ length = len(collection) - for i in range(length-1, -1, -1):#range(length-1, -1, -1) - for j in range(i):#range(1, i) - if collection[j] > collection[j+1]: - collection[j], collection[j+1] = collection[j+1], collection[j] + for i in range(length - 1, -1, -1): # range(length-1, -1, -1) + for j in range(i): # range(1, i) + if collection[j] > collection[j + 1]: + collection[j], collection[j + 1] = collection[j + 1], collection[j] return collection -if __name__ == '__main__': +if __name__ == "__main__": import sys + # For python 2.x and 3.x compatibility: 3.x has not raw_input builtin # otherwise 2.x's input builtin function is too "smart" if sys.version_info.major < 3: @@ -48,6 +49,6 @@ def bubble_sort(collection): else: input_function = input - user_input = input_function('Enter numbers separated by a comma:\n') - unsorted = [int(item) for item in user_input.split(',')] + user_input = input_function("Enter numbers separated by a comma:\n") + unsorted = [int(item) for item in user_input.split(",")] print(bubble_sort(unsorted)) diff --git a/Sorting algorithm list/sorts/bucket_sort.py b/Sorting algorithm list/sorts/bucket_sort.py index e378d65f4..aa500b6c8 100644 --- a/Sorting algorithm list/sorts/bucket_sort.py +++ b/Sorting algorithm list/sorts/bucket_sort.py @@ -3,7 +3,7 @@ # This program will illustrate how to implement bucket sort algorithm # Wikipedia says: Bucket sort, or bin sort, is a sorting algorithm that works by distributing the -# elements of an array into a number of buckets. Each bucket is then sorted individually, either using +# elements of an array into a number of buckets. Each bucket is then sorted individually, either using # a different sorting algorithm, or by recursively applying the bucket sorting algorithm. It is a # distribution sort, and is a cousin of radix sort in the most to least significant digit flavour. # Bucket sort is a generalization of pigeonhole sort. Bucket sort can be implemented with comparisons @@ -18,9 +18,10 @@ DEFAULT_BUCKET_SIZE = 5 + def bucketSort(myList, bucketSize=DEFAULT_BUCKET_SIZE): - if(len(myList) == 0): - print('You don\'t have any elements in array!') + if len(myList) == 0: + print("You don't have any elements in array!") minValue = myList[0] maxValue = myList[0] @@ -51,6 +52,7 @@ def bucketSort(myList, bucketSize=DEFAULT_BUCKET_SIZE): return sortedArray -if __name__ == '__main__': + +if __name__ == "__main__": sortedArray = bucketSort([12, 23, 4, 5, 3, 2, 12, 81, 56, 95]) print(sortedArray) diff --git a/Sorting algorithm list/sorts/cocktail_shaker_sort.py b/Sorting algorithm list/sorts/cocktail_shaker_sort.py index a21224632..b54235166 100644 --- a/Sorting algorithm list/sorts/cocktail_shaker_sort.py +++ b/Sorting algorithm list/sorts/cocktail_shaker_sort.py @@ -1,26 +1,28 @@ from __future__ import print_function + def cocktail_shaker_sort(unsorted): """ Pure implementation of the cocktail shaker sort algorithm in Python. """ - for i in range(len(unsorted)-1, 0, -1): + for i in range(len(unsorted) - 1, 0, -1): swapped = False - + for j in range(i, 0, -1): - if unsorted[j] < unsorted[j-1]: - unsorted[j], unsorted[j-1] = unsorted[j-1], unsorted[j] + if unsorted[j] < unsorted[j - 1]: + unsorted[j], unsorted[j - 1] = unsorted[j - 1], unsorted[j] swapped = True for j in range(i): - if unsorted[j] > unsorted[j+1]: - unsorted[j], unsorted[j+1] = unsorted[j+1], unsorted[j] + if unsorted[j] > unsorted[j + 1]: + unsorted[j], unsorted[j + 1] = unsorted[j + 1], unsorted[j] swapped = True - + if not swapped: return unsorted - -if __name__ == '__main__': + + +if __name__ == "__main__": import sys # For python 2.x and 3.x compatibility: 3.x has not raw_input builtin @@ -29,8 +31,8 @@ def cocktail_shaker_sort(unsorted): input_function = raw_input else: input_function = input - - user_input = input_function('Enter numbers separated by a comma:\n') - unsorted = [int(item) for item in user_input.split(',')] + + user_input = input_function("Enter numbers separated by a comma:\n") + unsorted = [int(item) for item in user_input.split(",")] cocktail_shaker_sort(unsorted) - print(unsorted) \ No newline at end of file + print(unsorted) diff --git a/Sorting algorithm list/sorts/gnome_sort.py b/Sorting algorithm list/sorts/gnome_sort.py index 6126f576d..6bbc41a31 100644 --- a/Sorting algorithm list/sorts/gnome_sort.py +++ b/Sorting algorithm list/sorts/gnome_sort.py @@ -1,27 +1,28 @@ from __future__ import print_function + def gnome_sort(unsorted): """ Pure implementation of the gnome sort algorithm in Python. """ if len(unsorted) <= 1: return unsorted - + i = 1 - + while i < len(unsorted): - if unsorted[i-1] <= unsorted[i]: + if unsorted[i - 1] <= unsorted[i]: i += 1 else: - unsorted[i-1], unsorted[i] = unsorted[i], unsorted[i-1] + unsorted[i - 1], unsorted[i] = unsorted[i], unsorted[i - 1] i -= 1 - if (i == 0): + if i == 0: i = 1 return unsorted - -if __name__ == '__main__': + +if __name__ == "__main__": import sys # For python 2.x and 3.x compatibility: 3.x has not raw_input builtin @@ -30,8 +31,8 @@ def gnome_sort(unsorted): input_function = raw_input else: input_function = input - - user_input = input_function('Enter numbers separated by a comma:\n') - unsorted = [int(item) for item in user_input.split(',')] + + user_input = input_function("Enter numbers separated by a comma:\n") + unsorted = [int(item) for item in user_input.split(",")] gnome_sort(unsorted) - print(unsorted) \ No newline at end of file + print(unsorted) diff --git a/Sorting algorithm list/sorts/heap_sort.py b/Sorting algorithm list/sorts/heap_sort.py index 2d9dd844d..869ed18fb 100644 --- a/Sorting algorithm list/sorts/heap_sort.py +++ b/Sorting algorithm list/sorts/heap_sort.py @@ -1,4 +1,4 @@ -''' +""" This is a pure python implementation of the heap sort algorithm. For doctests run following command: @@ -8,12 +8,12 @@ For manual testing run: python heap_sort.py -''' +""" from __future__ import print_function -def heapify(unsorted, index, heap_size): +def heapify(unsorted, index, heap_size) -> None: largest = index left_index = 2 * index + 1 right_index = 2 * index + 2 @@ -29,7 +29,7 @@ def heapify(unsorted, index, heap_size): def heap_sort(unsorted): - ''' + """ Pure implementation of the heap sort algorithm in Python :param collection: some mutable ordered collection with heterogeneous comparable items inside @@ -44,7 +44,7 @@ def heap_sort(unsorted): >>> heap_sort([-2, -5, -45]) [-45, -5, -2] - ''' + """ n = len(unsorted) for i in range(n // 2 - 1, -1, -1): heapify(unsorted, i, n) @@ -53,13 +53,15 @@ def heap_sort(unsorted): heapify(unsorted, 0, i) return unsorted -if __name__ == '__main__': + +if __name__ == "__main__": import sys + if sys.version_info.major < 3: input_function = raw_input else: input_function = input - user_input = input_function('Enter numbers separated by a comma:\n') - unsorted = [int(item) for item in user_input.split(',')] + user_input = input_function("Enter numbers separated by a comma:\n") + unsorted = [int(item) for item in user_input.split(",")] print(heap_sort(unsorted)) diff --git a/Sorting algorithm list/sorts/insertion_sort.py b/Sorting algorithm list/sorts/insertion_sort.py index caaa9305c..14bffb736 100644 --- a/Sorting algorithm list/sorts/insertion_sort.py +++ b/Sorting algorithm list/sorts/insertion_sort.py @@ -31,14 +31,16 @@ def insertion_sort(collection): """ for index in range(1, len(collection)): while 0 < index and collection[index] < collection[index - 1]: - collection[index], collection[ - index - 1] = collection[index - 1], collection[index] + collection[index], collection[index - 1] = ( + collection[index - 1], + collection[index], + ) index -= 1 return collection -if __name__ == '__main__': +if __name__ == "__main__": import sys # For python 2.x and 3.x compatibility: 3.x has not raw_input builtin @@ -48,6 +50,6 @@ def insertion_sort(collection): else: input_function = input - user_input = input_function('Enter numbers separated by a comma:\n') - unsorted = [int(item) for item in user_input.split(',')] + user_input = input_function("Enter numbers separated by a comma:\n") + unsorted = [int(item) for item in user_input.split(",")] print(insertion_sort(unsorted)) diff --git a/Sorting algorithm list/sorts/merge_sort.py b/Sorting algorithm list/sorts/merge_sort.py index 92a678016..7047ac3fa 100644 --- a/Sorting algorithm list/sorts/merge_sort.py +++ b/Sorting algorithm list/sorts/merge_sort.py @@ -61,7 +61,7 @@ def merge_sort(collection): return collection -if __name__ == '__main__': +if __name__ == "__main__": import sys # For python 2.x and 3.x compatibility: 3.x has not raw_input builtin @@ -71,6 +71,6 @@ def merge_sort(collection): else: input_function = input - user_input = input_function('Enter numbers separated by a comma:\n') - unsorted = [int(item) for item in user_input.split(',')] + user_input = input_function("Enter numbers separated by a comma:\n") + unsorted = [int(item) for item in user_input.split(",")] print(merge_sort(unsorted)) diff --git a/Sorting algorithm list/sorts/quick_sort.py b/Sorting algorithm list/sorts/quick_sort.py index 8974e1bd8..0bd836fec 100644 --- a/Sorting algorithm list/sorts/quick_sort.py +++ b/Sorting algorithm list/sorts/quick_sort.py @@ -30,16 +30,16 @@ def quick_sort(ARRAY): [-45, -5, -2] """ ARRAY_LENGTH = len(ARRAY) - if( ARRAY_LENGTH <= 1): + if ARRAY_LENGTH <= 1: return ARRAY else: PIVOT = ARRAY[0] - GREATER = [ element for element in ARRAY[1:] if element > PIVOT ] - LESSER = [ element for element in ARRAY[1:] if element <= PIVOT ] + GREATER = [element for element in ARRAY[1:] if element > PIVOT] + LESSER = [element for element in ARRAY[1:] if element <= PIVOT] return quick_sort(LESSER) + [PIVOT] + quick_sort(GREATER) -if __name__ == '__main__': +if __name__ == "__main__": import sys # For python 2.x and 3.x compatibility: 3.x has not raw_input builtin @@ -49,6 +49,6 @@ def quick_sort(ARRAY): else: input_function = input - user_input = input_function('Enter numbers separated by a comma:\n') - unsorted = [ int(item) for item in user_input.split(',') ] - print( quick_sort(unsorted) ) + user_input = input_function("Enter numbers separated by a comma:\n") + unsorted = [int(item) for item in user_input.split(",")] + print(quick_sort(unsorted)) diff --git a/Sorting algorithm list/sorts/random_normaldistribution_quicksort.py b/Sorting algorithm list/sorts/random_normaldistribution_quicksort.py index 19b180578..0c0276158 100644 --- a/Sorting algorithm list/sorts/random_normaldistribution_quicksort.py +++ b/Sorting algorithm list/sorts/random_normaldistribution_quicksort.py @@ -1,66 +1,60 @@ from random import randint from tempfile import TemporaryFile import numpy as np -import math - -def _inPlaceQuickSort(A,start,end): +def _inPlaceQuickSort(A, start, end): count = 0 - if start None: self.name = name self.age = age - def __repr__(self): + def __repr__(self) -> str: return "%s (%d)" % (self.name, self.age) @@ -48,7 +54,7 @@ def __repr__(self): print() words = ["he", "He", "Ab", "ab", "Cc", "cC"] -print("Words: {0}".format(words)) +print(f"Words: {words}") print("Sorting:") print(sorted(words)) print(sorted(words, reverse=True)) diff --git a/Split a string into two elements.py b/Split a string into two elements.py index 1d5f9bcb5..7e846a686 100644 --- a/Split a string into two elements.py +++ b/Split a string into two elements.py @@ -1,11 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import re def split_by_pair(text): - items = list() + items = [] for i in range(0, len(text), 2): pair = text[i] + text[i + 1] @@ -20,12 +23,11 @@ def split_by_pair_1(text): def split_by_pair_2(text): - import re - return re.findall('..', text) + return re.findall("..", text) -if __name__ == '__main__': - text = 'a1b2c3d4f5' +if __name__ == "__main__": + text = "a1b2c3d4f5" items = split_by_pair(text) print(items) # ['a1', 'b2', 'c3', 'd4', 'f5'] diff --git a/Start a file with its associated application/main.py b/Start a file with its associated application/main.py index cf211dff6..e8dabfef5 100644 --- a/Start a file with its associated application/main.py +++ b/Start a file with its associated application/main.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os -os.startfile('result.txt') -os.startfile('img.png') -os.startfile('cert.cer') + + +os.startfile("result.txt") +os.startfile("img.png") +os.startfile("cert.cer") diff --git a/TX_responsible_for_modules/main.py b/TX_responsible_for_modules/main.py deleted file mode 100644 index 30827b071..000000000 --- a/TX_responsible_for_modules/main.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from collections import defaultdict -from pathlib import Path - -from bs4 import BeautifulSoup - - -path_dir = Path(r'C:\DEV__TX\trunk_tx') -path_branch = path_dir / r'branch.xml' - -# Сбор названий модулей из всех слоев -module_id_by_name = dict() - -for file_module in path_dir.glob('*/ads/*/module.xml'): - root = BeautifulSoup(file_module.read_bytes(), 'html.parser') - module_root = root.select_one('Module') - module_id = module_root['id'] - module_name = module_root['name'] - - module_id_by_name[module_id] = module_name - -root = BeautifulSoup(path_branch.read_bytes(), 'html.parser') -owner_by_modules = defaultdict(set) - -for module_el in root.select('ModulesInfo > ModuleInfo'): - layer_url = module_el.select_one('LayerUrl').get_text(strip=True) - module_id = module_el.select_one('ModuleId').get_text(strip=True) - - module_name = module_id_by_name[module_id] - full_module_name = f'{layer_url}/{module_name}' - - for owner_el in module_el.select('OwnerEmail'): - owner = owner_el.get_text(strip=True) - owner_by_modules[owner].add(full_module_name) - -for owner, modules in sorted(owner_by_modules.items()): - print(f'{owner} ({len(modules)}):') - for x in sorted(modules): - print(f' {x}') - - print() diff --git a/Timer/pyside_qtimer.py b/Timer/pyside_qtimer.py index d23ecf4b0..4d6640a2f 100644 --- a/Timer/pyside_qtimer.py +++ b/Timer/pyside_qtimer.py @@ -1,14 +1,14 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" """Пример таймера.""" -from PySide.QtCore import * import sys +from PySide.QtCore import * -def say(): +def say() -> None: print("say!") @@ -19,4 +19,4 @@ def say(): t.timeout.connect(say) t.start() -sys.exit(app.exec_()) \ No newline at end of file +sys.exit(app.exec_()) diff --git a/WrapperMap__work_with_dict_through_atts.py b/WrapperMap__work_with_dict_through_atts.py index dfa1f2914..316813bf1 100644 --- a/WrapperMap__work_with_dict_through_atts.py +++ b/WrapperMap__work_with_dict_through_atts.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" class WrapperMap: - def __init__(self, d: dict): + def __init__(self, d: dict) -> None: self.d = d def get_value(self): @@ -18,16 +18,16 @@ def __getattr__(self, item: str): return value - def __repr__(self): + def __repr__(self) -> str: return repr(self.d) genMessage = { - 'from_user': { - 'id': 123, - 'username': "username", - 'full_name': "fullName" - } + "from_user": { + "id": 123, + "username": "username", + "full_name": "fullName", + }, } x = WrapperMap(genMessage) diff --git a/XML/XML_to_dict__xmltodict__examples/dict_to_xml__unparse.py b/XML/XML_to_dict__xmltodict__examples/dict_to_xml__unparse.py index 7073f16f0..9f52d5b65 100644 --- a/XML/XML_to_dict__xmltodict__examples/dict_to_xml__unparse.py +++ b/XML/XML_to_dict__xmltodict__examples/dict_to_xml__unparse.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/martinblech/xmltodict#roundtripping @@ -12,9 +12,9 @@ my_dict = { - 'response': { - 'status': 'good', - 'last_updated': '2014-02-16T23:10:12Z', + "response": { + "status": "good", + "last_updated": "2014-02-16T23:10:12Z", } } print(xmltodict.unparse(my_dict)) @@ -30,16 +30,16 @@ # 2014-02-16T23:10:12Z # -print('\n') +print("\n") # Text values for nodes can be specified with the cdata_key key in the python dict, while node properties can # be specified with the attr_prefix prefixed to the key name in the python dict. The default value for attr_ # prefix is @ and the default value for cdata_key is #text. my_dict = { - 'text': { - '@color': 'red', - '@stroke': '2', - '#text': 'This is a test', + "text": { + "@color": "red", + "@stroke": "2", + "#text": "This is a test", } } print(xmltodict.unparse(my_dict, pretty=True)) diff --git a/XML/XML_to_dict__xmltodict__examples/dict_to_xml__unparse__real_example.py b/XML/XML_to_dict__xmltodict__examples/dict_to_xml__unparse__real_example.py index 88aabe2d7..59a327073 100644 --- a/XML/XML_to_dict__xmltodict__examples/dict_to_xml__unparse__real_example.py +++ b/XML/XML_to_dict__xmltodict__examples/dict_to_xml__unparse__real_example.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/q/861914/201445 @@ -18,11 +18,9 @@ root = dict() for i, item in enumerate(items, 1): - root['person' + str(i)] = item + root["person" + str(i)] = item -my_dict = { - 'root': root -} +my_dict = {"root": root} # my_dict = { # 'root': { # 'person1': {"first_name": "Ivan", "last_name": "Ivanov", "city": "Moscow"}, diff --git a/XML/XML_to_dict__xmltodict__examples/hello_world.py b/XML/XML_to_dict__xmltodict__examples/hello_world.py index 2c6bbf829..e135e3d2f 100644 --- a/XML/XML_to_dict__xmltodict__examples/hello_world.py +++ b/XML/XML_to_dict__xmltodict__examples/hello_world.py @@ -1,17 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/martinblech/xmltodict +import json + # pip install xmltodict import xmltodict -doc = xmltodict.parse(""" +doc = xmltodict.parse( + """ elements @@ -19,16 +22,18 @@ element as well -""") - -print(doc['mydocument']['@has']) # an attribute -print(doc['mydocument']['and']["many"]) # ['elements', 'more elements'] -print(doc['mydocument']['plus']) # OrderedDict([('@a', 'complex'), ('#text', 'element as well')]) -print(doc['mydocument']['plus']['@a']) # complex -print(doc['mydocument']['plus']['#text']) # element as well +""" +) + +print(doc["mydocument"]["@has"]) # an attribute +print(doc["mydocument"]["and"]["many"]) # ['elements', 'more elements'] +print( + doc["mydocument"]["plus"] +) # OrderedDict([('@a', 'complex'), ('#text', 'element as well')]) +print(doc["mydocument"]["plus"]["@a"]) # complex +print(doc["mydocument"]["plus"]["#text"]) # element as well print() -import json print(json.dumps(doc, indent=4)) # { # "mydocument": { diff --git a/XML/XML_to_dict__xmltodict__examples/namespace_support.py b/XML/XML_to_dict__xmltodict__examples/namespace_support.py index af6cd206d..6aab421ff 100644 --- a/XML/XML_to_dict__xmltodict__examples/namespace_support.py +++ b/XML/XML_to_dict__xmltodict__examples/namespace_support.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/martinblech/xmltodict#namespace-support +import json + # pip install xmltodict import xmltodict @@ -24,7 +26,6 @@ """ doc = xmltodict.parse(xml, process_namespaces=True) -import json print(json.dumps(doc, indent=4)) # { # "http://defaultns.com/:root": { @@ -34,13 +35,13 @@ # } # } -print('\n') +print("\n") # It also lets you collapse certain namespaces to shorthand prefixes, or skip them altogether: namespaces = { - 'http://defaultns.com/': None, # skip this namespace - 'http://a.com/': 'ns_a', # collapse "http://a.com/" -> "ns_a" + "http://defaultns.com/": None, # skip this namespace + "http://a.com/": "ns_a", # collapse "http://a.com/" -> "ns_a" } doc = xmltodict.parse(xml, process_namespaces=True, namespaces=namespaces) print(json.dumps(doc, indent=4)) diff --git a/XML/XML_to_dict__xmltodict__examples/postprocessor.py b/XML/XML_to_dict__xmltodict__examples/postprocessor.py index f88e6cd85..cbe2c4844 100644 --- a/XML/XML_to_dict__xmltodict__examples/postprocessor.py +++ b/XML/XML_to_dict__xmltodict__examples/postprocessor.py @@ -1,29 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/martinblech/xmltodict +import json + # pip install xmltodict import xmltodict -# The optional argument `postprocessor` is a function that takes `path`, `key` and `value` as positional arguments +# The optional argument `postprocessor` is a function that takes `path`, `key` and `value` as positional arguments # and returns a new `(key, value)` pair where both `key` and `value` may have changed. Usage example: + def postprocessor(path, key, value): try: - return key + ':int', int(value) + return key + ":int", int(value) except (ValueError, TypeError): return key, value -doc = xmltodict.parse('12x', postprocessor=postprocessor) +doc = xmltodict.parse("12x", postprocessor=postprocessor) -import json print(json.dumps(doc, indent=4)) # { # "a": { diff --git a/XML/XML_to_dict__xmltodict__examples/simple_python_object_to_xml__unparse.py b/XML/XML_to_dict__xmltodict__examples/simple_python_object_to_xml__unparse.py index 5700c9901..1297bfeb7 100644 --- a/XML/XML_to_dict__xmltodict__examples/simple_python_object_to_xml__unparse.py +++ b/XML/XML_to_dict__xmltodict__examples/simple_python_object_to_xml__unparse.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/martinblech/xmltodict#roundtripping @@ -12,21 +12,21 @@ class Dog: - def __init__(self, name): + def __init__(self, name) -> None: self.name = name - self.type = 'Animal' + self.type = "Animal" self.paws = 4 self.has_tail = True - def __repr__(self): + def __repr__(self) -> str: return f'' -dog = Dog('Ray') +dog = Dog("Ray") print(dog) # print() -print(xmltodict.unparse({'Dog': dog.__dict__}, pretty=True)) +print(xmltodict.unparse({"Dog": dog.__dict__}, pretty=True)) # # # Ray diff --git a/XML/XML_to_dict__xmltodict__examples/streaming_mode.py b/XML/XML_to_dict__xmltodict__examples/streaming_mode.py index cd78e6e77..ead6df188 100644 --- a/XML/XML_to_dict__xmltodict__examples/streaming_mode.py +++ b/XML/XML_to_dict__xmltodict__examples/streaming_mode.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/martinblech/xmltodict#streaming-mode @@ -11,8 +11,8 @@ import xmltodict -def handle(path, item): - print('path: {} item: {}'.format(path, repr(item))) +def handle(path, item) -> bool: + print(f"path: {path} item: {item!r}") return True diff --git a/XML/XML_to_dict__xmltodict__examples/streaming_mode__from_url_gzip.py b/XML/XML_to_dict__xmltodict__examples/streaming_mode__from_url_gzip.py index 69218047b..49daf60e6 100644 --- a/XML/XML_to_dict__xmltodict__examples/streaming_mode__from_url_gzip.py +++ b/XML/XML_to_dict__xmltodict__examples/streaming_mode__from_url_gzip.py @@ -1,32 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/martinblech/xmltodict#streaming-mode -# pip install xmltodict -import xmltodict - import gzip from urllib.request import urlopen +# pip install xmltodict +import xmltodict + # 241.8 MB -url = 'http://discogs-data.s3-us-west-2.amazonaws.com/data/2018/discogs_20180201_artists.xml.gz' +url = "http://discogs-data.s3-us-west-2.amazonaws.com/data/2018/discogs_20180201_artists.xml.gz" -def handle_artist(_, artist): - print(artist['name']) +def handle_artist(_, artist) -> bool: + print(artist["name"]) return True xmltodict.parse( gzip.open(urlopen(url)), item_depth=2, - item_callback=handle_artist + item_callback=handle_artist, ) # The Persuader # Mr. James Barth & A.D. diff --git a/XML/XML_to_dict__xmltodict__examples/use_other__expat.py b/XML/XML_to_dict__xmltodict__examples/use_other__expat.py index 6ed590ad7..76700eca7 100644 --- a/XML/XML_to_dict__xmltodict__examples/use_other__expat.py +++ b/XML/XML_to_dict__xmltodict__examples/use_other__expat.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/martinblech/xmltodict @@ -10,8 +10,9 @@ # pip install xmltodict import xmltodict - # You can pass an alternate version of `expat` (such as `defusedexpat`) by using the `expat` parameter. E.g: import defusedexpat -doc = xmltodict.parse('hello', expat=defusedexpat.pyexpat) + + +doc = xmltodict.parse("hello", expat=defusedexpat.pyexpat) print(doc) # OrderedDict([('a', 'hello')]) diff --git a/XML/XML_to_python_objects__lxml.objectify__examples/from_text.py b/XML/XML_to_python_objects__lxml.objectify__examples/from_text.py index 6b1cf58b0..2c6cf5588 100644 --- a/XML/XML_to_python_objects__lxml.objectify__examples/from_text.py +++ b/XML/XML_to_python_objects__lxml.objectify__examples/from_text.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://lxml.de/objectify.html @@ -12,6 +12,7 @@ # pip install lxml from lxml import objectify + text = """\ @@ -30,13 +31,13 @@ def to_date(date_str): - return datetime.strptime(date_str, '%Y-%m-%d') + return datetime.strptime(date_str, "%Y-%m-%d") root = objectify.fromstring(text) items = root.Data.Report.LeaderList.Leader -leader = max(items, key=lambda x: to_date(x.attrib['ActualDate'])) -print(leader.attrib['FIO']) # Шxxxxxxx Аxxxxx Шxxxxxx -print(leader.attrib['ActualDate']) # 2009-12-01 -print(leader.attrib['Position']) # генеральный директор +leader = max(items, key=lambda x: to_date(x.attrib["ActualDate"])) +print(leader.attrib["FIO"]) # Шxxxxxxx Аxxxxx Шxxxxxx +print(leader.attrib["ActualDate"]) # 2009-12-01 +print(leader.attrib["Position"]) # генеральный директор diff --git a/XML/XML_to_python_objects__untagle__examples/from_file.py b/XML/XML_to_python_objects__untagle__examples/from_file.py index a174e71da..19f8fab93 100644 --- a/XML/XML_to_python_objects__untagle__examples/from_file.py +++ b/XML/XML_to_python_objects__untagle__examples/from_file.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/stchris/untangle @@ -12,8 +12,11 @@ # pip install git+https://github.com/stchris/untangle.git import untangle -obj = untangle.parse('data.xml') -print(obj.root.child) # [Element(name = child, attributes = {'name': 'child1'}, cdata = ), ... -print(obj.root.child[0]['name']) # child1 -print(obj.root.child[1].cdata) # Text + +obj = untangle.parse("data.xml") +print( + obj.root.child +) # [Element(name = child, attributes = {'name': 'child1'}, cdata = ), ... +print(obj.root.child[0]["name"]) # child1 +print(obj.root.child[1].cdata) # Text print(repr(obj.root.child[2].cdata)) # '\n This text!\n ' diff --git a/XML/XML_to_python_objects__untagle__examples/from_text.py b/XML/XML_to_python_objects__untagle__examples/from_text.py index 893e6cc91..bf2fe79b7 100644 --- a/XML/XML_to_python_objects__untagle__examples/from_text.py +++ b/XML/XML_to_python_objects__untagle__examples/from_text.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/stchris/untangle @@ -13,7 +13,8 @@ import untangle -obj = untangle.parse('''\ +obj = untangle.parse( + """\ Text @@ -21,20 +22,27 @@ This text! -''') -print(obj.root.child) # [Element(name = child, attributes = {'name': 'child1'}, cdata = ), ... -print(obj.root.child[0]['name']) # child1 -print(obj.root.child[1].cdata) # Text +""" +) +print( + obj.root.child +) # [Element(name = child, attributes = {'name': 'child1'}, cdata = ), ... +print(obj.root.child[0]["name"]) # child1 +print(obj.root.child[1].cdata) # Text print(repr(obj.root.child[2].cdata)) # '\n This text!\n ' # print(','.join([child['name'] for child in obj.root.child])) print() print() -obj = untangle.parse('''\ +obj = untangle.parse( + """\ Text -''') -print(obj.root.child) # Element with attributes {'name': 'child2'}, children [] and cdata Text -print(obj.root.child['name']) # child2 -print(obj.root.child.cdata) # Text +""" +) +print( + obj.root.child +) # Element with attributes {'name': 'child2'}, children [] and cdata Text +print(obj.root.child["name"]) # child2 +print(obj.root.child.cdata) # Text diff --git a/XML/XML_to_python_objects__untagle__examples/from_url.py b/XML/XML_to_python_objects__untagle__examples/from_url.py index e71381f10..accb05dc9 100644 --- a/XML/XML_to_python_objects__untagle__examples/from_url.py +++ b/XML/XML_to_python_objects__untagle__examples/from_url.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/stchris/untangle @@ -12,10 +12,11 @@ # pip install git+https://github.com/stchris/untangle.git import untangle -obj = untangle.parse('https://news.yandex.ru/games.rss') + +obj = untangle.parse("https://news.yandex.ru/games.rss") channel = obj.rss.channel -print(channel.title.cdata) # Яндекс.Новости: Игры -print(channel.link.cdata) # https://news.yandex.ru/games.html?from=rss +print(channel.title.cdata) # Яндекс.Новости: Игры +print(channel.link.cdata) # https://news.yandex.ru/games.html?from=rss print(channel.image.url.cdata) # https://company.yandex.ru/i/50x23.gif print() diff --git a/XML/lxml__xml.etree__examples/css_selector.py b/XML/lxml__xml.etree__examples/css_selector.py index 2ffd15910..eb696446f 100644 --- a/XML/lxml__xml.etree__examples/css_selector.py +++ b/XML/lxml__xml.etree__examples/css_selector.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from lxml import html -root = html.fromstring('

Hello
world!


') -word = root.cssselect('b')[0] -print(html.tostring(word, encoding='unicode', with_tail=False)) # world + +root = html.fromstring("

Hello
world!


") + +word = root.cssselect("b")[0] +print(html.tostring(word, encoding="unicode", with_tail=False)) # world diff --git a/XML/lxml__xml.etree__examples/get_inner_html.py b/XML/lxml__xml.etree__examples/get_inner_html.py index 390eb55f7..1e72e74a0 100644 --- a/XML/lxml__xml.etree__examples/get_inner_html.py +++ b/XML/lxml__xml.etree__examples/get_inner_html.py @@ -1,31 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from html import escape # Доступно с Python 3.2 import lxml.html +from html import escape # SOURCE: https://ru.stackoverflow.com/a/862559/201445 def inner_html(elem): # Текст в самом начале внутри тега # (не забываем про экранирование!) - result = [escape(elem.text or '')] + result = [escape(elem.text or "")] # Все элементы-потомки for child in elem.iterchildren(): - result.append(lxml.html.tostring(child, encoding='unicode')) + result.append(lxml.html.tostring(child, encoding="unicode")) # Текст в конце тега принадлежит последнему элементу-потомку (tail) # и добавится автоматически # Собираем результат в одну строку - return ''.join(result) + return "".join(result) -if __name__ == '__main__': +if __name__ == "__main__": text = """\
Самая популярная игра в Steam diff --git a/XML/lxml__xml.etree__examples/lxml_remove_blank_text.py b/XML/lxml__xml.etree__examples/lxml_remove_blank_text.py new file mode 100644 index 000000000..10634f5f9 --- /dev/null +++ b/XML/lxml__xml.etree__examples/lxml_remove_blank_text.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from lxml import etree + + +data: bytes = b"\r\nHello\r\n\r\nworld\r\n\r\n!\r\n" + +root = etree.fromstring(data) +result: bytes = etree.tostring(root) +print(result) +# b'\nHello\n\nworld\n\n!\n' + +assert result == b'\nHello\n\nworld\n\n!\n' + +print() + +root = etree.fromstring( + text=data, + parser=etree.XMLParser(remove_blank_text=True), +) +result: bytes = etree.tostring(root) +print(result) +# b'Hello\nworld\n\n!' + +assert result == b'Hello\nworld\n\n!' diff --git a/XML/lxml__xml.etree__examples/parse_WSDL/print__operation.py b/XML/lxml__xml.etree__examples/parse_WSDL/print__operation.py index e067985d8..fa6d215a8 100644 --- a/XML/lxml__xml.etree__examples/parse_WSDL/print__operation.py +++ b/XML/lxml__xml.etree__examples/parse_WSDL/print__operation.py @@ -1,16 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from lxml import etree -root = etree.parse(open('smev-message-exchange-service-1.3.wsdl', 'rb')) -for operation_node in root.xpath('//*[local-name()="portType"]/*[local-name()="operation"]'): - print(operation_node.attrib['name']) + +root = etree.parse(open("smev-message-exchange-service-1.3.wsdl", "rb")) + +for operation_node in root.xpath( + '//*[local-name()="portType"]/*[local-name()="operation"]' +): + print(operation_node.attrib["name"]) text = operation_node.xpath('./*[local-name()="documentation"]/text()')[0] - text = ' '.join(filter(None, map(str.strip, text.split('\n')))) + text = " ".join(filter(None, map(str.strip, text.split("\n")))) print(f' "{text}"') print() diff --git a/XML/lxml__xml.etree__examples/parse_html.py b/XML/lxml__xml.etree__examples/parse_html.py index ae871d2f6..f9ab60411 100644 --- a/XML/lxml__xml.etree__examples/parse_html.py +++ b/XML/lxml__xml.etree__examples/parse_html.py @@ -1,16 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from lxml import html -root = html.fromstring('

Hello
world!


') -print(html.tostring(root)) # b'

Hello
world!


' -print(html.tostring(root, encoding='unicode')) #

Hello
world!


-print(html.tostring(root, pretty_print=True)) # b'
\n

Hello
world!

\n
\n
\n' -print(html.tostring(root, encoding='unicode', pretty_print=True)) + +root = html.fromstring("

Hello
world!


") + +print(html.tostring(root)) # b'

Hello
world!


' +print(html.tostring(root, encoding="unicode")) #

Hello
world!


+print( + html.tostring(root, pretty_print=True) +) # b'
\n

Hello
world!

\n
\n
\n' +print(html.tostring(root, encoding="unicode", pretty_print=True)) #
#

Hello
world!

#
diff --git a/XML/lxml__xml.etree__examples/parse_xml.py b/XML/lxml__xml.etree__examples/parse_xml.py index 596e0ea0a..e3be61ec4 100644 --- a/XML/lxml__xml.etree__examples/parse_xml.py +++ b/XML/lxml__xml.etree__examples/parse_xml.py @@ -1,16 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from lxml import etree -root = etree.fromstring('Helloworld!') -print(etree.tostring(root)) # b'Helloworld!' -print(etree.tostring(root, encoding='unicode')) # Helloworld! -print(etree.tostring(root, pretty_print=True)) # b'\n Hello\n world!\n\n' -print(etree.tostring(root, encoding='unicode', pretty_print=True)) + +root = etree.fromstring("Helloworld!") + +print(etree.tostring(root)) # b'Helloworld!' +print(etree.tostring(root, encoding="unicode")) # Helloworld! +print( + etree.tostring(root, pretty_print=True) +) # b'\n Hello\n world!\n\n' +print(etree.tostring(root, encoding="unicode", pretty_print=True)) # # Hello # world! diff --git a/XML/lxml__xml.etree__examples/small_parsing_of_different_sites/https___aquapolis_ru_jelektronagrevatel-elecro-titan-optima-plus-380v_html.py b/XML/lxml__xml.etree__examples/small_parsing_of_different_sites/https___aquapolis_ru_jelektronagrevatel-elecro-titan-optima-plus-380v_html.py index 639f2fbbc..b6f73d799 100644 --- a/XML/lxml__xml.etree__examples/small_parsing_of_different_sites/https___aquapolis_ru_jelektronagrevatel-elecro-titan-optima-plus-380v_html.py +++ b/XML/lxml__xml.etree__examples/small_parsing_of_different_sites/https___aquapolis_ru_jelektronagrevatel-elecro-titan-optima-plus-380v_html.py @@ -1,32 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from lxml import html import requests +from lxml import html + -rs = requests.get('https://aquapolis.ru/jelektronagrevatel-elecro-titan-optima-plus-380v.html') +rs = requests.get( + "https://aquapolis.ru/jelektronagrevatel-elecro-titan-optima-plus-380v.html" +) root = html.fromstring(rs.content) def get_text(node): - return html.tostring(node, method='text', encoding='unicode').strip() + return html.tostring(node, method="text", encoding="unicode").strip() -for tr in root.cssselect('#super-product-table tr'): - tds = tr.cssselect('td') +for tr in root.cssselect("#super-product-table tr"): + tds = tr.cssselect("td") if not tds: continue name = get_text(tds[0]) - price = get_text(tds[1].cssselect('.price-box > .regular-price > .price')[0]) + price = get_text(tds[1].cssselect(".price-box > .regular-price > .price")[0]) - value = tds[2].cssselect('input')[0].get('value') + value = tds[2].cssselect("input")[0].get("value") stock_status = "Нет в наличии" if value == "0" else "Есть" - print('{:65} | {:16} | {}'.format(name, price, stock_status)) + print(f"{name:65} | {price:16} | {stock_status}") # Электронагреватель Elecro Titan Optima Plus СP-18 18 кВт (380В) | 211 572,00 руб. | Есть diff --git a/XML/sax_FIAS_with_progress__HOUSEGUID.py b/XML/sax_FIAS_with_progress__HOUSEGUID.py index c657f5372..39f211183 100644 --- a/XML/sax_FIAS_with_progress__HOUSEGUID.py +++ b/XML/sax_FIAS_with_progress__HOUSEGUID.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import xml.sax @@ -13,32 +13,32 @@ class AttrHandler(xml.sax.handler.ContentHandler): - def startDocument(self): + def startDocument(self) -> None: self.it = iter(tqdm(iter(lambda: 0, 1))) - def startElement(self, name, attrs): - if 'HOUSEGUID' in attrs: - all_house_guid.add(attrs['HOUSEGUID']) + def startElement(self, name, attrs) -> None: + if "HOUSEGUID" in attrs: + all_house_guid.add(attrs["HOUSEGUID"]) next(self.it) -print('Сбор HOUSEGUID') +print("Сбор HOUSEGUID") parser = xml.sax.make_parser() parser.setContentHandler(AttrHandler()) -parser.parse('AS_HOUSE_20210318_88f2df80-430a-400f-9373-da5b2c80e051.XML') +parser.parse("AS_HOUSE_20210318_88f2df80-430a-400f-9373-da5b2c80e051.XML") -print(f'Найдено {len(all_house_guid)}') +print(f"Найдено {len(all_house_guid)}") -print('Сохранение в JSON...') +print("Сохранение в JSON...") -with open('all_house_guid.json', 'w') as f: - f.write('[') +with open("all_house_guid.json", "w") as f: + f.write("[") for i, guid in tqdm(enumerate(all_house_guid), total=len(all_house_guid)): if i > 0: - f.write(',') + f.write(",") f.write(f'"{guid}"') - f.write(']') + f.write("]") diff --git a/XML/sax_FIAS_with_progress__HOUSEGUID__no_collect.py b/XML/sax_FIAS_with_progress__HOUSEGUID__no_collect.py index 62609dc5d..71e03517a 100644 --- a/XML/sax_FIAS_with_progress__HOUSEGUID__no_collect.py +++ b/XML/sax_FIAS_with_progress__HOUSEGUID__no_collect.py @@ -1,27 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import xml.sax from tqdm import tqdm -f = open('all_house_guid.json', 'w') -f.write('[') +f = open("all_house_guid.json", "w") +f.write("[") class AttrHandler(xml.sax.handler.ContentHandler): - def startDocument(self): + def startDocument(self) -> None: self.it = iter(tqdm(iter(lambda: 0, 1))) self.number = 0 - def startElement(self, name, attrs): - if 'HOUSEGUID' in attrs: - guid = attrs['HOUSEGUID'] + def startElement(self, name, attrs) -> None: + if "HOUSEGUID" in attrs: + guid = attrs["HOUSEGUID"] if self.number > 0: - f.write(',') + f.write(",") f.write(f'"{guid}"') self.number += 1 @@ -29,11 +29,11 @@ def startElement(self, name, attrs): next(self.it) -print('Сбор HOUSEGUID и сохранение в JSON') +print("Сбор HOUSEGUID и сохранение в JSON") parser = xml.sax.make_parser() parser.setContentHandler(AttrHandler()) -parser.parse('AS_HOUSE_20210318_88f2df80-430a-400f-9373-da5b2c80e051.XML') +parser.parse("AS_HOUSE_20210318_88f2df80-430a-400f-9373-da5b2c80e051.XML") -f.write(']') +f.write("]") f.close() diff --git a/XML/sax_FIAS_with_progress__HOUSEGUID__no_collect__with_write_handler.py b/XML/sax_FIAS_with_progress__HOUSEGUID__no_collect__with_write_handler.py index 916bb10e1..644ab2882 100644 --- a/XML/sax_FIAS_with_progress__HOUSEGUID__no_collect__with_write_handler.py +++ b/XML/sax_FIAS_with_progress__HOUSEGUID__no_collect__with_write_handler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import xml.sax @@ -9,37 +9,37 @@ class WriteHouseGuidHandler(xml.sax.handler.ContentHandler): - def __init__(self, file_name: str): + def __init__(self, file_name: str) -> None: super().__init__() - self.f = open(file_name, 'w') + self.f = open(file_name, "w") self.it = None self.number = 0 - def startDocument(self): + def startDocument(self) -> None: self.it = iter(tqdm(iter(lambda: 0, 1))) self.number = 0 - self.f.write('[') + self.f.write("[") - def startElement(self, name, attrs): - if 'HOUSEGUID' in attrs: - guid = attrs['HOUSEGUID'] + def startElement(self, name, attrs) -> None: + if "HOUSEGUID" in attrs: + guid = attrs["HOUSEGUID"] if self.number > 0: - self.f.write(',') + self.f.write(",") self.f.write(f'"{guid}"') self.number += 1 next(self.it) - def endDocument(self): - self.f.write(']') + def endDocument(self) -> None: + self.f.write("]") self.f.close() -print('Сбор HOUSEGUID и сохранение в JSON') +print("Сбор HOUSEGUID и сохранение в JSON") parser = xml.sax.make_parser() -parser.setContentHandler(WriteHouseGuidHandler('all_house_guid.json')) -parser.parse('AS_HOUSE_20210318_88f2df80-430a-400f-9373-da5b2c80e051.XML') +parser.setContentHandler(WriteHouseGuidHandler("all_house_guid.json")) +parser.parse("AS_HOUSE_20210318_88f2df80-430a-400f-9373-da5b2c80e051.XML") diff --git a/XML/xml.dom.minidom__examples/print_element_with_max_date.py b/XML/xml.dom.minidom__examples/print_element_with_max_date.py index 6d6a87507..294384d00 100644 --- a/XML/xml.dom.minidom__examples/print_element_with_max_date.py +++ b/XML/xml.dom.minidom__examples/print_element_with_max_date.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import xml.dom.minidom from datetime import datetime + # SOURCE: https://ru.stackoverflow.com/questions/905565/ text = """\ @@ -25,16 +27,13 @@ def to_date(date_str): - return datetime.strptime(date_str, '%Y-%m-%d') + return datetime.strptime(date_str, "%Y-%m-%d") -# Из стандартной библиотеки -import xml.dom.minidom - dom = xml.dom.minidom.parseString(text) -items = dom.getElementsByTagName('Leader') -leader = max(items, key=lambda x: to_date(x.attributes['ActualDate'].value)) +items = dom.getElementsByTagName("Leader") +leader = max(items, key=lambda x: to_date(x.attributes["ActualDate"].value)) -print(leader.attributes['FIO'].value) # Шxxxxxxx Аxxxxx Шxxxxxx -print(leader.attributes['ActualDate'].value) # 2009-12-01 -print(leader.attributes['Position'].value) # генеральный директор +print(leader.attributes["FIO"].value) # Шxxxxxxx Аxxxxx Шxxxxxx +print(leader.attributes["ActualDate"].value) # 2009-12-01 +print(leader.attributes["Position"].value) # генеральный директор diff --git a/XML/xml.etree.ElementTree__examples/create_xml.py b/XML/xml.etree.ElementTree__examples/create_xml.py index 5dfad2993..6f63fdb9e 100644 --- a/XML/xml.etree.ElementTree__examples/create_xml.py +++ b/XML/xml.etree.ElementTree__examples/create_xml.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://docs.python.org/3/library/xml.etree.elementtree.html @@ -12,28 +12,28 @@ from pretty_print import indent -root = ET.Element('data') +root = ET.Element("data") -country = ET.Element('country', name="Liechtenstein") +country = ET.Element("country", name="Liechtenstein") -rank = ET.Element('rank', updated="yes") -rank.text = '2' +rank = ET.Element("rank", updated="yes") +rank.text = "2" country.append(rank) -year = ET.Element('year') -year.text = '2008' +year = ET.Element("year") +year.text = "2008" country.append(year) -gdppc = ET.Element('gdppc') -gdppc.text = '141100' +gdppc = ET.Element("gdppc") +gdppc.text = "141100" country.append(gdppc) -country.append(ET.Element('neighbor', name="Austria", direction="E")) -country.append(ET.Element('neighbor', name="Switzerland", direction="W")) +country.append(ET.Element("neighbor", name="Austria", direction="E")) +country.append(ET.Element("neighbor", name="Switzerland", direction="W")) root.append(country) -country = ET.Element('country', name="Singapore") +country = ET.Element("country", name="Singapore") root.append(country) ... @@ -55,7 +55,7 @@ etree = ET.ElementTree(root) f = io.BytesIO() -etree.write(f, encoding='utf-8', xml_declaration=True) +etree.write(f, encoding="utf-8", xml_declaration=True) print(f.getvalue().decode(encoding="utf-8")) # # diff --git a/XML/xml.etree.ElementTree__examples/dict_to_xml.etree.ElementTree.py b/XML/xml.etree.ElementTree__examples/dict_to_xml.etree.ElementTree.py index cbb7bd860..f59a0e96e 100644 --- a/XML/xml.etree.ElementTree__examples/dict_to_xml.etree.ElementTree.py +++ b/XML/xml.etree.ElementTree__examples/dict_to_xml.etree.ElementTree.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import xml.etree.ElementTree as ET @@ -12,16 +12,16 @@ {"first_name": "Sergey", "last_name": "Sidorov", "city": "Sochi"}, ] -root = ET.Element('root') +root = ET.Element("root") for i, item in enumerate(items, 1): - person = ET.SubElement(root, f'person{i}') + person = ET.SubElement(root, f"person{i}") for k, v in item.items(): ET.SubElement(person, k).text = v tree = ET.ElementTree(root) -tree.write('person.xml') +tree.write("person.xml") # # # Ivan diff --git a/XML/xml.etree.ElementTree__examples/pretty_print.py b/XML/xml.etree.ElementTree__examples/pretty_print.py index 4561ca3c7..93836b547 100644 --- a/XML/xml.etree.ElementTree__examples/pretty_print.py +++ b/XML/xml.etree.ElementTree__examples/pretty_print.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://docs.python.org/3/library/xml.etree.elementtree.html @@ -10,15 +10,15 @@ # SOURCE: http://effbot.org/zone/element-lib.htm#prettyprint -def indent(elem, level=0): - i = "\n" + level*" " +def indent(elem, level=0) -> None: + i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " if not elem.tail or not elem.tail.strip(): elem.tail = i for elem in elem: - indent(elem, level+1) + indent(elem, level + 1) if not elem.tail or not elem.tail.strip(): elem.tail = i else: @@ -26,12 +26,12 @@ def indent(elem, level=0): elem.tail = i -if __name__ == '__main__': - root = ET.Element('data') +if __name__ == "__main__": + root = ET.Element("data") - country = ET.Element('country', name="Liechtenstein") - rank = ET.Element('rank', updated="yes") - rank.text = '2' + country = ET.Element("country", name="Liechtenstein") + rank = ET.Element("rank", updated="yes") + rank.text = "2" country.append(rank) root.append(country) diff --git a/XML/xml.etree.ElementTree__examples/print_element_with_max_date.py b/XML/xml.etree.ElementTree__examples/print_element_with_max_date.py index 2cf12595d..6f1bc5971 100644 --- a/XML/xml.etree.ElementTree__examples/print_element_with_max_date.py +++ b/XML/xml.etree.ElementTree__examples/print_element_with_max_date.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import xml.etree.ElementTree as ET from datetime import datetime + # SOURCE: https://ru.stackoverflow.com/questions/905565/ text = """\ @@ -25,18 +27,16 @@ def to_date(date_str): - return datetime.strptime(date_str, '%Y-%m-%d') + return datetime.strptime(date_str, "%Y-%m-%d") -# Из стандартной библиотеки -import xml.etree.ElementTree as ET root = ET.fromstring(text) -items = root.iter('Leader') +items = root.iter("Leader") # OR: # items = root.findall('.//Leader') -leader = max(items, key=lambda x: to_date(x.attrib['ActualDate'])) +leader = max(items, key=lambda x: to_date(x.attrib["ActualDate"])) -print(leader.attrib['FIO']) # Шxxxxxxx Аxxxxx Шxxxxxx -print(leader.attrib['ActualDate']) # 2009-12-01 -print(leader.attrib['Position']) # генеральный директор +print(leader.attrib["FIO"]) # Шxxxxxxx Аxxxxx Шxxxxxx +print(leader.attrib["ActualDate"]) # 2009-12-01 +print(leader.attrib["Position"]) # генеральный директор diff --git a/XML/xml.etree.ElementTree__examples/xml_declaration.py b/XML/xml.etree.ElementTree__examples/xml_declaration.py index 11ce349bb..a1cab22d8 100644 --- a/XML/xml.etree.ElementTree__examples/xml_declaration.py +++ b/XML/xml.etree.ElementTree__examples/xml_declaration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://docs.python.org/3/library/xml.etree.elementtree.html @@ -12,12 +12,12 @@ from pretty_print import indent -root = ET.Element('data') +root = ET.Element("data") -country = ET.Element('country', name="Liechtenstein") +country = ET.Element("country", name="Liechtenstein") -rank = ET.Element('rank', updated="yes") -rank.text = '2' +rank = ET.Element("rank", updated="yes") +rank.text = "2" country.append(rank) root.append(country) @@ -34,7 +34,7 @@ etree = ET.ElementTree(root) f = io.BytesIO() -etree.write(f, encoding='utf-8', xml_declaration=True) +etree.write(f, encoding="utf-8", xml_declaration=True) print(f.getvalue().decode(encoding="utf-8")) # # diff --git a/XML/xml.parsers.expat__examples__like_sax/hello_world.py b/XML/xml.parsers.expat__examples__like_sax/hello_world.py index 18dbd6df6..0fa8d4d0d 100644 --- a/XML/xml.parsers.expat__examples__like_sax/hello_world.py +++ b/XML/xml.parsers.expat__examples__like_sax/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://docs.python.org/3.6/library/pyexpat.html @@ -11,16 +11,16 @@ # 3 handler functions -def on_start_element(name, attrs): - print('Start element:', name, attrs) +def on_start_element(name, attrs) -> None: + print("Start element:", name, attrs) -def on_end_element(name): - print('End element:', name) +def on_end_element(name) -> None: + print("End element:", name) -def on_char_data(data): - print('Character data:', repr(data)) +def on_char_data(data) -> None: + print("Character data:", repr(data)) p = xml.parsers.expat.ParserCreate() @@ -28,9 +28,12 @@ def on_char_data(data): p.EndElementHandler = on_end_element p.CharacterDataHandler = on_char_data -p.Parse("""\ +p.Parse( + """\ Text goes here More text -""", 1) +""", + 1, +) diff --git a/XML/xml_replace_comments/xml_replace_comments.py b/XML/xml_replace_comments/xml_replace_comments.py index 88454cb4e..868ee74e4 100644 --- a/XML/xml_replace_comments/xml_replace_comments.py +++ b/XML/xml_replace_comments/xml_replace_comments.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re @@ -9,39 +9,39 @@ from lxml import etree -def replace(file_name, save_file_name): - with open(file_name, encoding='utf8') as f: +def replace(file_name, save_file_name) -> None: + with open(file_name, encoding="utf8") as f: text = f.read() # Ищем закрывающиеся скобки комментариев и удаляем из них многочисленные дефисы, из-за которых # xml парсер ругается - text = re.sub('-{3,}>', '-->', text) + text = re.sub("-{3,}>", "-->", text) - for match in re.findall('', text): + for match in re.findall("", text): # Вырезаем текст из тега комментария comment_text = match[4:][:-3] # Заменяем дефисы одинарным, если они подряд идут от 2 и больше - comment_text = re.sub('-{2,}', '-', comment_text) - comment_tag = ''.format(comment_text) + comment_text = re.sub("-{2,}", "-", comment_text) + comment_tag = f"" if match != comment_tag: print(match) - print('\t', comment_tag, '\n') + print("\t", comment_tag, "\n") # Делаем замему в тексте text = text.replace(match, comment_tag) - with open(save_file_name, mode='w', encoding='utf8') as f_in: + with open(save_file_name, mode="w", encoding="utf8") as f_in: f_in.write(text) # TODO: уверен есть какой то метож для проверки валидности xml # Пытаемся открыть парсером, если не получится, значит xml еще невалидная - with open(save_file_name, encoding='utf8') as fb2: + with open(save_file_name, encoding="utf8") as fb2: tree = etree.XML(fb2.read().encode()) -if __name__ == '__main__': +if __name__ == "__main__": # file_name = "Mahouka_Koukou_no_Rettousei_13.fb2" # replace(file_name, '_' + file_name) @@ -59,7 +59,7 @@ def replace(file_name, save_file_name): # tree = etree.XML(fb2.read().encode()) file_name = "Mahouka_Koukou_no_Rettousei_16.fb2" - replace(file_name, '_' + file_name) + replace(file_name, "_" + file_name) # for i in range(13, 17): # file_name = "Mahouka_Koukou_no_Rettousei_{}.fb2".format(i) diff --git a/_FOO_TEST_TEST/1.html b/_FOO_TEST_TEST/1.html new file mode 100644 index 000000000..16931446f --- /dev/null +++ b/_FOO_TEST_TEST/1.html @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
38
39
40
010203040506070809101112
2024
+ +

+ + + + + + + + + + + + +
091011120102030405060708
20232024
+ +

+ + + + + + + + + + + + + +
120102030405060708091011
20222023
diff --git a/_FOO_TEST_TEST/FOO_TEST_TEST.py b/_FOO_TEST_TEST/FOO_TEST_TEST.py index 962fa3083..86c64dacc 100644 --- a/_FOO_TEST_TEST/FOO_TEST_TEST.py +++ b/_FOO_TEST_TEST/FOO_TEST_TEST.py @@ -1,7 +1,888 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +# # pip install ollama==0.6.1 +# import ollama +# from ollama import chat +# from ollama import ChatResponse +# +# +# response = ollama.generate( +# model='qwen2.5:7b', +# prompt="Выдели из фразы: 'Напомни купить молоко завтра в 9 утра' событие и время.", +# format='json', # КЛЮЧЕВОЙ МОМЕНТ +# system="Отвечай строго в формате JSON с полями 'task' и 'time'.", +# ) +# print(response['response']) +# # +# # rs = requests.get("http://localhost:11434") +# # print(rs.text) + +from ollama import chat +from pydantic import BaseModel + +# 1. Описываем структуру данных +class CityInfo(BaseModel): + city: str + population: int + is_capital: bool + about: str + region: str + country: str + +class PersonInfo(BaseModel): + full_name: str + url_github: str + city: str + about: str + country: str + +# # 2. Делаем запрос +# response = chat( +# # model='qwen2.5', +# model='qwen3:4b', +# messages=[ +# {'role': 'system', 'content': 'Отвечай на вопросы на русском языке'}, +# {'role': 'user', 'content': 'Расскажи про gil9red'}, +# ], +# format=PersonInfo.model_json_schema(), # Передаем схему JSON +# options={'temperature': 0}, +# ) +# +# # 3. Валидируем и превращаем в объект Python +# print(response) +# print(response.message.content) +# person_data = PersonInfo.model_validate_json(response.message.content) +# print(person_data) + +from datetime import datetime +dt = datetime.now() + +# 2. Делаем запрос +response = chat( + # model='qwen2.5', + model='qwen3:4b', + messages=[ + {'role': 'system', 'content': 'Отвечай на вопросы на русском языке'}, + {'role': 'user', 'content': 'Расскажи про Магнитогорск'}, + ], + format=CityInfo.model_json_schema(), # Передаем схему JSON + options={'temperature': 0}, +) + +# 3. Валидируем и превращаем в объект Python +print(response) +print(response.message.content) +city_data = CityInfo.model_validate_json(response.message.content) +print(city_data) +print(city_data.city, city_data.population) +print(datetime.now() - dt) + +quit() + +import sys +import traceback +from random import randint +from timeit import default_timer + +from PyQt6.QtWidgets import ( + QApplication, + QGraphicsScene, + QGraphicsView, + QGraphicsItem, + QGraphicsRectItem, + QGraphicsEllipseItem, + QMessageBox, + QMainWindow, +) +from PyQt6.QtCore import QRectF, QLineF, Qt, QTimer + + +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}\n" + text += "".join(traceback.format_tb(tb)) + print(text) + + if QApplication.instance(): + msg_box = QMessageBox( + QMessageBox.Critical, + "Error", + f"Error: {ex}", + parent=None, + ) + msg_box.setDetailedText(text) + msg_box.setStandardButtons(QMessageBox.Ok) + msg_box.exec() + + +sys.excepthook = log_uncaught_exceptions + + +# TODO: Пусть будет 13 x 3 +board = """\ + x x x x x +xxx xxx xxx +xxx x x xxx +""".rstrip() +# TODO: +brick_width: int = 40 +brick_height: int = 20 + +app = QApplication([]) + +scene_width, scene_height = 600, 300 +scene_rect = QRectF(0, 0, scene_width, scene_height) + +scene = QGraphicsScene() +scene.setSceneRect(scene_rect) + +# TODO: У линий есть проблема с координатами как у QRectF? +scene_top_line_item = scene.addLine(QLineF(scene_rect.topLeft(), scene_rect.topRight())) +scene_left_line_item = scene.addLine( + QLineF(scene_rect.topLeft(), scene_rect.bottomLeft()) +) +scene_bottom_line_item = scene.addLine( + QLineF(scene_rect.bottomRight(), scene_rect.bottomLeft()) +) +scene_right_line_item = scene.addLine( + QLineF(scene_rect.topRight(), scene_rect.bottomRight()) +) + +print( + "scene_top_line_item", + scene_top_line_item.sceneBoundingRect(), + scene_top_line_item.sceneBoundingRect().bottom(), +) +print( + "scene_left_line_item", + scene_left_line_item.sceneBoundingRect(), + scene_left_line_item.sceneBoundingRect().right(), +) +print( + "scene_bottom_line_item", + scene_bottom_line_item.sceneBoundingRect(), + scene_bottom_line_item.sceneBoundingRect().top(), +) +print( + "scene_right_line_item", + scene_right_line_item.sceneBoundingRect(), + scene_right_line_item.sceneBoundingRect().left(), +) + +bricks: list[QGraphicsRectItem] = [] +top: int = 0 +for line in board.splitlines(): + print(repr(line)) + left: int = 0 + for x in line: + if x == "x": + ball_item = scene.addRect( + QRectF(0, 0, brick_width, brick_height), + brush=Qt.GlobalColor.red, + ) + ball_item.setPos(left, top) + bricks.append(ball_item) + left += brick_width + + top += brick_height + +ball_radius: int = 40 + +platform_width: int = 100 +platform_height: int = 20 + +# TODO: +platform_item = scene.addRect( + QRectF( + 0, + 0, + platform_width, + platform_height, + ), + brush=Qt.GlobalColor.red, +) +platform_item.setPos( + (scene.width() / 2) - (platform_width / 2), scene.height() - platform_height +) +platform_item.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable) + +ball_item = scene.addEllipse( + QRectF( + 0, + 0, + ball_radius, + ball_radius, + ), + brush=Qt.GlobalColor.green, +) +ball_item.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable) + +ball_item.setPos( + platform_item.sceneBoundingRect().center().x() + - (ball_item.sceneBoundingRect().width() / 2), + platform_item.sceneBoundingRect().top() - ball_item.sceneBoundingRect().height(), +) + + +class Ball: + def __init__(self, ball_item: QGraphicsEllipseItem, v_x, v_y) -> None: + # def __init__(self, x, y, r, v_x, v_y, color): + # TODO: + self.x = ball_item.x() + self.y = ball_item.y() + self.r = ball_item.sceneBoundingRect().width() + + self.ball_item = ball_item + self.v_x = v_x + self.v_y = v_y + + self.is_collision: bool = False + # self.color = color + + def update(self) -> None: + self.x += self.v_x + self.y += self.v_y + + self.ball_item.setPos(self.x, self.y) + + # def draw(self): + # # def draw(self, screen): + # self.ball_item.setPos(self.center) + # # pygame.draw.circle(screen, self.color, self.center, self.r) + # # + # # # Нарисуем поверх первого, прозрачный второй с границей (параметр width) + # # pygame.draw.circle(screen, (0, 0, 0), self.center, self.r, 1) + + # @property + # def center(self): + # return self.x, self.y + # + # @property + # def top(self): + # return self.y - self.r + # + # @property + # def bottom(self): + # return self.y + self.r + # + # @property + # def left(self): + # return self.x - self.r + # + # @property + # def right(self): + # return self.x + self.r + + +# class Ball: +# r = 50 # Радиус шарика +# x = 0 # Координата по х центра шарика +# y = 0 # Координата по y центра шарика +# speed = 0 # Скорость движения +# dir_x = 0 # Компонент x вектора движения шарика +# dir_y = 0 # Компонент y вектора движения шарика +# # TODO: Не нужно +# damp = 10 # Скорость уменьшения скорости движения (сопротивление) +# collision = False # Признак коллизии с внешним кругом +# # TODO: Не нужно +# speed_after_collision = 300 # Скорость движения шарика после столкновения +# +# # # Функция, которая проверяет наличие коллизии шарика с внешним кругом +# # def hit_outer_circle_check(self, outer_circle: int): +# # dr = outer_circle - self.r # Разница радиусов +# # +# # # По теореме пифагора проверяем выход за пределы круга (коллизию) +# # if self.x * self.x + self.y * self.y > dr * dr: +# # # Если коллизия уже была обсчитана, но шарик еще не вернулся в круг, +# # # чтобы он не застревал больше не надо обсчитывать коллизии, поэтому выходим +# # if self.collision: +# # return +# # +# # # Устанавливаем для шарика признак коллизии +# # self.collision = True +# # +# # # Далее идет код расчета нового вектора движения +# # +# # # Найдем вектор нормали. тут он берется приближенно, +# # # в точке центра шарика в момент обсчета коллизии, +# # # при том что шарик уже проскочил границу. по идее тут +# # # необходимо посчитать точку соударения геометрически. +# # max_value = max(abs(self.x), abs(self.y)) +# # nx = -self.x / max_value +# # ny = -self.y / max_value +# # +# # # Найдем новый вектор движения по формуле +# # # r = i−2(i⋅n)n , где +# # # i - исходный вектор +# # # n - нормаль +# # # ⋅ знак скалярного произведения +# # +# # dot2 = self.dir_x * nx * 2 + self.dir_y * ny * 2 +# # self.dir_x = self.dir_x - dot2 * nx +# # self.dir_y = self.dir_y - dot2 * ny +# # +# # # Нормализуем вектор движения +# # max_value = max(abs(self.dir_x), abs(self.dir_y)) +# # self.dir_x /= max_value +# # self.dir_y /= max_value +# # +# # else: +# # # Сбрасываем признак коллизии когда шарик вернулся в круг. +# # self.collision = False +# # +# # # Функция проверки коллизии шарика и мышки +# # def hit_mouse_check(self, x, y): +# # # Если есть коллизия с внешним кругом игнорируем мышку +# # if self.collision: +# # return +# # +# # # Разница координат мышки и шарика +# # dx = self.x - x +# # dy = self.y - y +# # +# # # Проверяем по теореме Пифагора столкновение с мышкой +# # if dx * dx + dy * dy < self.r * self.r: +# # # Задаем вектор движения и нормализуем его +# # max_value = max(abs(dx), abs(dy)) +# # if not max_value: +# # return +# # +# # self.dir_x = dx / max_value +# # self.dir_y = dy / max_value +# # +# # # Задаем скорость +# # self.speed = self.speed_after_collision +# +# # Тут осуществляется передвижение +# # dt - кол-во секунд с прошлого обсчета +# def do_move(self, dt): +# # К текущей координате прибавляем вектор скорости помноженный +# # на значение скорости помноженные на прошедшее время +# self.x += self.dir_x * self.speed * dt +# self.y += self.dir_y * self.speed * dt +# +# # Тормозим объект, так же на значение зависящее от времени +# self.speed = max(0, self.speed - self.damp * dt) + + +class MainWindow(QMainWindow): + def __init__(self) -> None: + super().__init__() + + self.setWindowTitle("TODO") + + self.view = QGraphicsView() + self.view.setScene(scene) + + scene.changed.connect(self.on_scene_changed) + + timeout = 1000 // 60 + + # Используется, чтобы в независимости от количества вызовов + # tick скорость шарика была одинаковая + self.t = 0 + + # Таймер обновления движения и обработки столкновения шариков + self.timer = QTimer() + self.timer.timeout.connect(self.tick) + # TODO: + # self.timer.start(timeout) + + # TODO: Вектор вниз не нужно генерировать + def get_random_vector() -> tuple[int, int]: + pos = 0, 0 + # Если pos равен (0, 0), пересчитываем значения, т.к. шарик должен двигаться + while pos == (0, 0): + pos = randint(-3, 3), randint(-3, 3) + + return pos + + v_x, v_y = get_random_vector() + self.ball = Ball(ball_item=ball_item, v_x=v_x, v_y=v_y) + # self.ball.dir_x + # self.ball.r = ball_item.sceneBoundingRect().width() # TODO: + # self.ball.x = ball_item.sceneBoundingRect().center().x() # TODO: просто ball_item.x()? + # self.ball.y = ball_item.sceneBoundingRect().center().y() + + self.setCentralWidget(self.view) + + # TODO: + def tick(self) -> None: + # TODO: Использовать + # Считаем сколько времени прошло с прошлого обсчета + dt = default_timer() - self.t + + # self.ball.hit_mouse_check(self.mouse_center_x, self.mouse_center_y) + # self.ball.do_move(dt) + # self.ball.hit_outer_circle_check(self.outer_circle) + + ball = self.ball # TODO: + # ball.draw() + ball.update() + + # TODO: определять глубину проникновения шарика за границы и выталкивать его перед сменой вектора движения + + # # Условия отскакивания шарика от левого и правого края + # if ball.left <= 0 or ball.right >= self.width: + # ball.v_x = -ball.v_x + # + # # Условия отскакивания шарика верхнего и нижнего края + # if ball.top <= 0 or ball.bottom >= self.height: + # ball.v_y = -ball.v_y + + self.t = default_timer() + # + # self.update() + + def on_scene_changed(self, region: list[QRectF]) -> None: + print("on_scene_changed", region) + + # if self.ball.is_collision: + # return + + # TODO: технически, ball_item может быть много + + # TODO: Проверка выхода за сцену ball_item + colliding_items = ball_item.collidingItems() + print(colliding_items) + + # for item in colliding_items: + # color = Qt.GlobalColor.darkMagenta if item.collidesWithItem(ball_item) else Qt.GlobalColor.red + # + # if isinstance(item, QGraphicsRectItem): + # item.setBrush(color) + # elif isinstance(item, QGraphicsLineItem): + # item.setPen(color) + + collisions: list[str] = [] + for brick in bricks: + brick.setBrush( + Qt.GlobalColor.darkMagenta + if brick.collidesWithItem(ball_item) + else Qt.GlobalColor.red + ) + if brick in colliding_items: + # if brick.collidesWithItem(ball_item): # TODO: + collisions.append("brick") + + # NOTE: Фиксация по Y + platform_item.setY(scene.sceneRect().bottom() - platform_height) + + if ( + platform_item.sceneBoundingRect().left() + <= scene_left_line_item.sceneBoundingRect().right() + ): + platform_item.setX(scene_left_line_item.sceneBoundingRect().right()) + elif ( + platform_item.sceneBoundingRect().right() + >= scene_right_line_item.sceneBoundingRect().left() + ): + # TODO: Немного не доходит до границ + platform_item.setX( + scene_right_line_item.sceneBoundingRect().left() + - platform_item.sceneBoundingRect().width() + ) + + # if platform_item.collidesWithItem(ball_item): # TODO: + if platform_item in colliding_items: + platform_item.setBrush(Qt.GlobalColor.darkMagenta) + collisions.append("platform") + else: + platform_item.setBrush(Qt.GlobalColor.red) + + # if scene_top_line_item.collidesWithItem(ball_item): # TODO: + if scene_top_line_item in colliding_items: + collisions.append("top") + scene_top_line_item.setPen(Qt.GlobalColor.red) + else: + scene_top_line_item.setPen(Qt.GlobalColor.black) + + # if scene_right_line_item.collidesWithItem(ball_item): # TODO: + if scene_right_line_item in colliding_items: + collisions.append("right") + scene_right_line_item.setPen(Qt.GlobalColor.red) + else: + scene_right_line_item.setPen(Qt.GlobalColor.black) + + # if scene_bottom_line_item.collidesWithItem(ball_item): # TODO: + if scene_bottom_line_item in colliding_items: + collisions.append("bottom") + scene_bottom_line_item.setPen(Qt.GlobalColor.red) + else: + scene_bottom_line_item.setPen(Qt.GlobalColor.black) + + # if scene_left_line_item.collidesWithItem(ball_item): # TODO: + if scene_left_line_item in colliding_items: + collisions.append("left") + scene_left_line_item.setPen(Qt.GlobalColor.red) + else: + scene_left_line_item.setPen(Qt.GlobalColor.black) + + self.setWindowTitle( + f"collidingItems: {len(colliding_items)}. Collisions: {', '.join(collisions)}" + ) + + # Условия отскакивания шарика от левого и правого края + ball = self.ball # TODO: + # ball.is_collision = bool(collisions) + + if "left" in collisions or "right" in collisions: + ball.v_x = -ball.v_x + ball.is_collision = True + else: + ball.is_collision = False + + # Условия отскакивания шарика верхнего и нижнего края + if "top" in collisions or "bottom" in collisions: + ball.v_y = -ball.v_y + ball.is_collision = True + else: + ball.is_collision = False + + +mw = MainWindow() + +# TODO: +# n = 3 +# view.scale(1.0 / n, 1.0 / n) + +mw.resize(scene_width + 20, scene_height + 20) +mw.show() + +app.exec() + + +quit() + +from datetime import date, timedelta, datetime + +start = date(year=1992, month=8, day=18) +year = 1 +while True: + print(year, start) + start += timedelta(days=365) + year += 1 + if start.year > 2025: + break + +# print((date.today() - ).days / 366) + +quit() + +from pathlib import Path + +from typing import Any, Generator, Sized + + +def chunks(l: Sized, n: int) -> Generator[Any, None, None]: + """Yield successive n-sized chunks from l.""" + for i in range(0, len(l), n): + yield l[i : i + n] + + +p = Path("C:/Users/ipetrash/Downloads/0000_0039_Trnv_P_20250201_01_CIB0983543.ebc") +data = p.read_bytes() +for line in chunks(data, 170): + print(line.hex().upper()) + +quit() + +import copy2clipboard__via_pyperclip as copy2clipboard + +while n := input(): + value = n.title() + copy2clipboard.to(value) + print(value + "\n") + + +quit() + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import ( + QApplication, + QDockWidget, + QMainWindow, + QTextEdit, + QPushButton, +) + +import sys +import traceback + +from PyQt5.QtWidgets import QApplication, QTextEdit, QMessageBox + + +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) + + print(text) + QMessageBox.critical(None, "Error", text) + sys.exit(1) + + +sys.excepthook = log_uncaught_exceptions + + +app = QApplication([]) + +dock_widget_2 = QDockWidget("Right2") +pb = QPushButton("!!!", clicked=dock_widget_2.setFloating) +pb.setCheckable(True) +dock_widget_2.setTitleBarWidget(pb) + +dock_widget_left = QDockWidget("Left") +# TODO: Добавить кнопку PIN, которая вытаскивает доквиджет, отвязывает от родителя, делает поверх всех окон +# Показывать кнопку возврата обратно +dock_widget_left.topLevelChanged.connect( + lambda flag: ( + dock_widget_left.setWindowFlag(Qt.WindowStaysOnTopHint, flag), + dock_widget_left.setParent(None) if flag else None, + dock_widget_left.show(), + ) +) +# dock_widget_left.setWindowFlags(Qt.WindowType.Window) +# dock_widget_left.show() + +mw = QMainWindow() +mw.setCentralWidget(QTextEdit()) +mw.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, QDockWidget("Right")) +mw.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dock_widget_2) +mw.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock_widget_left) +mw.show() + +app.exec() + + +quit() + +from dataclasses import dataclass, field +from datetime import datetime, timedelta, date, timezone + + +def add_to_month(d: date, inc: bool = True, number: int = 1) -> date: + d = d.replace(day=1) + + year = d.year + month = d.month + + for _ in range(number): + month += 1 if inc else -1 + if month > 12: + month = 1 + year += 1 + elif month < 1: + month = 12 + year -= 1 + + d = date(year=year, month=month, day=1) + + return d + + +""" +Date: 06 мая 2024 г. 21:11:42 | Release version 3.2.40.10 (release based on revision 324264) +Date: 03 июля 2024 г. 19:11:35 | Release version 3.2.41.10 (release based on revision 327113) +Date: 04 сентября 2024 г. 15:08:49 | Release version 3.2.42.10 (release based on revision 330920) +Date: 05 ноября 2024 г. 19:40:28 | Release version 3.2.43.10 (release based on revision 335027) + +26, 11.01.2022, 14.03.2022, 15.11.2022 +8, + +27, 03.03.2022, 27.05.2022, 09.12.2022 +7, +1 +28, 05.05.2022, 15.07.2022, 10.02.2023 +7, +2 +29, 04.07.2022, 15.09.2022, 24.05.2023 +8, +3 + +30, 01.09.2022, 15.11.2022, 05.06.2023 +7, +1 +31, 01.11.2023, 19.01.2023, 01.08.2023 +7, +2 +32, 09.01.2023, 14.03.2023, 07.11.2023 +8, +3 + +33, 02.03.2023, 18.05.2023, 04.12.2023 +7, +1 +34, 04.05.2023, 21.07.2023, 01.02.2024 +7, +2 +35, 05.07.2023, 15.09.2023, 03.05.2024 +8, +3 + +36, 04.09.2023, 21.11.2023, 07.06.2024 +7, +1 +37, 02.11.2023, 23.01.2024, 05.08.2024 +7, +2 +38, 11.01.2024, 15.03.2024, 28.11.2024 +8, +3 +""" + + +INIT_RELEASE_VERSION: int = 31 +INIT_RELEASE_DATE: date = date(year=2022, month=11, day=1) + + +@dataclass +class Release: + version: int + date: date + free_commit_date: date = field(init=False) + testing_finish_date: date = field(init=False) + support_end_date: date = field(init=False) + + def __post_init__(self) -> None: + self.free_commit_date = add_to_month(self.date, number=1) - timedelta(days=1) + self.testing_finish_date = add_to_month(self.date, number=2) + self.support_end_date = add_to_month( + self.testing_finish_date, + # NOTE: Месяца 3 и 9, похоже, связаны с IPS mandates + number=8 if self.testing_finish_date.month in (3, 9) else 7, + ) + + @classmethod + def get_by(cls, d: date = None, version: int = None) -> "Release": + if d is None and version is None: + # TODO: Нормальное исключение + raise Exception() + + if d is not None: + _is_found = lambda r: r.date <= d < r.testing_finish_date + else: + _is_found = lambda r: r.version == version + + if d is not None: + _is_need_next = lambda r: d > r.date + else: + _is_need_next = lambda r: version > r.version + + release = Release( + version=INIT_RELEASE_VERSION, + date=INIT_RELEASE_DATE, + ) + + while True: + if _is_found(release): + return release + + release = ( + release.get_next_release() + if _is_need_next(release) + else release.get_prev_release() + ) + + @classmethod + def get_by_date(cls, d: date) -> "Release": + return cls.get_by(d=d) + + @classmethod + def get_by_version(cls, version: int) -> "Release": + return cls.get_by(version=version) + + @classmethod + def get_last_release(cls) -> "Release": + return cls.get_by_date(date.today()) + + def get_next_release(self) -> "Release": + return Release( + version=self.version + 1, + date=add_to_month(self.date, number=2), + ) + + def get_prev_release(self) -> "Release": + return Release( + version=self.version - 1, + date=add_to_month(self.date, inc=False, number=2), + ) + + def is_last_release(self) -> bool: + return self == self.get_last_release() + + +last_release: Release = Release.get_last_release() +print("last_release:", last_release) +print("trunk:", last_release.get_next_release()) +print() + +releases: list[Release] = [ + Release.get_by_version(version) + for version in range(last_release.version - 6, last_release.version + 6 + 1) +] +for release in releases: + print(release, release.is_last_release()) + + +# for _ in range(15): +# release = releases[-1] +# releases.append(release.get_next_release()) + + +# TODO: В тесты +# release = releases[-1] +# releases_v2 = [release] +# for _ in range(15): +# release = release.get_prev_release() +# releases_v2.append(release) +# print(releases_v2 == releases) +# +# for r1, r2 in zip(releases, releases_v2[::-1]): +# print(r1 == r2) +# print(f"{r1}\n{r2}") +# print() + +# for r in releases: +# print(r) + + +# d = date.today().replace(day=1) +# print(d) +# print() +# +# for _ in range(20): +# d = change_month(d) +# print(d) + + +print("\n" + "-" * 100 + "\n") + + +def get_items( + start_date: date, + end_date: date, + delta: timedelta, +) -> list[tuple[date, date]]: + items = [] + + dt = end_date + while True: + if dt <= start_date: + break + + dt1 = dt + dt -= delta + + if dt < start_date: + dt = start_date + + items.append((dt, dt1)) + + return items + + +def to_ms(d: date) -> int: + utc_timestamp = datetime.combine( + d, datetime.min.time(), tzinfo=timezone.utc + ).timestamp() + return int(utc_timestamp * 1000) + + +d = datetime.now(timezone.utc).date() +print(d) +# 2025-01-10 + +for d1, d2 in get_items( + start_date=d - timedelta(weeks=6), + end_date=d + timedelta(days=1), + delta=timedelta(weeks=1), +): + print(f"{d1} - {d2}. {to_ms(d1)}+{to_ms(d2)}") +""" +2025-01-04 - 2025-01-11. 1735948800000+1736553600000 +2024-12-28 - 2025-01-04. 1735344000000+1735948800000 +2024-12-21 - 2024-12-28. 1734739200000+1735344000000 +2024-12-14 - 2024-12-21. 1734134400000+1734739200000 +2024-12-07 - 2024-12-14. 1733529600000+1734134400000 +2024-11-30 - 2024-12-07. 1732924800000+1733529600000 +2024-11-29 - 2024-11-30. 1732838400000+1732924800000 +""" diff --git a/_FOO_TEST_TEST/config.py b/_FOO_TEST_TEST/config.py new file mode 100644 index 000000000..07b3e6c3d --- /dev/null +++ b/_FOO_TEST_TEST/config.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + + +DIR = Path(__file__).resolve().parent + +PATH_DB = DIR / "db.sqlite" diff --git a/_FOO_TEST_TEST/db.py b/_FOO_TEST_TEST/db.py new file mode 100644 index 000000000..129c2fef2 --- /dev/null +++ b/_FOO_TEST_TEST/db.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime +from dataclasses import dataclass, fields +from typing import Any, Generator, Optional, Type + +from PyQt5.QtSql import QSqlDatabase, QSqlQuery + +from config import PATH_DB + + +def get_fields(class_or_instance) -> list[str]: + return [f.name for f in fields(class_or_instance)] + + +db = QSqlDatabase.addDatabase("QSQLITE") +db.setDatabaseName(str(PATH_DB)) +if not db.open(): + raise Exception(db.lastError().text()) + + +class BaseModel: + @classmethod + def get_table_name(cls) -> str: + return cls.__name__ + + @classmethod + def create_table(cls): + raise NotImplemented() + + @classmethod + def select(cls, where: dict[str, Any] = None) -> Generator[Type["BaseModel"], None, None]: + this_fields: list[str] = get_fields(cls) + + if where: + where_filter = "AND".join(f"{k} = :{k}" for k, v in where.items()) + where_str = f"WHERE {where_filter}" + else: + where_str = "" + + query = QSqlQuery() + query.prepare( + f""" + SELECT {",".join(this_fields)} + FROM {cls.get_table_name()} + {where_str} + """ + ) + if where: + for k, v in where.items(): + query.bindValue(f":{k}", v) + query.exec() + + while query.next(): + data: dict[str, Any] = { + name: query.value(name) + for name in this_fields + } + yield cls(**data) + + @classmethod + def select_one(cls, where: dict[str, Any] = None) -> Optional[Type["BaseModel"]]: + return next( + cls.select(where=where), + None, + ) + + @classmethod + def get_inherited_models(cls) -> list[Type["BaseModel"]]: + return sorted(cls.__subclasses__(), key=lambda x: x.__name__) + + +@dataclass +class Logged(BaseModel): + id: int + date: str + total_seconds: int + total_seconds_human: str + + @classmethod + def create_table(cls) -> None: + QSqlQuery( + f""" + CREATE TABLE IF NOT EXISTS {cls.get_table_name()}( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date VARCHAR(10) UNIQUE, + total_seconds INTEGER DEFAULT 0, + total_seconds_human TEXT + ) + """ + ).exec() + + @classmethod + def get_by_date(cls, date: str) -> Optional["Logged"]: + return cls.select_one(where=dict(date=date)) + + @classmethod + def add(cls, date: str) -> "Logged": + # Если уже есть + if obj := cls.get_by_date(date): + return obj + + query = QSqlQuery() + query.prepare(f"INSERT INTO {cls.get_table_name()} (date) VALUES (:date)") + query.bindValue(":date", date) + query.exec() + + return cls.get_by_date(date) + + @classmethod + def update( + cls, + id: int, + total_seconds: int, + total_seconds_human: str, + ) -> None: + query = QSqlQuery() + query.prepare( + f""" + UPDATE {cls.get_table_name()} + SET + total_seconds = :total_seconds, + total_seconds_human = :total_seconds_human + WHERE id = :id + """ + ) + query.bindValue(":id", id) + query.bindValue(":total_seconds", total_seconds) + query.bindValue(":total_seconds_human", total_seconds_human) + query.exec() + + +@dataclass +class LoggedItem(BaseModel): + uuid: str + logged_id: int + time: str + seconds: int + seconds_human: str + jira_id: str + jira_title: str + + @classmethod + def create_table(cls) -> None: + QSqlQuery( + f""" + CREATE TABLE IF NOT EXISTS {cls.get_table_name()}( + uuid TEXT PRIMARY KEY, + logged_id INTEGER, + time VARCHAR(8), + seconds INTEGER DEFAULT 0, + seconds_human TEXT, + jira_id TEXT, + jira_title TEXT, + + FOREIGN KEY (logged_id) REFERENCES Logged (id) ON DELETE CASCADE + ) + """ + ).exec() + + @classmethod + def get_by_uuid(cls, uuid: str) -> Optional["LoggedItem"]: + return cls.select_one(where=dict(uuid=uuid)) + + @classmethod + def add( + cls, + uuid: str, + logged_id: int, + time_str: str, + seconds: int, + seconds_human: str, + jira_id: str, + jira_title: str, + ) -> "LoggedItem": + # Если уже есть + if obj := cls.get_by_uuid(uuid): + return obj + + query = QSqlQuery() + query.prepare( + f""" + INSERT INTO {cls.get_table_name()} (uuid, logged_id, time, seconds, seconds_human, jira_id, jira_title) + VALUES (:uuid, :logged_id, :time, :seconds, :seconds_human, :jira_id, :jira_title) + """ + ) + query.bindValue(":uuid", uuid) + query.bindValue(":logged_id", logged_id) + query.bindValue(":time", time_str) + query.bindValue(":seconds", seconds) + query.bindValue(":seconds_human", seconds_human) + query.bindValue(":jira_id", jira_id) + query.bindValue(":jira_title", jira_title) + query.exec() + + return cls.get_by_uuid(uuid) + + +for model in BaseModel.get_inherited_models(): + model.create_table() + + +# date = "2024-09-23" +# print(Logged.get_by_date(date)) +# print(Logged.add(date)) +# print(Logged.add(date)) +# print(Logged.get_by_date(date)) +# print() +# +# for obj in Logged.select(): +# print(obj) + +items = [ + { + "uuid": "bf5540c1-2614-4521-898c-64fcd2222c1d", + "date_time": "24/08/2024 21:46:41", + "logged_human_time": "1 hour", + "logged_seconds": 3600, + "jira_id": "FOO-11202", + "jira_title": "Учет времени, не связанного с конкретной джирой" + }, + { + "uuid": "e8ea6140-daa2-46ca-981f-bee449fe4a34", + "date_time": "24/08/2024 20:56:56", + "logged_human_time": "4 hours", + "logged_seconds": 14400, + "jira_id": "FOO-10238", + "jira_title": "October 2024" + }, + { + "uuid": "64469ae4-325d-4fae-833d-7b3c61e4d8ce", + "date_time": "24/08/2024 20:28:38", + "logged_human_time": "1 hour", + "logged_seconds": 3600, + "jira_id": "FOO-10468", + "jira_title": "January 2025" + }, + { + "uuid": "1f5540c1-2614-4521-898c-64fcd2222c1d", + "date_time": "25/08/2024 11:23:11", + "logged_human_time": "1 hour", + "logged_seconds": 3600, + "jira_id": "FOO-11202", + "jira_title": "Учет времени, не связанного с конкретной джирой" + }, +] +from collections import defaultdict +date_by_items = defaultdict(list) +for item in items: + date_time = datetime.strptime(item["date_time"], "%d/%m/%Y %H:%M:%S") + date_str = date_time.date().isoformat() + date_by_items[date_str].append(item) + +for date_str, items in date_by_items.items(): + logged_id = Logged.add(date_str).id + + total_seconds: int = 0 + for item in items: + date_time = datetime.strptime(item["date_time"], "%d/%m/%Y %H:%M:%S") + time_str = date_time.time().isoformat() + + logged_seconds = item["logged_seconds"] + total_seconds += logged_seconds + + LoggedItem.add( + uuid=item["uuid"], + logged_id=logged_id, + time_str=time_str, + seconds=logged_seconds, + seconds_human=item["logged_human_time"], + jira_id=item["jira_id"], + jira_title=item["jira_title"], + ) + + # TODO: + from datetime import timedelta + total_seconds_human = str(timedelta(seconds=total_seconds)) + + Logged.update( + id=logged_id, + total_seconds=total_seconds, + total_seconds_human=total_seconds_human, + ) diff --git a/_FOO_TEST_TEST/parse_web_outlook_calendar/config.py b/_FOO_TEST_TEST/parse_web_outlook_calendar/config.py deleted file mode 100644 index 20e74f071..000000000 --- a/_FOO_TEST_TEST/parse_web_outlook_calendar/config.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -LOGIN = '' -PASSWORD = '' diff --git a/_FOO_TEST_TEST/parse_web_outlook_calendar/main.py b/_FOO_TEST_TEST/parse_web_outlook_calendar/main.py deleted file mode 100644 index 1c5e88c15..000000000 --- a/_FOO_TEST_TEST/parse_web_outlook_calendar/main.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import time - -# pip install selenium -from selenium import webdriver -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.common.by import By - -from config import LOGIN, PASSWORD - - -URL = 'https://mail.compassplus.com' - -driver = webdriver.Firefox() - -try: - driver.implicitly_wait(10) # seconds - driver.get(URL) - print('Title: "{}"'.format(driver.title)) - - driver.find_element_by_id('username').send_keys(LOGIN) - driver.find_element_by_id('password').send_keys(PASSWORD) - - # Делаем скриншот результата - driver.save_screenshot('before_auth.png') - - driver.find_element_by_class_name("signinbutton").click() - - driver.save_screenshot('after_auth.png') - print('Title: "{}"'.format(driver.title)) - - driver.save_screenshot('before_click_on_calendar.png') - print('Title: "{}"'.format(driver.title)) - - html = driver.page_source - print('Length:', len(html)) - open('driver.before_click_on_calendar.html', 'w', encoding='utf-8').write(html) - - # Ждем и кликаем на кнопку - driver.find_element_by_xpath('//*[text()="Календарь"]').click() - - html = driver.page_source - print('Length:', len(html)) - open('driver.after_click_on_calendar.html', 'w', encoding='utf-8').write(html) - - driver.save_screenshot('after_click_on_calendar.png') - print('Title: "{}"'.format(driver.title)) - - # Ждем пока появится элемент - driver.find_element_by_css_selector('[aria-label="Представление календаря"]') - - # Даем еще время на прогрузку календаря - time.sleep(10) - - html = driver.page_source - print('Length:', len(html)) - open('driver.after_click_on_calendar_2.html', 'w', encoding='utf-8').write(html) - - driver.save_screenshot('after_click_on_calendar_2.png') - print('Title: "{}"'.format(driver.title)) - - # TODO: нужно - -finally: - # TODO: - # driver.quit() - pass diff --git a/_FOO_TEST_TEST/print_processes_in_directory.py b/_FOO_TEST_TEST/print_processes_in_directory.py new file mode 100644 index 000000000..e081ba42d --- /dev/null +++ b/_FOO_TEST_TEST/print_processes_in_directory.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from psutil import process_iter, Process, Error + + +def is_found(p: Process, cwd: str | Path) -> bool: + if isinstance(cwd, str): + cwd = Path(cwd) + return cwd.exists() and Path(p.cwd()).is_relative_to(cwd) + + +def get_processes(cwd: str | Path = None) -> list[Process]: + items = [] + for p in process_iter(): + try: + if is_found(p, cwd): + items.append(p) + except Error: + pass + return items + + +# TODO: Вывести деревом список процессов +for p in get_processes(r"C:\DEV__TX\trunk"): + print(p, p.cwd(), p.name(), p.cmdline(), p.parent()) + +# TODO: Алгоритм преобразования список в дерево diff --git a/_FOO_TEST_TEST/tx_version_calendar.html b/_FOO_TEST_TEST/tx_version_calendar.html new file mode 100644 index 000000000..b932157f3 --- /dev/null +++ b/_FOO_TEST_TEST/tx_version_calendar.html @@ -0,0 +1,283 @@ + + + + TX Version Calendar + + + + + + +

TX Version Calendar

+ + + +
+ + + + + +
TaskMinor BugImportant BugEnd of Support
+
+ + + + \ No newline at end of file diff --git a/about clipboard changed/with_notification/main.py b/about clipboard changed/with_notification/main.py index 97cf1a3a9..1de9a1b8b 100644 --- a/about clipboard changed/with_notification/main.py +++ b/about clipboard changed/with_notification/main.py @@ -23,7 +23,7 @@ if data != last_data: # First changed without notification if last_data is not None: - title = 'Clipboard changed (len: {})'.format(len(data)) + title = f'Clipboard changed (len: {len(data)})' # Max text: 20 text = data[:100] diff --git a/about clipboard changed/with_notification/notifications.py b/about clipboard changed/with_notification/notifications.py index 983993f5c..09aa68cfa 100644 --- a/about clipboard changed/with_notification/notifications.py +++ b/about clipboard changed/with_notification/notifications.py @@ -19,7 +19,7 @@ class WindowsBalloonTip: @staticmethod - def balloon_tip(title, msg, duration=5, icon_path_name=None): + def balloon_tip(title, msg, duration=5, icon_path_name=None) -> None: message_map = { win32con.WM_DESTROY: WindowsBalloonTip.on_destroy, } @@ -62,7 +62,7 @@ def balloon_tip(title, msg, duration=5, icon_path_name=None): UnregisterClass(wc.lpszClassName, None) @staticmethod - def on_destroy(hwnd, msg, wparam, lparam): + def on_destroy(hwnd, msg, wparam, lparam) -> None: nid = (hwnd, 0) Shell_NotifyIcon(NIM_DELETE, nid) PostQuitMessage(0) # Terminate the app. diff --git a/about__user_agent/bots.py b/about__user_agent/bots.py index a996205ac..b89726929 100644 --- a/about__user_agent/bots.py +++ b/about__user_agent/bots.py @@ -70,24 +70,24 @@ ] RAMBLER = [ - 'StackRambler/2.0 (MSIE incompatible)', - 'StackRambler/2.0', + "StackRambler/2.0 (MSIE incompatible)", + "StackRambler/2.0", ] YAHOO = [ - 'Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)', - 'Mozilla/5.0 (compatible; Yahoo! Slurp/3.0; http://help.yahoo.com/help/us/ysearch/slurp)', + "Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)", + "Mozilla/5.0 (compatible; Yahoo! Slurp/3.0; http://help.yahoo.com/help/us/ysearch/slurp)", ] MSN = [ - 'msnbot/1.1 (+http://search.msn.com/msnbot.htm)', - 'msnbot-media/1.0 (+http://search.msn.com/msnbot.htm)', - 'msnbot-media/1.1 (+http://search.msn.com/msnbot.htm)', - 'msnbot-news (+http://search.msn.com/msnbot.htm)', + "msnbot/1.1 (+http://search.msn.com/msnbot.htm)", + "msnbot-media/1.0 (+http://search.msn.com/msnbot.htm)", + "msnbot-media/1.1 (+http://search.msn.com/msnbot.htm)", + "msnbot-news (+http://search.msn.com/msnbot.htm)", ] BING = [ - 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)', + "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", ] ALL = [] @@ -100,6 +100,6 @@ ALL.extend(BING) -if __name__ == '__main__': +if __name__ == "__main__": import random print(random.choice(ALL)) diff --git a/about__user_agent/fake_useragent__examples.py b/about__user_agent/fake_useragent__examples.py index 745b51d75..b54db910e 100644 --- a/about__user_agent/fake_useragent__examples.py +++ b/about__user_agent/fake_useragent__examples.py @@ -1,28 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# SOURCE: https://github.com/hellysmile/fake-useragent - +# SOURCE: https://github.com/fake-useragent/fake-useragent # pip install fake-useragent from fake_useragent import UserAgent -ua = UserAgent() - -print(ua.ie) -# Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/4.0; GTB7.4; InfoPath.3; SV1; .NET CLR 3.1.76908; WOW64; en-US) -print(ua.msie) -# Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) - -print(ua['Internet Explorer']) -# Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 4.0; InfoPath.3; MS-RTC LM 8; .NET4.0C; .NET4.0E) - -print(ua.opera) -# Opera/9.80 (Windows NT 6.1; WOW64; U; pt) Presto/2.10.229 Version/11.62 +ua = UserAgent() print(ua.chrome) # Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36 @@ -30,7 +18,7 @@ print(ua.google) # Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1866.237 Safari/537.36 -print(ua['google chrome']) +print(ua["google chrome"]) # Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36 print(ua.firefox) diff --git a/about__user_agent/get_random_win_UserAgent.py b/about__user_agent/get_random_win_UserAgent.py index 0ad815505..7126c8da8 100644 --- a/about__user_agent/get_random_win_UserAgent.py +++ b/about__user_agent/get_random_win_UserAgent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import random @@ -17,16 +17,16 @@ def get_chrome_version(cls) -> str: a = random.randint(40, 69) b = random.randint(2987, 3497) c = random.randint(80, 140) - return '{}.0.{}.{}'.format(a, b, c) + return f"{a}.0.{b}.{c}" @classmethod def get(cls) -> str: - a = 'Mozilla/5.0 (Windows NT {}; Win64; x64)'.format(cls.get_win_version()) - b = 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36'.format(cls.get_chrome_version()) - return '{} {}'.format(a, b) + a = f"Mozilla/5.0 (Windows NT {cls.get_win_version()}; Win64; x64)" + b = f"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{cls.get_chrome_version()} Safari/537.36" + return f"{a} {b}" -if __name__ == '__main__': +if __name__ == "__main__": print(UserAgent.get()) # Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 # (KHTML, like Gecko) Chrome/41.0.2993.140 Safari/537.36 diff --git a/about__user_agent/ua_parser__examples.py b/about__user_agent/ua_parser__examples.py index 0ff45ca6e..282b28c4c 100644 --- a/about__user_agent/ua_parser__examples.py +++ b/about__user_agent/ua_parser__examples.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/ua-parser/uap-python @@ -13,8 +13,10 @@ from ua_parser import user_agent_parser -ua_string = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' \ - '(KHTML, like Gecko) Chrome/61.0.3347.109 Safari/537.36' +ua_string = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/61.0.3347.109 Safari/537.36" +) pp = pprint.PrettyPrinter(indent=4) diff --git a/about__user_agent/user_agents_parse__examples.py b/about__user_agent/user_agents_parse__examples.py index 7060fe83d..e54f86282 100644 --- a/about__user_agent/user_agents_parse__examples.py +++ b/about__user_agent/user_agents_parse__examples.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/selwin/python-user-agents @@ -11,8 +11,10 @@ from user_agents import parse -ua_string = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' \ - '(KHTML, like Gecko) Chrome/61.0.3347.109 Safari/537.36' +ua_string = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/61.0.3347.109 Safari/537.36" +) user_agent = parse(ua_string) print(user_agent) diff --git a/ago.py b/ago.py new file mode 100644 index 000000000..42bdca4b3 --- /dev/null +++ b/ago.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import timedelta +from enum import IntEnum + + +class UnitSeconds(IntEnum): + SECOND = 1 + MINUTE = 60 + HOUR = 60 * MINUTE + DAY = 24 * HOUR + WEEK = 7 * DAY + MONTH = 4 * WEEK + YEAR = 12 * MONTH + + +class L10n: + def get_template(self) -> str: + return "{value} {unit} ago" + + def get_unit(self, value: int, unit: UnitSeconds) -> str: + unit = unit.name.lower() + if value != 1: + unit = f"{unit}s" + return unit + + def get_value(self, value: int, unit: UnitSeconds) -> str: + unit = self.get_unit(value, unit) + return self.get_template().format(value=value, unit=unit) + + +class L10nRu(L10n): + def get_template(self) -> str: + return "{value} {unit} назад" + + # SOURCE: https://ru.stackoverflow.com/a/1380460/201445 + @staticmethod + def declension(n: int, form_0: str, form_1: str, form_2: str) -> str: + units = n % 10 + tens = (n // 10) % 10 + if tens == 1: + return form_0 + if units in [0, 5, 6, 7, 8, 9]: + return form_0 + if units == 1: + return form_1 + if units in [2, 3, 4]: + return form_2 + return "" + + def get_unit(self, value: int, unit: UnitSeconds) -> str: + match unit: + case UnitSeconds.SECOND: + return self.declension(value, "секунд", "секунда", "секунды") + + case UnitSeconds.MINUTE: + return self.declension(value, "минут", "минута", "минуты") + + case UnitSeconds.HOUR: + return self.declension(value, "часов", "час", "часа") + + case UnitSeconds.DAY: + return self.declension(value, "дней", "день", "дня") + + case UnitSeconds.WEEK: + return self.declension(value, "недель", "неделя", "недели") + + case UnitSeconds.MONTH: + return self.declension(value, "месяцев", "месяц", "месяца") + + case UnitSeconds.YEAR: + return self.declension(value, "лет", "год", "года") + + case _: + raise NotImplemented() + + +def ago(seconds: timedelta, l10n: L10n = L10n()) -> str: + seconds = int(seconds.total_seconds()) + if seconds < 0: + seconds = -seconds + + for unit in sorted(UnitSeconds, reverse=True): + value, seconds = divmod(seconds, unit) + if value: + return l10n.get_value(value, unit) + + return l10n.get_value(seconds, UnitSeconds.SECOND) + + +if __name__ == "__main__": + from datetime import datetime + + dt = datetime(year=2024, month=12, day=12) + + items = [ + (timedelta(seconds=0), "0 seconds ago"), + (timedelta(seconds=1), "1 second ago"), + (timedelta(seconds=59), "59 seconds ago"), + (timedelta(seconds=60), "1 minute ago"), + (timedelta(seconds=90), "1 minute ago"), + (timedelta(minutes=1), "1 minute ago"), + (timedelta(minutes=5), "5 minutes ago"), + (timedelta(minutes=45), "45 minutes ago"), + (timedelta(hours=1, minutes=45), "1 hour ago"), + (timedelta(hours=4, minutes=50), "4 hours ago"), + (timedelta(hours=23, minutes=50), "23 hours ago"), + (timedelta(hours=40, minutes=50), "1 day ago"), + (timedelta(days=1), "1 day ago"), + (timedelta(hours=48), "2 days ago"), + (timedelta(days=2), "2 days ago"), + (timedelta(days=7), "1 week ago"), + (timedelta(days=8), "1 week ago"), + (timedelta(weeks=1), "1 week ago"), + (timedelta(weeks=2), "2 weeks ago"), + (timedelta(weeks=4), "1 month ago"), + (timedelta(weeks=8), "2 months ago"), + (timedelta(weeks=12 * 4), "1 year ago"), + (timedelta(weeks=5 * 12 * 4), "5 years ago"), + ] + for value, expected in items: + actual = ago(dt - (dt - value)) + print(f"{value!r} -> {actual!r}") + assert expected == actual, f"{expected!r} != {actual!r}" + + dt2 = datetime(year=2024, month=12, day=10) + assert ago(dt2 - dt) == "2 days ago" + + print("\n" + "-" * 10 + "\n") + + items = [ + (timedelta(seconds=0), "0 секунд назад"), + (timedelta(seconds=1), "1 секунда назад"), + (timedelta(seconds=59), "59 секунд назад"), + (timedelta(seconds=60), "1 минута назад"), + (timedelta(seconds=90), "1 минута назад"), + (timedelta(minutes=1), "1 минута назад"), + (timedelta(minutes=5), "5 минут назад"), + (timedelta(minutes=45), "45 минут назад"), + (timedelta(hours=1, minutes=45), "1 час назад"), + (timedelta(hours=4, minutes=50), "4 часа назад"), + (timedelta(hours=23, minutes=50), "23 часа назад"), + (timedelta(hours=40, minutes=50), "1 день назад"), + (timedelta(days=1), "1 день назад"), + (timedelta(hours=48), "2 дня назад"), + (timedelta(days=2), "2 дня назад"), + (timedelta(days=7), "1 неделя назад"), + (timedelta(days=8), "1 неделя назад"), + (timedelta(weeks=1), "1 неделя назад"), + (timedelta(weeks=2), "2 недели назад"), + (timedelta(weeks=4), "1 месяц назад"), + (timedelta(weeks=8), "2 месяца назад"), + (timedelta(weeks=12 * 4), "1 год назад"), + (timedelta(weeks=5 * 12 * 4), "5 лет назад"), + ] + for value, expected in items: + actual = ago(dt - (dt - value), l10n=L10nRu()) + print(f"{value!r} -> {actual!r}") + assert expected == actual, f"{expected!r} != {actual!r}" + + dt2 = datetime(year=2024, month=12, day=10) + assert ago(dt2 - dt, l10n=L10nRu()) == "2 дня назад" diff --git a/aiohttp__asyncio__examples/aiohttp_check_user_agent.py b/aiohttp__asyncio__examples/aiohttp_check_user_agent.py index f5df227b4..b905dc183 100644 --- a/aiohttp__asyncio__examples/aiohttp_check_user_agent.py +++ b/aiohttp__asyncio__examples/aiohttp_check_user_agent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import asyncio @@ -10,17 +10,17 @@ import aiohttp -async def main(): +async def main() -> None: async with aiohttp.ClientSession() as session: - async with session.get('https://httpbin.org/get') as rs: + async with session.get("https://httpbin.org/get") as rs: print("Status:", rs.status) - print("Content-type:", rs.headers['content-type']) + print("Content-type:", rs.headers["content-type"]) data = await rs.json() - print(data['headers']['User-Agent']) + print(data["headers"]["User-Agent"]) # Python/3.7 aiohttp/3.7.3 -if __name__ == '__main__': +if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) diff --git a/aiohttp__asyncio__examples/aiohttp_client.py b/aiohttp__asyncio__examples/aiohttp_client.py index 3fba5e51e..44ba6d470 100644 --- a/aiohttp__asyncio__examples/aiohttp_client.py +++ b/aiohttp__asyncio__examples/aiohttp_client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import asyncio @@ -10,13 +10,13 @@ import aiohttp -async def main(): - url = 'https://python.org' +async def main() -> None: + url = "https://python.org" async with aiohttp.ClientSession() as session: async with session.get(url) as rs: print("Status:", rs.status) - print("Content-type:", rs.headers['Content-Type']) + print("Content-type:", rs.headers["Content-Type"]) html = await rs.text() print("Body:", html[:15], "...") @@ -25,12 +25,12 @@ async def main(): async with aiohttp.request("GET", url) as rs: print("Status:", rs.status) - print("Content-type:", rs.headers['Content-Type']) + print("Content-type:", rs.headers["Content-Type"]) html = await rs.text() print("Body:", html[:15], "...") -if __name__ == '__main__': +if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) diff --git a/aiohttp__asyncio__examples/aiohttp_server.py b/aiohttp__asyncio__examples/aiohttp_server.py index 8db089c7e..5c0f6bf08 100644 --- a/aiohttp__asyncio__examples/aiohttp_server.py +++ b/aiohttp__asyncio__examples/aiohttp_server.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install aiohttp @@ -9,7 +9,7 @@ async def handle(request): - name = request.match_info.get('name', "Anonymous") + name = request.match_info.get("name", "Anonymous") text = "Hello, " + name return web.Response(text=text) @@ -20,7 +20,7 @@ async def wshandle(request): async for msg in ws: if msg.type == web.WSMsgType.text: - await ws.send_str("Hello, {}".format(msg.data)) + await ws.send_str(f"Hello, {msg.data}") elif msg.type == web.WSMsgType.binary: await ws.send_bytes(msg.data) elif msg.type == web.WSMsgType.close: @@ -31,11 +31,11 @@ async def wshandle(request): app = web.Application() app.add_routes([ - web.get('/', handle), - web.get('/echo', wshandle), - web.get('/{name}', handle) + web.get("/", handle), + web.get("/echo", wshandle), + web.get("/{name}", handle) ]) -if __name__ == '__main__': +if __name__ == "__main__": web.run_app(app) diff --git a/aiohttp__asyncio__examples/asyncio__hello_world.py b/aiohttp__asyncio__examples/asyncio__hello_world.py index 64edc9eb4..432a7777e 100644 --- a/aiohttp__asyncio__examples/asyncio__hello_world.py +++ b/aiohttp__asyncio__examples/asyncio__hello_world.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import asyncio -async def main(): - print('Hello ', end='') +async def main() -> None: + print("Hello ", end="") await asyncio.sleep(1) - print('World!') + print("World!") # Python 3.7+ diff --git a/aiohttp__asyncio__examples/concurrent_requests.py b/aiohttp__asyncio__examples/concurrent_requests.py index 2980ca86b..647a97859 100644 --- a/aiohttp__asyncio__examples/concurrent_requests.py +++ b/aiohttp__asyncio__examples/concurrent_requests.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import asyncio @@ -12,7 +12,7 @@ from ignore_aiohttp_ssl_error import ignore_aiohttp_ssl_error -async def fetch_page(url: str, idx: int): +async def fetch_page(url: str, idx: int) -> None: async with aiohttp.request("GET", url) as rs: if rs.status == 200: print(f"[{idx}] Data fetched successfully") @@ -21,8 +21,8 @@ async def fetch_page(url: str, idx: int): print(rs.content) -async def main(): - url = 'https://python.org' +async def main() -> None: + url = "https://python.org" urls = [url] * 100 tasks = [ @@ -32,7 +32,7 @@ async def main(): await asyncio.gather(*tasks) -if __name__ == '__main__': +if __name__ == "__main__": import time t = time.perf_counter() @@ -40,4 +40,4 @@ async def main(): ignore_aiohttp_ssl_error(loop) loop.run_until_complete(main()) - print(f'Elapsed {time.perf_counter() - t:.2f} secs') + print(f"Elapsed {time.perf_counter() - t:.2f} secs") diff --git a/aiohttp__asyncio__examples/hello_world.py b/aiohttp__asyncio__examples/hello_world.py index 2007933e4..0a395a322 100644 --- a/aiohttp__asyncio__examples/hello_world.py +++ b/aiohttp__asyncio__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/aio-libs/aiohttp @@ -17,12 +17,12 @@ async def fetch(session, url): return await response.text() -async def main(): +async def main() -> None: async with aiohttp.ClientSession() as session: - html = await fetch(session, 'http://python.org') + html = await fetch(session, "http://python.org") print(html) -if __name__ == '__main__': +if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) diff --git a/aiohttp__asyncio__examples/hello_world__parse__lxml_etree.py b/aiohttp__asyncio__examples/hello_world__parse__lxml_etree.py index 47eb2b786..508eefbc5 100644 --- a/aiohttp__asyncio__examples/hello_world__parse__lxml_etree.py +++ b/aiohttp__asyncio__examples/hello_world__parse__lxml_etree.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/aio-libs/aiohttp @@ -20,15 +20,15 @@ async def fetch(session, url): return await response.content.read() -async def main(): +async def main() -> None: async with aiohttp.ClientSession() as session: - xml_str = await fetch(session, 'https://sdvk-oboi.ru/sitemap.xml') + xml_str = await fetch(session, "https://sdvk-oboi.ru/sitemap.xml") root = etree.fromstring(xml_str) for url in root.xpath('//*[local-name()="loc"]/text()'): print(url) -if __name__ == '__main__': +if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) diff --git a/aiohttp__asyncio__examples/ignore_aiohttp_ssl_error.py b/aiohttp__asyncio__examples/ignore_aiohttp_ssl_error.py index d3b0fe7ce..f6beffd36 100644 --- a/aiohttp__asyncio__examples/ignore_aiohttp_ssl_error.py +++ b/aiohttp__asyncio__examples/ignore_aiohttp_ssl_error.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import asyncio @@ -19,7 +19,7 @@ SSL_PROTOCOLS = (*SSL_PROTOCOLS, uvloop.loop.SSLProtocol) -def ignore_aiohttp_ssl_error(loop): +def ignore_aiohttp_ssl_error(loop) -> None: """Ignore aiohttp #3535 / cpython #13548 issue with SSL data after close There is an issue in Python 3.7 up to 3.7.3 that over-reports a @@ -41,21 +41,21 @@ def ignore_aiohttp_ssl_error(loop): orig_handler = loop.get_exception_handler() - def ignore_ssl_error(loop, context): + def ignore_ssl_error(loop, context) -> None: if context.get("message") in { "SSL error in data received", "Fatal error on transport", }: # validate we have the right exception, transport and protocol - exception = context.get('exception') - protocol = context.get('protocol') + exception = context.get("exception") + protocol = context.get("protocol") if ( isinstance(exception, ssl.SSLError) - and exception.reason == 'KRB5_S_INIT' + and exception.reason == "KRB5_S_INIT" and isinstance(protocol, SSL_PROTOCOLS) ): if loop.get_debug(): - asyncio.log.logger.debug('Ignoring asyncio SSL KRB5_S_INIT error') + asyncio.log.logger.debug("Ignoring asyncio SSL KRB5_S_INIT error") return if orig_handler is not None: orig_handler(loop, context) diff --git a/alice_and_bob.py b/alice_and_bob.py index 0b832e99e..0a31b91d7 100644 --- a/alice_and_bob.py +++ b/alice_and_bob.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # «Если Алиса будет кидать монетки до тех пор, пока не получит решку, следующую за орлом, а Боб – до тех пор, # пока не получит две решки подряд, то Алисе в среднем потребуется четыре броска монеты, в то время как Бобу – шесть.» + from random import randrange diff --git a/alpha2_to_country/main.py b/alpha2_to_country/main.py index 3b37f9705..c8ca59b42 100644 --- a/alpha2_to_country/main.py +++ b/alpha2_to_country/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/f2c11477ae4410e5a9d47272b72cb69354c271b8/html_parsing/ru_wikipedia_org__wiki__ISO_3166_1.py @@ -14,26 +14,26 @@ import requests -FILE_NAME_COUNTRY = Path(__file__).resolve().parent / 'alpha2_to_country.json' +FILE_NAME_COUNTRY = Path(__file__).resolve().parent / "alpha2_to_country.json" ALPHA2_TO_COUNTRY = None -def init(): +def init() -> None: global ALPHA2_TO_COUNTRY if FILE_NAME_COUNTRY.exists(): ALPHA2_TO_COUNTRY = json.loads( - FILE_NAME_COUNTRY.read_text('utf-8') + FILE_NAME_COUNTRY.read_text("utf-8") ) return - rs = requests.get('https://ru.wikipedia.org/wiki/ISO_3166-1') - root = BeautifulSoup(rs.content, 'html.parser') + rs = requests.get("https://ru.wikipedia.org/wiki/ISO_3166-1") + root = BeautifulSoup(rs.content, "html.parser") ALPHA2_TO_COUNTRY = dict() - for tr in root.select_one('.wikitable').select('tr'): - td_list = tr.select('td') + for tr in root.select_one(".wikitable").select("tr"): + td_list = tr.select("td") if not td_list: continue @@ -45,9 +45,9 @@ def init(): json.dump( ALPHA2_TO_COUNTRY, - open(FILE_NAME_COUNTRY, 'w', encoding='utf-8'), + open(FILE_NAME_COUNTRY, "w", encoding="utf-8"), ensure_ascii=False, - indent=4 + indent=4, ) @@ -58,12 +58,12 @@ def get_country(alpha2: str) -> str: return ALPHA2_TO_COUNTRY[alpha2] -if __name__ == '__main__': - print(get_country('RU')) +if __name__ == "__main__": + print(get_country("RU")) # Россия - print(get_country('US')) + print(get_country("US")) # США - print(get_country('DE')) + print(get_country("DE")) # Германия diff --git a/analog_using_webkit.py b/analog_using_webkit.py index ceece96a3..0074f199d 100644 --- a/analog_using_webkit.py +++ b/analog_using_webkit.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Использование selenium для загрузки и получения страницы с javascript.""" @@ -11,15 +11,20 @@ from operator import itemgetter from selenium import webdriver + url = "http://www.oddsportal.com/soccer/england/premier-league/everton-arsenal-tnWxil2o#over-under;2" browser = webdriver.Firefox() browser.get(url) soup = BeautifulSoup(browser.page_source) -data_table = soup.find('div', {'id': 'odds-data-table'}) +data_table = soup.find("div", {"id": "odds-data-table"}) -for div in data_table.find_all_next('div', attrs={'class=': 'table-container'}): - row = div.find_all(['span', 'strong']) +for div in data_table.find_all_next("div", attrs={"class=": "table-container"}): + row = div.find_all(["span", "strong"]) if len(row): - print(','.join(cell.get_text(strip=True) for cell in itemgetter(0, 4, 3, 2, 1)(row))) + print( + ",".join( + cell.get_text(strip=True) for cell in itemgetter(0, 4, 3, 2, 1)(row) + ) + ) diff --git a/analysis_append_game_date__sqlite_db__from__gist_commits/main.py b/analysis_append_game_date__sqlite_db__from__gist_commits/main.py index fbbcf7474..ae999c6bb 100644 --- a/analysis_append_game_date__sqlite_db__from__gist_commits/main.py +++ b/analysis_append_game_date__sqlite_db__from__gist_commits/main.py @@ -1,35 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Parser from https://github.com/gil9red/played_games/blob/f23777a1368f9124450bedac036791068d8ca099/mini_played_games_parser.py#L7 -def parse_played_games(text: str, silence: bool=False) -> dict: +def parse_played_games(text: str, silence: bool = False) -> dict: """ Функция для парсинга списка игр. """ - FINISHED_GAME = 'FINISHED_GAME' - NOT_FINISHED_GAME = 'NOT_FINISHED_GAME' - FINISHED_WATCHED = 'FINISHED_WATCHED' - NOT_FINISHED_WATCHED = 'NOT_FINISHED_WATCHED' + FINISHED_GAME = "FINISHED_GAME" + NOT_FINISHED_GAME = "NOT_FINISHED_GAME" + FINISHED_WATCHED = "FINISHED_WATCHED" + NOT_FINISHED_WATCHED = "NOT_FINISHED_WATCHED" FLAG_BY_CATEGORY = { - ' ': FINISHED_GAME, - '- ': NOT_FINISHED_GAME, - ' -': NOT_FINISHED_GAME, - ' @': FINISHED_WATCHED, - '@ ': FINISHED_WATCHED, - '-@': NOT_FINISHED_WATCHED, - '@-': NOT_FINISHED_WATCHED, + " ": FINISHED_GAME, + "- ": NOT_FINISHED_GAME, + " -": NOT_FINISHED_GAME, + " @": FINISHED_WATCHED, + "@ ": FINISHED_WATCHED, + "-@": NOT_FINISHED_WATCHED, + "@-": NOT_FINISHED_WATCHED, } # Регулярка вытаскивает выражения вида: 1, 2, 3 или 1-3, или римские цифры: III, IV import re PARSE_GAME_NAME_PATTERN = re.compile( - r'(\d+(, *?\d+)+)|(\d+ *?- *?\d+)|([MDCLXVI]+(, ?[MDCLXVI]+)+)', - flags=re.IGNORECASE + r"(\d+(, *?\d+)+)|(\d+ *?- *?\d+)|([MDCLXVI]+(, ?[MDCLXVI]+)+)", + flags=re.IGNORECASE, ) def parse_game_name(game_name: str) -> list: @@ -54,14 +54,14 @@ def parse_game_name(game_name: str) -> list: index = game_name.index(seq_str) base_name = game_name[:index].strip() - seq_str = seq_str.replace(' ', '') + seq_str = seq_str.replace(" ", "") - if ',' in seq_str: + if "," in seq_str: # '1,2,3' -> ['1', '2', '3'] - seq = seq_str.split(',') + seq = seq_str.split(",") - elif '-' in seq_str: - seq = seq_str.split('-') + elif "-" in seq_str: + seq = seq_str.split("-") # ['1', '7'] -> [1, 7] seq = list(map(int, seq)) @@ -73,7 +73,7 @@ def parse_game_name(game_name: str) -> list: return [game_name] # Сразу проверяем номер игры в серии и если она первая, то не добавляем в названии ее номер - return [base_name if num == '1' else base_name + " " + num for num in seq] + return [base_name if num == "1" else base_name + " " + num for num in seq] platforms = dict() platform = None @@ -84,7 +84,7 @@ def parse_game_name(game_name: str) -> list: continue flag = line[:2] - if flag not in FLAG_BY_CATEGORY and line.endswith(':'): + if flag not in FLAG_BY_CATEGORY and line.endswith(":"): platform_name = line[:-1] platform = { @@ -103,7 +103,7 @@ def parse_game_name(game_name: str) -> list: category_name = FLAG_BY_CATEGORY.get(flag) if not category_name: if not silence: - print('Странный формат строки: "{}"'.format(line)) + print(f'Странный формат строки: "{line}"') continue category = platform[category_name] @@ -112,7 +112,7 @@ def parse_game_name(game_name: str) -> list: for game in parse_game_name(game_name): if game in category: if not silence: - print('Предотвращено добавление дубликата игры "{}"'.format(game)) + print(f'Предотвращено добавление дубликата игры "{game}"') continue category.append(game) @@ -121,7 +121,7 @@ def parse_game_name(game_name: str) -> list: # Get FULL database from: https://github.com/gil9red/SimplePyScripts/blob/2f50908d5c70fafa885db009bfe9570f8fc111e8/PyGithub_examples/gist_history_to_sqlite_db.py -DB_FILE_NAME = '../PyGithub_examples/gist_commits.sqlite' +DB_FILE_NAME = "../PyGithub_examples/gist_commits.sqlite" def create_connect(): @@ -129,19 +129,19 @@ def create_connect(): return sqlite3.connect(DB_FILE_NAME) -FINISHED_GAME = 'FINISHED_GAME' -FINISHED_WATCHED = 'FINISHED_WATCHED' -NOT_FINISHED_GAME = 'NOT_FINISHED_GAME' -NOT_FINISHED_WATCHED = 'NOT_FINISHED_WATCHED' +FINISHED_GAME = "FINISHED_GAME" +FINISHED_WATCHED = "FINISHED_WATCHED" +NOT_FINISHED_GAME = "NOT_FINISHED_GAME" +NOT_FINISHED_WATCHED = "NOT_FINISHED_WATCHED" CATEGORIES = [FINISHED_GAME, FINISHED_WATCHED, NOT_FINISHED_GAME, NOT_FINISHED_WATCHED] -if __name__ == '__main__': +if __name__ == "__main__": from collections import defaultdict append_game_date = defaultdict(dict) with create_connect() as connect: - sql = 'SELECT committed_at, content FROM GistFile ORDER BY committed_at' + sql = "SELECT committed_at, content FROM GistFile ORDER BY committed_at" for committed_at, content in connect.execute(sql): platforms = parse_played_games(content, silence=True) @@ -155,10 +155,10 @@ def create_connect(): append_game_date[platform][category][game] = committed_at # Check - print('Ведьмак:', append_game_date['PC']['FINISHED_GAME']['Ведьмак']) - print('Dragon Age II:', append_game_date['PC']['FINISHED_GAME']['Dragon Age II']) + print("Ведьмак:", append_game_date["PC"]["FINISHED_GAME"]["Ведьмак"]) + print("Dragon Age II:", append_game_date["PC"]["FINISHED_GAME"]["Dragon Age II"]) # Dump this - with open('dumn.json', mode='w', encoding='utf-8') as f: + with open("dumn.json", mode="w", encoding="utf-8") as f: import json json.dump(append_game_date, f, ensure_ascii=False, indent=4, sort_keys=True) diff --git a/anonymization_quotes.py b/anonymization_quotes.py index dcdf738d2..0b31d6e83 100644 --- a/anonymization_quotes.py +++ b/anonymization_quotes.py @@ -1,7 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import re +import string def anonymization_quotes(quote_text): @@ -13,21 +17,19 @@ def anonymization_quotes(quote_text): """ - import re - login_pattern = re.compile(r'^(.+?):') + login_pattern = re.compile(r"^(.+?):") # Словарь, в котором ключом является логин, а значением псевдоним - all_logins = {} + all_logins = dict() # Счетчик логинов count_logins = 0 # Сгенерируем список с псевдонимами. Список будет вида: ['aaa', 'bbb', ..., 'zzz', 'AAA', ... 'ZZZ'] - import string - login_aliases = [c*3 for c in string.ascii_letters] + login_aliases = [c * 3 for c in string.ascii_letters] # Разбиваем цитату по строчно - for line in quote_text.split('\n'): + for line in quote_text.split("\n"): # Ищем логин match = login_pattern.search(line) @@ -50,7 +52,7 @@ def anonymization_quotes(quote_text): return quote -if __name__ == '__main__': +if __name__ == "__main__": quote = """ Аня: Не хочу и комп занят Кирилл: вредный старший брат окупировал комп? diff --git a/api_clans.py b/api_clans.py index 2675cfa2e..54ee5fbb6 100644 --- a/api_clans.py +++ b/api_clans.py @@ -1,22 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import base64 +import json + import requests class Api: # TODO: this - API_URL = '/api_clans/1/index.php?request=' + API_URL = "/api_clans/1/index.php?request=" - def __init__(self, login: str, password: str): + def __init__(self, login: str, password: str) -> None: self.login = login self.password = password self.session = requests.Session() - self.session.headers['Authorization'] = self.make_authorization(login, password) + self.session.headers["Authorization"] = self.make_authorization(login, password) # # Or: # self.session = requests.Session() # self.auth = (login, password) @@ -25,33 +28,31 @@ def method(self, method: str, data: dict = None) -> requests.Response: url = self.API_URL + method # Debug - print('POST: url: {}, data: {}'.format(url, data)) + print(f"POST: url: {url}, data: {data}") rs = self.session.post(url, data) # Debug print(rs) - print('rs.text: "{}"'.format(rs.text)) - import json - print('pretty rs:', json.dumps(rs.json(), indent=4, ensure_ascii=False)) - print('\n') + print(f'rs.text: "{rs.text}"') + print("pretty rs:", json.dumps(rs.json(), indent=4, ensure_ascii=False)) + print("\n") return rs def get_user_data(self, people_id=None) -> requests.Response: - return self.method('get_user_data', {'people_id': people_id}) + return self.method("get_user_data", {"people_id": people_id}) # Append more api methods @staticmethod def make_authorization(login: str, password: str) -> str: - credentials = login + ':' + password + credentials = login + ":" + password # As base64 - import base64 credentials = base64.b64encode(credentials.encode()).decode() - return 'Basic ' + credentials + return "Basic " + credentials # # NOTE: Requests debug @@ -75,41 +76,41 @@ def make_authorization(login: str, password: str) -> str: # requests_log.propagate = True -if __name__ == '__main__': +if __name__ == "__main__": # TODO: this - LOGIN = '' - PASSWORD = '' + LOGIN = "" + PASSWORD = "" api = Api(LOGIN, PASSWORD) # Получение информации о текущем пользователе rs = api.get_user_data() # Or: - rs = api.method('get_user_data') + rs = api.method("get_user_data") # Получение информации о пользователе с id = 1 rs = api.get_user_data(people_id=1) # Or: - rs = api.method('get_user_data', data={'people_id': 1}) + rs = api.method("get_user_data", data={"people_id": 1}) - print('\n') + print("\n") # Создание пользователя data = { - 'name': 'Вася', - 'lastname': 'Пупкин', - 'secondname': 'secondname', - 'sex': 'man', - 'phone': '79957777555', - 'email': 'guvuwer@p33.org', - 'pass': '123', - 'is_live': '1', + "name": "Вася", + "lastname": "Пупкин", + "secondname": "secondname", + "sex": "man", + "phone": "79957777555", + "email": "guvuwer@p33.org", + "pass": "123", + "is_live": "1", } - rs = api.method('add_user', data) + rs = api.method("add_user", data) new_user_id = rs.json() # Регистрация (Проверка кода подтверждения e-mail) - rs = api.method('check_email_code', data={'people_id': new_user_id}) + rs = api.method("check_email_code", data={"people_id": new_user_id}) email_code = rs.json() # Получение информации о новом пользователе с id = new_user_id diff --git a/append_attrs_in_class_by_condition__MetaClass.py b/append_attrs_in_class_by_condition__MetaClass.py index d52cb24d0..7c0f7f56f 100644 --- a/append_attrs_in_class_by_condition__MetaClass.py +++ b/append_attrs_in_class_by_condition__MetaClass.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" class MyMeta(type): def __new__(cls, name, bases, attrs): print(f"{name}, initialize={attrs['initialize']}") - if attrs['initialize']: - attrs['SUPER_VALUE'] = 42 - attrs['run'] = lambda self=None: print('run!') + if attrs["initialize"]: + attrs["SUPER_VALUE"] = 42 + attrs["run"] = lambda self=None: print("run!") - attrs['reverse_initialize'] = not attrs['initialize'] + attrs["reverse_initialize"] = not attrs["initialize"] return super().__new__(cls, name, bases, attrs) @@ -25,8 +25,8 @@ class B(A): initialize = True -print(hasattr(A, 'SUPER_VALUE')) # False -print(hasattr(B, 'SUPER_VALUE')) # True +print(hasattr(A, "SUPER_VALUE")) # False +print(hasattr(B, "SUPER_VALUE")) # True print() print(A.initialize, A.reverse_initialize) # False True @@ -34,8 +34,8 @@ class B(A): print() print(B.SUPER_VALUE) # 42 -B.run() # run! +B.run() # run! b = B() print(b.SUPER_VALUE) # 42 -b.run() # run! +b.run() # run! diff --git a/ascii_magic__examples/input.png b/ascii_magic__examples/input.png new file mode 100644 index 000000000..4d5126505 Binary files /dev/null and b/ascii_magic__examples/input.png differ diff --git a/ascii_magic__examples/print_ascii_string.py b/ascii_magic__examples/print_ascii_string.py new file mode 100644 index 000000000..1da291a89 --- /dev/null +++ b/ascii_magic__examples/print_ascii_string.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/LeandroBarone/python-ascii_magic + + +# pip install ascii-magic +from ascii_magic import AsciiArt + + +file_name = "input.png" +my_art = AsciiArt.from_image(file_name) +print(my_art.to_ascii(columns=40, monochrome=True)) +""" + + ii + .]EE]. + ;JXEEXJ; + iqX4444Xqi + `tXg444444gXt` + +CFLdg4444gdLFC+ + xuv. >6gEEg6> .vnx + `au` :2442: `ua` + ^wXc+////[!^^![////+cXw^ + %SX4d44444GLiiLG44444d4XS% + `tEg44EEEEEXn^::^nXEEEEE44gEt` + =fPEEEEEEEEd* *dEEEEEEEEPf= + i6dhhhhhhhhh6a*%ii%*a6hhhhhhhhhd6i + ^=^^^^^^^^^^=////////=^^^^^^^^^^=^ +""" diff --git a/ascii_table.py b/ascii_table.py index 4c375c12f..9093a99f8 100644 --- a/ascii_table.py +++ b/ascii_table.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # source: http://stackoverflow.com/a/5910078/5909792 @@ -11,10 +11,7 @@ def ascii_table(rows): for i in range(len(rows[0])): lens.append( len( - max( - [str(x[i]) for x in rows] + [headers[i]], - key=lambda x: len(str(x)) - ) + max([str(x[i]) for x in rows] + [headers[i]], key=lambda x: len(str(x))) ) ) @@ -22,18 +19,356 @@ def ascii_table(rows): pattern = " | ".join(formats) hpattern = " | ".join(formats) - separator = "-+-".join(['-' * n for n in lens]) + separator = "-+-".join(["-" * n for n in lens]) text_lines = [hpattern % tuple(headers), separator] for line in rows[1:]: text_lines.append(pattern % tuple(t for t in line)) - return '\n'.join(text_lines) + return "\n".join(text_lines) -if __name__ == '__main__': - columns = ['id', 'url', 'name', 'short_name', 'birthday', 'job', 'department', 'photo', 'work_phone', 'mobile_phone', 'email'] - rows = [['#1', 'http://amiller.example.com', 'Andrew Miller', 'amiller', '11 December', 'Testing Engineer', 'CD, Product Support Service', 'amiller.jpg', '888888888888', '', 'amiller@example.com'], ['#2', 'http://ataylor.example.com', 'Anthony Taylor', 'ataylor', '17 July', 'Software Engineer', 'CD, Product Support Service', 'ataylor.jpg', '', '', 'ataylor@example.com'], ['#3', 'http://dmoore.example.com', 'Daniel Moore', 'dmoore', '2 March', 'Testing Engineer', 'CD, Product Support Service', 'dmoore.jpg', '', '', 'dmoore@example.com'], ['#4', 'http://dsmith.example.com', 'David Smith', 'dsmith', '5 January', 'Testing Engineer', 'Processing Services Division', 'dsmith.jpg', '', '', 'dsmith@example.com'], ['#5', 'http://awilson.example.com', 'Alexander Wilson', 'awilson', '11 April', 'Software Engineer', 'CD, Product Support Service', 'awilson.jpg', '', '', 'awilson@example.com'], ['#6', 'http://asmith.example.com', 'Alexander Smith', 'asmith', '3 November', 'Testing Engineer', 'CD, Product Support Service', 'asmith.jpg', '', '', 'asmith@example.com'], ['#7', 'http://jdavis.example.com', 'Jayden Davis', 'jdavis', '10 April', 'Shift Engineer', 'BSD, Presale Solution Bureau', 'jdavis.jpg', '', '', 'jdavis@example.com'], ['#8', 'http://jdavis.example.com', 'Jacob Davis', 'jdavis', '5 July', 'Shift Engineer', 'DD, Technical Translation Bureau', 'jdavis.jpg', '', '', 'jdavis@example.com'], ['#9', 'http://ejones.example.com', 'Ethan Jones', 'ejones', '14 September', 'Software Engineer', 'BSD, Presale Solution Bureau', 'ejones.jpg', '', '', 'ejones@example.com'], ['#10', 'http://abrown.example.com', 'Angel Brown', 'abrown', '15 April', 'Testing Engineer', 'Processing Services Division', 'abrown.jpg', '', '', 'abrown@example.com'], ['#11', 'http://dwilliams.example.com', 'David Williams', 'dwilliams', '23 November', 'Application Developer', 'DD, Technical Translation Bureau', 'dwilliams.jpg', '', '', 'dwilliams@example.com'], ['#12', 'http://ddavis.example.com', 'Daniel Davis', 'ddavis', '5 September', 'Shift Engineer', 'DD, Technical Translation Bureau', 'ddavis.jpg', '', '', 'ddavis@example.com'], ['#13', 'http://ntaylor.example.com', 'Nathan Taylor', 'ntaylor', '16 February', 'Shift Engineer', 'BSD, Presale Solution Bureau', 'ntaylor.jpg', '', '', 'ntaylor@example.com'], ['#14', 'http://dtaylor.example.com', 'Daniel Taylor', 'dtaylor', '10 March', 'Application Developer', 'DD, Technical Translation Bureau', 'dtaylor.jpg', '', '', 'dtaylor@example.com'], ['#15', 'http://jbrown.example.com', 'Jayden Brown', 'jbrown', '1 January', 'Software Engineer', 'DD, Technical Translation Bureau', 'jbrown.jpg', '', '', 'jbrown@example.com'], ['#16', 'http://asmith.example.com', 'Andrew Smith', 'asmith', '9 August', 'Software Engineer', 'CD, Product Support Service', 'asmith.jpg', '', '', 'asmith@example.com'], ['#17', 'http://jsmith.example.com', 'Jayden Smith', 'jsmith', '8 April', 'Testing Engineer', 'BSD, Presale Solution Bureau', 'jsmith.jpg', '', '', 'jsmith@example.com'], ['#18', 'http://asmith.example.com', 'Alexander Smith', 'asmith', '7 November', 'Shift Engineer', 'Processing Services Division', 'asmith.jpg', '', '', 'asmith@example.com'], ['#19', 'http://dbrown.example.com', 'David Brown', 'dbrown', '20 October', 'Shift Engineer', 'Processing Services Division', 'dbrown.jpg', '', '', 'dbrown@example.com'], ['#20', 'http://nwilliams.example.com', 'Nathan Williams', 'nwilliams', '9 March', 'Shift Engineer', 'CD, Product Support Service', 'nwilliams.jpg', '', '', 'nwilliams@example.com'], ['#21', 'http://dtaylor.example.com', 'Daniel Taylor', 'dtaylor', '5 April', 'Shift Engineer', 'CD, Product Support Service', 'dtaylor.jpg', '', '', 'dtaylor@example.com'], ['#22', 'http://nmoore.example.com', 'Nathan Moore', 'nmoore', '14 July', 'Testing Engineer', 'DD, Technical Translation Bureau', 'nmoore.jpg', '', '', 'nmoore@example.com'], ['#23', 'http://ewilson.example.com', 'Ethan Wilson', 'ewilson', '7 September', 'Software Engineer', 'CD, Product Support Service', 'ewilson.jpg', '', '', 'ewilson@example.com'], ['#24', 'http://awilson.example.com', 'Angel Wilson', 'awilson', '5 December', 'Application Developer', 'BSD, Presale Solution Bureau', 'awilson.jpg', '', '', 'awilson@example.com'], ['#25', 'http://abrown.example.com', 'Angel Brown', 'abrown', '21 October', 'Shift Engineer', 'BSD, Presale Solution Bureau', 'abrown.jpg', '', '', 'abrown@example.com']] +if __name__ == "__main__": + columns = [ + "id", + "url", + "name", + "short_name", + "birthday", + "job", + "department", + "photo", + "work_phone", + "mobile_phone", + "email", + ] + rows = [ + [ + "#1", + "http://amiller.example.com", + "Andrew Miller", + "amiller", + "11 December", + "Testing Engineer", + "CD, Product Support Service", + "amiller.jpg", + "888888888888", + "", + "amiller@example.com", + ], + [ + "#2", + "http://ataylor.example.com", + "Anthony Taylor", + "ataylor", + "17 July", + "Software Engineer", + "CD, Product Support Service", + "ataylor.jpg", + "", + "", + "ataylor@example.com", + ], + [ + "#3", + "http://dmoore.example.com", + "Daniel Moore", + "dmoore", + "2 March", + "Testing Engineer", + "CD, Product Support Service", + "dmoore.jpg", + "", + "", + "dmoore@example.com", + ], + [ + "#4", + "http://dsmith.example.com", + "David Smith", + "dsmith", + "5 January", + "Testing Engineer", + "Processing Services Division", + "dsmith.jpg", + "", + "", + "dsmith@example.com", + ], + [ + "#5", + "http://awilson.example.com", + "Alexander Wilson", + "awilson", + "11 April", + "Software Engineer", + "CD, Product Support Service", + "awilson.jpg", + "", + "", + "awilson@example.com", + ], + [ + "#6", + "http://asmith.example.com", + "Alexander Smith", + "asmith", + "3 November", + "Testing Engineer", + "CD, Product Support Service", + "asmith.jpg", + "", + "", + "asmith@example.com", + ], + [ + "#7", + "http://jdavis.example.com", + "Jayden Davis", + "jdavis", + "10 April", + "Shift Engineer", + "BSD, Presale Solution Bureau", + "jdavis.jpg", + "", + "", + "jdavis@example.com", + ], + [ + "#8", + "http://jdavis.example.com", + "Jacob Davis", + "jdavis", + "5 July", + "Shift Engineer", + "DD, Technical Translation Bureau", + "jdavis.jpg", + "", + "", + "jdavis@example.com", + ], + [ + "#9", + "http://ejones.example.com", + "Ethan Jones", + "ejones", + "14 September", + "Software Engineer", + "BSD, Presale Solution Bureau", + "ejones.jpg", + "", + "", + "ejones@example.com", + ], + [ + "#10", + "http://abrown.example.com", + "Angel Brown", + "abrown", + "15 April", + "Testing Engineer", + "Processing Services Division", + "abrown.jpg", + "", + "", + "abrown@example.com", + ], + [ + "#11", + "http://dwilliams.example.com", + "David Williams", + "dwilliams", + "23 November", + "Application Developer", + "DD, Technical Translation Bureau", + "dwilliams.jpg", + "", + "", + "dwilliams@example.com", + ], + [ + "#12", + "http://ddavis.example.com", + "Daniel Davis", + "ddavis", + "5 September", + "Shift Engineer", + "DD, Technical Translation Bureau", + "ddavis.jpg", + "", + "", + "ddavis@example.com", + ], + [ + "#13", + "http://ntaylor.example.com", + "Nathan Taylor", + "ntaylor", + "16 February", + "Shift Engineer", + "BSD, Presale Solution Bureau", + "ntaylor.jpg", + "", + "", + "ntaylor@example.com", + ], + [ + "#14", + "http://dtaylor.example.com", + "Daniel Taylor", + "dtaylor", + "10 March", + "Application Developer", + "DD, Technical Translation Bureau", + "dtaylor.jpg", + "", + "", + "dtaylor@example.com", + ], + [ + "#15", + "http://jbrown.example.com", + "Jayden Brown", + "jbrown", + "1 January", + "Software Engineer", + "DD, Technical Translation Bureau", + "jbrown.jpg", + "", + "", + "jbrown@example.com", + ], + [ + "#16", + "http://asmith.example.com", + "Andrew Smith", + "asmith", + "9 August", + "Software Engineer", + "CD, Product Support Service", + "asmith.jpg", + "", + "", + "asmith@example.com", + ], + [ + "#17", + "http://jsmith.example.com", + "Jayden Smith", + "jsmith", + "8 April", + "Testing Engineer", + "BSD, Presale Solution Bureau", + "jsmith.jpg", + "", + "", + "jsmith@example.com", + ], + [ + "#18", + "http://asmith.example.com", + "Alexander Smith", + "asmith", + "7 November", + "Shift Engineer", + "Processing Services Division", + "asmith.jpg", + "", + "", + "asmith@example.com", + ], + [ + "#19", + "http://dbrown.example.com", + "David Brown", + "dbrown", + "20 October", + "Shift Engineer", + "Processing Services Division", + "dbrown.jpg", + "", + "", + "dbrown@example.com", + ], + [ + "#20", + "http://nwilliams.example.com", + "Nathan Williams", + "nwilliams", + "9 March", + "Shift Engineer", + "CD, Product Support Service", + "nwilliams.jpg", + "", + "", + "nwilliams@example.com", + ], + [ + "#21", + "http://dtaylor.example.com", + "Daniel Taylor", + "dtaylor", + "5 April", + "Shift Engineer", + "CD, Product Support Service", + "dtaylor.jpg", + "", + "", + "dtaylor@example.com", + ], + [ + "#22", + "http://nmoore.example.com", + "Nathan Moore", + "nmoore", + "14 July", + "Testing Engineer", + "DD, Technical Translation Bureau", + "nmoore.jpg", + "", + "", + "nmoore@example.com", + ], + [ + "#23", + "http://ewilson.example.com", + "Ethan Wilson", + "ewilson", + "7 September", + "Software Engineer", + "CD, Product Support Service", + "ewilson.jpg", + "", + "", + "ewilson@example.com", + ], + [ + "#24", + "http://awilson.example.com", + "Angel Wilson", + "awilson", + "5 December", + "Application Developer", + "BSD, Presale Solution Bureau", + "awilson.jpg", + "", + "", + "awilson@example.com", + ], + [ + "#25", + "http://abrown.example.com", + "Angel Brown", + "abrown", + "21 October", + "Shift Engineer", + "BSD, Presale Solution Bureau", + "abrown.jpg", + "", + "", + "abrown@example.com", + ], + ] rows.insert(0, columns) print(ascii_table(rows)) diff --git a/ascii_table__simple_pretty__format.py b/ascii_table__simple_pretty__format.py index 38e9caf79..afe0340f7 100644 --- a/ascii_table__simple_pretty__format.py +++ b/ascii_table__simple_pretty__format.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def pretty_table(data, cell_sep=' | ', header_separator=True, align='>') -> str: +def pretty_table(data, cell_sep=" | ", header_separator=True, align=">") -> str: rows = len(data) cols = len(data[0]) @@ -13,8 +13,8 @@ def pretty_table(data, cell_sep=' | ', header_separator=True, align='>') -> str: columns = [str(data[row][col]) for row in range(rows)] col_width.append(len(max(columns, key=len))) - templates = ['{:' + align + '%d}' % width for width in col_width] - separator = "-+-".join('-' * n for n in col_width) + templates = ["{:" + align + "%d}" % width for width in col_width] + separator = "-+-".join("-" * n for n in col_width) lines = [] for i, row in enumerate(range(rows)): @@ -29,20 +29,20 @@ def pretty_table(data, cell_sep=' | ', header_separator=True, align='>') -> str: if i == 0 and header_separator: lines.append(separator) - return '\n'.join(lines) + return "\n".join(lines) -def print_pretty_table(data, cell_sep=' | ', header_separator=True, align='>'): +def print_pretty_table(data, cell_sep=" | ", header_separator=True, align=">") -> None: print(pretty_table(data, cell_sep, header_separator, align)) -if __name__ == '__main__': +if __name__ == "__main__": table_data = [ - ['FRUIT', 'PERSON', 'ANIMAL'], - ['apples', 'Alice', 'dogs'], - ['oranges', 'Bob', 'cats'], - ['cherries', 'Carol', 'moose'], - ['banana', 'David', 'goose'], + ["FRUIT", "PERSON", "ANIMAL"], + ["apples", "Alice", "dogs"], + ["oranges", "Bob", "cats"], + ["cherries", "Carol", "moose"], + ["banana", "David", "goose"], ] print_pretty_table(table_data, header_separator=False) @@ -64,4 +64,4 @@ def print_pretty_table(data, cell_sep=' | ', header_separator=True, align='>'): print() - print_pretty_table([['FRUIT', 'PERSON', 'ANIMAL']]) + print_pretty_table([["FRUIT", "PERSON", "ANIMAL"]]) diff --git a/ascii_table__simple_pretty__ljust.py b/ascii_table__simple_pretty__ljust.py index 25fe9a90e..b762934b4 100644 --- a/ascii_table__simple_pretty__ljust.py +++ b/ascii_table__simple_pretty__ljust.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def pretty_table(data, cell_sep=' | ', header_separator=True) -> str: +def pretty_table(data, cell_sep=" | ", header_separator=True) -> str: rows = len(data) cols = len(data[0]) @@ -13,7 +13,7 @@ def pretty_table(data, cell_sep=' | ', header_separator=True) -> str: columns = [str(data[row][col]) for row in range(rows)] col_width.append(len(max(columns, key=len))) - separator = "-+-".join('-' * n for n in col_width) + separator = "-+-".join("-" * n for n in col_width) lines = [] @@ -28,20 +28,20 @@ def pretty_table(data, cell_sep=' | ', header_separator=True) -> str: if i == 0 and header_separator: lines.append(separator) - return '\n'.join(lines) + return "\n".join(lines) -def print_pretty_table(data, cell_sep=' | ', header_separator=True): +def print_pretty_table(data, cell_sep=" | ", header_separator=True) -> None: print(pretty_table(data, cell_sep, header_separator)) -if __name__ == '__main__': +if __name__ == "__main__": table_data = [ - ['FRUIT', 'PERSON', 'ANIMAL'], - ['apples', 'Alice', 'dogs'], - ['oranges', 'Bob', 'cats'], - ['cherries', 'Carol', 'moose'], - ['banana', 'David', 'goose'], + ["FRUIT", "PERSON", "ANIMAL"], + ["apples", "Alice", "dogs"], + ["oranges", "Bob", "cats"], + ["cherries", "Carol", "moose"], + ["banana", "David", "goose"], ] print_pretty_table(table_data, header_separator=False) @@ -63,4 +63,4 @@ def print_pretty_table(data, cell_sep=' | ', header_separator=True): print() - print_pretty_table([['FRUIT', 'PERSON', 'ANIMAL']]) + print_pretty_table([["FRUIT", "PERSON", "ANIMAL"]]) diff --git a/ascii_table__simple_pretty__rjust.py b/ascii_table__simple_pretty__rjust.py index f12ab1fc3..8abdf420d 100644 --- a/ascii_table__simple_pretty__rjust.py +++ b/ascii_table__simple_pretty__rjust.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def pretty_table(data, cell_sep=' | ', header_separator=True) -> str: +def pretty_table(data, cell_sep=" | ", header_separator=True) -> str: rows = len(data) cols = len(data[0]) @@ -13,7 +13,7 @@ def pretty_table(data, cell_sep=' | ', header_separator=True) -> str: columns = [str(data[row][col]) for row in range(rows)] col_width.append(len(max(columns, key=len))) - separator = "-+-".join('-' * n for n in col_width) + separator = "-+-".join("-" * n for n in col_width) lines = [] @@ -28,20 +28,20 @@ def pretty_table(data, cell_sep=' | ', header_separator=True) -> str: if i == 0 and header_separator: lines.append(separator) - return '\n'.join(lines) + return "\n".join(lines) -def print_pretty_table(data, cell_sep=' | ', header_separator=True): +def print_pretty_table(data, cell_sep=" | ", header_separator=True) -> None: print(pretty_table(data, cell_sep, header_separator)) -if __name__ == '__main__': +if __name__ == "__main__": table_data = [ - ['FRUIT', 'PERSON', 'ANIMAL'], - ['apples', 'Alice', 'dogs'], - ['oranges', 'Bob', 'cats'], - ['cherries', 'Carol', 'moose'], - ['banana', 'David', 'goose'], + ["FRUIT", "PERSON", "ANIMAL"], + ["apples", "Alice", "dogs"], + ["oranges", "Bob", "cats"], + ["cherries", "Carol", "moose"], + ["banana", "David", "goose"], ] print_pretty_table(table_data, header_separator=False) @@ -63,4 +63,4 @@ def print_pretty_table(data, cell_sep=' | ', header_separator=True): print() - print_pretty_table([['FRUIT', 'PERSON', 'ANIMAL']]) + print_pretty_table([["FRUIT", "PERSON", "ANIMAL"]]) diff --git a/ascii_table__using_tabulate.py b/ascii_table__using_tabulate.py index d635231b9..c76faa7cc 100644 --- a/ascii_table__using_tabulate.py +++ b/ascii_table__using_tabulate.py @@ -1,24 +1,364 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -headers = ['id', 'url', 'name', 'short_name', 'birthday', 'job', 'department', 'photo', 'work_phone', 'mobile_phone', 'email'] -rows = [['#1', 'http://amiller.example.com', 'Andrew Miller', 'amiller', '11 December', 'Testing Engineer', 'CD, Product Support Service', 'amiller.jpg', '888888888888', '', 'amiller@example.com'], ['#2', 'http://ataylor.example.com', 'Anthony Taylor', 'ataylor', '17 July', 'Software Engineer', 'CD, Product Support Service', 'ataylor.jpg', '', '', 'ataylor@example.com'], ['#3', 'http://dmoore.example.com', 'Daniel Moore', 'dmoore', '2 March', 'Testing Engineer', 'CD, Product Support Service', 'dmoore.jpg', '', '', 'dmoore@example.com'], ['#4', 'http://dsmith.example.com', 'David Smith', 'dsmith', '5 January', 'Testing Engineer', 'Processing Services Division', 'dsmith.jpg', '', '', 'dsmith@example.com'], ['#5', 'http://awilson.example.com', 'Alexander Wilson', 'awilson', '11 April', 'Software Engineer', 'CD, Product Support Service', 'awilson.jpg', '', '', 'awilson@example.com'], ['#6', 'http://asmith.example.com', 'Alexander Smith', 'asmith', '3 November', 'Testing Engineer', 'CD, Product Support Service', 'asmith.jpg', '', '', 'asmith@example.com'], ['#7', 'http://jdavis.example.com', 'Jayden Davis', 'jdavis', '10 April', 'Shift Engineer', 'BSD, Presale Solution Bureau', 'jdavis.jpg', '', '', 'jdavis@example.com'], ['#8', 'http://jdavis.example.com', 'Jacob Davis', 'jdavis', '5 July', 'Shift Engineer', 'DD, Technical Translation Bureau', 'jdavis.jpg', '', '', 'jdavis@example.com'], ['#9', 'http://ejones.example.com', 'Ethan Jones', 'ejones', '14 September', 'Software Engineer', 'BSD, Presale Solution Bureau', 'ejones.jpg', '', '', 'ejones@example.com'], ['#10', 'http://abrown.example.com', 'Angel Brown', 'abrown', '15 April', 'Testing Engineer', 'Processing Services Division', 'abrown.jpg', '', '', 'abrown@example.com'], ['#11', 'http://dwilliams.example.com', 'David Williams', 'dwilliams', '23 November', 'Application Developer', 'DD, Technical Translation Bureau', 'dwilliams.jpg', '', '', 'dwilliams@example.com'], ['#12', 'http://ddavis.example.com', 'Daniel Davis', 'ddavis', '5 September', 'Shift Engineer', 'DD, Technical Translation Bureau', 'ddavis.jpg', '', '', 'ddavis@example.com'], ['#13', 'http://ntaylor.example.com', 'Nathan Taylor', 'ntaylor', '16 February', 'Shift Engineer', 'BSD, Presale Solution Bureau', 'ntaylor.jpg', '', '', 'ntaylor@example.com'], ['#14', 'http://dtaylor.example.com', 'Daniel Taylor', 'dtaylor', '10 March', 'Application Developer', 'DD, Technical Translation Bureau', 'dtaylor.jpg', '', '', 'dtaylor@example.com'], ['#15', 'http://jbrown.example.com', 'Jayden Brown', 'jbrown', '1 January', 'Software Engineer', 'DD, Technical Translation Bureau', 'jbrown.jpg', '', '', 'jbrown@example.com'], ['#16', 'http://asmith.example.com', 'Andrew Smith', 'asmith', '9 August', 'Software Engineer', 'CD, Product Support Service', 'asmith.jpg', '', '', 'asmith@example.com'], ['#17', 'http://jsmith.example.com', 'Jayden Smith', 'jsmith', '8 April', 'Testing Engineer', 'BSD, Presale Solution Bureau', 'jsmith.jpg', '', '', 'jsmith@example.com'], ['#18', 'http://asmith.example.com', 'Alexander Smith', 'asmith', '7 November', 'Shift Engineer', 'Processing Services Division', 'asmith.jpg', '', '', 'asmith@example.com'], ['#19', 'http://dbrown.example.com', 'David Brown', 'dbrown', '20 October', 'Shift Engineer', 'Processing Services Division', 'dbrown.jpg', '', '', 'dbrown@example.com'], ['#20', 'http://nwilliams.example.com', 'Nathan Williams', 'nwilliams', '9 March', 'Shift Engineer', 'CD, Product Support Service', 'nwilliams.jpg', '', '', 'nwilliams@example.com'], ['#21', 'http://dtaylor.example.com', 'Daniel Taylor', 'dtaylor', '5 April', 'Shift Engineer', 'CD, Product Support Service', 'dtaylor.jpg', '', '', 'dtaylor@example.com'], ['#22', 'http://nmoore.example.com', 'Nathan Moore', 'nmoore', '14 July', 'Testing Engineer', 'DD, Technical Translation Bureau', 'nmoore.jpg', '', '', 'nmoore@example.com'], ['#23', 'http://ewilson.example.com', 'Ethan Wilson', 'ewilson', '7 September', 'Software Engineer', 'CD, Product Support Service', 'ewilson.jpg', '', '', 'ewilson@example.com'], ['#24', 'http://awilson.example.com', 'Angel Wilson', 'awilson', '5 December', 'Application Developer', 'BSD, Presale Solution Bureau', 'awilson.jpg', '', '', 'awilson@example.com'], ['#25', 'http://abrown.example.com', 'Angel Brown', 'abrown', '21 October', 'Shift Engineer', 'BSD, Presale Solution Bureau', 'abrown.jpg', '', '', 'abrown@example.com']] - # pip install tabulate from tabulate import tabulate + + +headers = [ + "id", + "url", + "name", + "short_name", + "birthday", + "job", + "department", + "photo", + "work_phone", + "mobile_phone", + "email", +] +rows = [ + [ + "#1", + "http://amiller.example.com", + "Andrew Miller", + "amiller", + "11 December", + "Testing Engineer", + "CD, Product Support Service", + "amiller.jpg", + "888888888888", + "", + "amiller@example.com", + ], + [ + "#2", + "http://ataylor.example.com", + "Anthony Taylor", + "ataylor", + "17 July", + "Software Engineer", + "CD, Product Support Service", + "ataylor.jpg", + "", + "", + "ataylor@example.com", + ], + [ + "#3", + "http://dmoore.example.com", + "Daniel Moore", + "dmoore", + "2 March", + "Testing Engineer", + "CD, Product Support Service", + "dmoore.jpg", + "", + "", + "dmoore@example.com", + ], + [ + "#4", + "http://dsmith.example.com", + "David Smith", + "dsmith", + "5 January", + "Testing Engineer", + "Processing Services Division", + "dsmith.jpg", + "", + "", + "dsmith@example.com", + ], + [ + "#5", + "http://awilson.example.com", + "Alexander Wilson", + "awilson", + "11 April", + "Software Engineer", + "CD, Product Support Service", + "awilson.jpg", + "", + "", + "awilson@example.com", + ], + [ + "#6", + "http://asmith.example.com", + "Alexander Smith", + "asmith", + "3 November", + "Testing Engineer", + "CD, Product Support Service", + "asmith.jpg", + "", + "", + "asmith@example.com", + ], + [ + "#7", + "http://jdavis.example.com", + "Jayden Davis", + "jdavis", + "10 April", + "Shift Engineer", + "BSD, Presale Solution Bureau", + "jdavis.jpg", + "", + "", + "jdavis@example.com", + ], + [ + "#8", + "http://jdavis.example.com", + "Jacob Davis", + "jdavis", + "5 July", + "Shift Engineer", + "DD, Technical Translation Bureau", + "jdavis.jpg", + "", + "", + "jdavis@example.com", + ], + [ + "#9", + "http://ejones.example.com", + "Ethan Jones", + "ejones", + "14 September", + "Software Engineer", + "BSD, Presale Solution Bureau", + "ejones.jpg", + "", + "", + "ejones@example.com", + ], + [ + "#10", + "http://abrown.example.com", + "Angel Brown", + "abrown", + "15 April", + "Testing Engineer", + "Processing Services Division", + "abrown.jpg", + "", + "", + "abrown@example.com", + ], + [ + "#11", + "http://dwilliams.example.com", + "David Williams", + "dwilliams", + "23 November", + "Application Developer", + "DD, Technical Translation Bureau", + "dwilliams.jpg", + "", + "", + "dwilliams@example.com", + ], + [ + "#12", + "http://ddavis.example.com", + "Daniel Davis", + "ddavis", + "5 September", + "Shift Engineer", + "DD, Technical Translation Bureau", + "ddavis.jpg", + "", + "", + "ddavis@example.com", + ], + [ + "#13", + "http://ntaylor.example.com", + "Nathan Taylor", + "ntaylor", + "16 February", + "Shift Engineer", + "BSD, Presale Solution Bureau", + "ntaylor.jpg", + "", + "", + "ntaylor@example.com", + ], + [ + "#14", + "http://dtaylor.example.com", + "Daniel Taylor", + "dtaylor", + "10 March", + "Application Developer", + "DD, Technical Translation Bureau", + "dtaylor.jpg", + "", + "", + "dtaylor@example.com", + ], + [ + "#15", + "http://jbrown.example.com", + "Jayden Brown", + "jbrown", + "1 January", + "Software Engineer", + "DD, Technical Translation Bureau", + "jbrown.jpg", + "", + "", + "jbrown@example.com", + ], + [ + "#16", + "http://asmith.example.com", + "Andrew Smith", + "asmith", + "9 August", + "Software Engineer", + "CD, Product Support Service", + "asmith.jpg", + "", + "", + "asmith@example.com", + ], + [ + "#17", + "http://jsmith.example.com", + "Jayden Smith", + "jsmith", + "8 April", + "Testing Engineer", + "BSD, Presale Solution Bureau", + "jsmith.jpg", + "", + "", + "jsmith@example.com", + ], + [ + "#18", + "http://asmith.example.com", + "Alexander Smith", + "asmith", + "7 November", + "Shift Engineer", + "Processing Services Division", + "asmith.jpg", + "", + "", + "asmith@example.com", + ], + [ + "#19", + "http://dbrown.example.com", + "David Brown", + "dbrown", + "20 October", + "Shift Engineer", + "Processing Services Division", + "dbrown.jpg", + "", + "", + "dbrown@example.com", + ], + [ + "#20", + "http://nwilliams.example.com", + "Nathan Williams", + "nwilliams", + "9 March", + "Shift Engineer", + "CD, Product Support Service", + "nwilliams.jpg", + "", + "", + "nwilliams@example.com", + ], + [ + "#21", + "http://dtaylor.example.com", + "Daniel Taylor", + "dtaylor", + "5 April", + "Shift Engineer", + "CD, Product Support Service", + "dtaylor.jpg", + "", + "", + "dtaylor@example.com", + ], + [ + "#22", + "http://nmoore.example.com", + "Nathan Moore", + "nmoore", + "14 July", + "Testing Engineer", + "DD, Technical Translation Bureau", + "nmoore.jpg", + "", + "", + "nmoore@example.com", + ], + [ + "#23", + "http://ewilson.example.com", + "Ethan Wilson", + "ewilson", + "7 September", + "Software Engineer", + "CD, Product Support Service", + "ewilson.jpg", + "", + "", + "ewilson@example.com", + ], + [ + "#24", + "http://awilson.example.com", + "Angel Wilson", + "awilson", + "5 December", + "Application Developer", + "BSD, Presale Solution Bureau", + "awilson.jpg", + "", + "", + "awilson@example.com", + ], + [ + "#25", + "http://abrown.example.com", + "Angel Brown", + "abrown", + "21 October", + "Shift Engineer", + "BSD, Presale Solution Bureau", + "abrown.jpg", + "", + "", + "abrown@example.com", + ], +] + print(tabulate(rows, headers=headers, tablefmt="grid")) -print('\n') +print("\n") # Without header rows = [ - ['randint(x)', 'Return a random int below x'], - ['rand()', 'Return a random float between 0 and 1'], - ['int(x)', 'Convert x to an int.'], - ['float(x)', 'Convert x to a float.'], - ['str(x)', 'Convert x to a str (unicode in py2)'], + ["randint(x)", "Return a random int below x"], + ["rand()", "Return a random float between 0 and 1"], + ["int(x)", "Convert x to an int."], + ["float(x)", "Convert x to a float."], + ["str(x)", "Convert x to a str (unicode in py2)"], ] print(tabulate(rows, headers=[], tablefmt="grid")) diff --git a/autoclick_RDP_task_icon/main.py b/autoclick_RDP_task_icon/main.py index f05f6262b..d8caa22fe 100644 --- a/autoclick_RDP_task_icon/main.py +++ b/autoclick_RDP_task_icon/main.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT import logging import time import traceback import sys -from typing import Optional, Tuple + +from datetime import date # pip install pyautogui # pip install pillow @@ -26,27 +26,27 @@ logging.basicConfig( level=logging.INFO, - format='[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s', + format="[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s", stream=sys.stdout, ) def is_exists_rdp_process() -> bool: - return any(process.name() == 'mstsc.exe' for process in psutil.process_iter()) + return any(process.name() == "mstsc.exe" for process in psutil.process_iter()) -def get_pos_rdp_task_icon() -> Optional[Tuple[int, int]]: +def get_pos_rdp_task_icon() -> tuple[int, int] | None: try: return pyautogui.locateCenterOnScreen("RDP_task_icon.png", minSearchTime=5) except: pass -def run(): +def run() -> None: logging.info("") logging.info("Run") - if DT.date.today().weekday() in [5, 6]: + if date.today().weekday() in [5, 6]: logging.info("Today is a weekend, skip") return @@ -60,14 +60,14 @@ def run(): img = pyautogui.screenshot() for _ in range(5): # Свернуть все окна - pyautogui.hotkey('win', 'd') + pyautogui.hotkey("win", "d") time.sleep(2) if img != pyautogui.screenshot(): break pos = get_pos_rdp_task_icon() - logging.info(f'1. get_pos_rdp_task_icon: {pos}') + logging.info(f"1. get_pos_rdp_task_icon: {pos}") if not pos: # Нужно сдвинуть курсор туда, где точно не может находиться кнопка задачи RPD, # это поможет если курсор уже находится на кнопке задачи RDP @@ -76,7 +76,7 @@ def run(): time.sleep(2) pos = get_pos_rdp_task_icon() - logging.info(f'2. get_pos_rdp_task_icon: {pos}') + logging.info(f"2. get_pos_rdp_task_icon: {pos}") if pos: pyautogui.click(*pos) logging.info("Click") @@ -85,21 +85,18 @@ def run(): print(traceback.format_exc()) -if __name__ == '__main__': +if __name__ == "__main__": logging.info("Started...") # Каждый день в 08:30 - schedule \ - .every().day.at("08:30") \ - .do(run) + schedule.every().day.at("08:30").do(run) - logging.info('Jobs:') + logging.info("Jobs:") for job in schedule.jobs: - logging.info(f' {job!r}') + logging.info(f" {job!r}") - logging.info('') + logging.info("") while True: schedule.run_pending() time.sleep(1) - diff --git a/autoclick_in_game__pyautoit.py b/autoclick_in_game__pyautoit.py deleted file mode 100644 index d56d75b25..000000000 --- a/autoclick_in_game__pyautoit.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import threading -import time - -import keyboard - -# pip install PyAutoIt -import autoit - - -DATA = { - 'START': False, -} - - -def change_start(): - DATA['START'] = not DATA['START'] - print(DATA['START']) - - -def process_auto_click(): - while True: - if not DATA['START']: - time.sleep(0.100) - continue - - # Симуляция атаки - if DATA['START']: - autoit.mouse_click() - - time.sleep(0.070) - - -keyboard.add_hotkey('Ctrl+Alt+Space', change_start) - -# Запуск потока для автоатаки -thread_auto_click = threading.Thread(target=process_auto_click) -thread_auto_click.start() - - -while True: - if not DATA['START']: - time.sleep(0.100) - continue diff --git a/autoindent_code_JASS_war3map_j.py b/autoindent_code_JASS_war3map_j.py deleted file mode 100644 index fe7b77f49..000000000 --- a/autoindent_code_JASS_war3map_j.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import re - - -DEBUG = False - - -def merge_str_literal(text: str) -> str: - def _on_match(m: re.Match): - return m.group().replace('"+"', '') - - return re.sub(r'".+?"(\+".+?")+ ', _on_match, text) - - -lines = """ -function II1I1_II takes real II1I1__I returns nothing -local real II1I1_1I -local real st=TimerGetElapsed(II1I___I) -if st<=0 then -set II1I___I=CreateTimer() -call TimerStart(II1I___I,1000000,false,null) -endif -if(II1I1__I>0)then -loop -set II1I1_1I=II1I1__I-TimerGetElapsed(II1I___I)+st -exitwhen II1I1_1I<=0 -if(II1I1_1I>bj_POLLED_WAIT_SKIP_THRESHOLD)then -call TriggerSleepAction(0.1*II1I1_1I) -else -call TriggerSleepAction(bj_POLLED_WAIT_INTERVAL) -endif -endloop -endif -endfunction -""".strip().splitlines() - -stack = [] -items = [] -for line in lines: - if line.startswith('globals'): - stack.append('globals') - - elif line.startswith('endglobals'): - stack.pop(-1) - stack.append('endglobals') - - elif line.startswith('function'): - stack.append('function') - - elif line.startswith('endfunction'): - stack.pop(-1) - stack.append('endfunction') - - elif line.startswith('loop'): - stack.append('loop') - - elif line.startswith('endloop'): - stack.pop(-1) - stack.append('endloop') - - elif line.startswith('if'): - stack.append('if') - - elif line.startswith('elseif'): - stack.pop(-1) - stack.append('elseif') - - elif line.startswith('else'): - stack.pop(-1) - stack.append('else') - - elif line.startswith('endif'): - stack.pop(-1) - stack.append('endif') - else: - stack.append(line[:8] + '...') - - indent = len(stack) - 1 - line = merge_str_literal(line) - items.append(' ' * indent + line) - - DEBUG and print(f'{indent}. {line!r}', stack) - - # Add empty line after endglobals and endfunction - if line.startswith('endglobals') or line.startswith('endfunction'): - items.append('') - - if stack[-1] not in ['globals', 'function', 'loop', 'if', 'elseif', 'else']: - stack.pop(-1) - - -new_text = '\n'.join(items).strip() -print(new_text) -""" -function II1I1_II takes real II1I1__I returns nothing - local real II1I1_1I - local real st=TimerGetElapsed(II1I___I) - if st<=0 then - set II1I___I=CreateTimer() - call TimerStart(II1I___I,1000000,false,null) - endif - if(II1I1__I>0)then - loop - set II1I1_1I=II1I1__I-TimerGetElapsed(II1I___I)+st - exitwhen II1I1_1I<=0 - if(II1I1_1I>bj_POLLED_WAIT_SKIP_THRESHOLD)then - call TriggerSleepAction(0.1*II1I1_1I) - else - call TriggerSleepAction(bj_POLLED_WAIT_INTERVAL) - endif - endloop - endif -endfunction -""" diff --git a/bcrypt__hash_password__kdf_key_derivation_function/hash_password.py b/bcrypt__hash_password__kdf_key_derivation_function/hash_password.py index df15cc720..38aabead8 100644 --- a/bcrypt__hash_password__kdf_key_derivation_function/hash_password.py +++ b/bcrypt__hash_password__kdf_key_derivation_function/hash_password.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/pyca/bcrypt diff --git a/bcrypt__hash_password__kdf_key_derivation_function/kdf__key_derivation_function.py b/bcrypt__hash_password__kdf_key_derivation_function/kdf__key_derivation_function.py index 02122420b..053723207 100644 --- a/bcrypt__hash_password__kdf_key_derivation_function/kdf__key_derivation_function.py +++ b/bcrypt__hash_password__kdf_key_derivation_function/kdf__key_derivation_function.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/pyca/bcrypt @@ -18,9 +18,10 @@ key = bcrypt.kdf( - password=b'password', - salt=b'salt', + password=b"password", + salt=b"salt", desired_key_bytes=32, - rounds=100 + rounds=100, ) -print(key) # b'W\x1cq\xbd\xf25W\xf9\xe7\x99\x0fH\xfb\x1a-:n\xbd\x03\xd0\x1a>\x12\xa7\xf7\x0b\x85\x03\xc9\xf9\xbe8' +print(key) +# b'W\x1cq\xbd\xf25W\xf9\xe7\x99\x0fH\xfb\x1a-:n\xbd\x03\xd0\x1a>\x12\xa7\xf7\x0b\x85\x03\xc9\xf9\xbe8' diff --git a/bigfishgames_com__hidden_object/find__Sable_Maze__CE.py b/bigfishgames_com__hidden_object/find__Sable_Maze__CE.py deleted file mode 100644 index 73cdb41e9..000000000 --- a/bigfishgames_com__hidden_object/find__Sable_Maze__CE.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List -from get_all_games import get_all_games - - -def get_games() -> List[str]: - prefix = "Sable Maze".upper() - postfix = "Collector's Edition".upper() - - return get_all_games(prefix=prefix, postfix=postfix) - - -if __name__ == '__main__': - games = get_games() - print(f'Games ({len(games)}):') - - for game in games: - print(' ' + game) - diff --git a/bigfishgames_com__hidden_object/find__Saga_of_the_Nine_Worlds__CE.py b/bigfishgames_com__hidden_object/find__Saga_of_the_Nine_Worlds__CE.py deleted file mode 100644 index 82af46e02..000000000 --- a/bigfishgames_com__hidden_object/find__Saga_of_the_Nine_Worlds__CE.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List -from get_all_games import get_all_games - - -def get_games() -> List[str]: - prefix = "Saga of the Nine Worlds" - postfix = "Collector's Edition" - - return get_all_games(prefix=prefix, postfix=postfix) - - -if __name__ == '__main__': - games = get_games() - print(f'Games ({len(games)}):') - - for game in games: - print(' ' + game) diff --git a/bigfishgames_com__hidden_object/find__Spirits_of_Mystery__CE.py b/bigfishgames_com__hidden_object/find__Spirits_of_Mystery__CE.py deleted file mode 100644 index ac605e870..000000000 --- a/bigfishgames_com__hidden_object/find__Spirits_of_Mystery__CE.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List -from get_all_games import get_all_games - - -def get_games() -> List[str]: - prefix = "Spirits of Mystery" - postfix = "Collector's Edition" - - return get_all_games(prefix=prefix, postfix=postfix) - - -if __name__ == '__main__': - games = get_games() - print(f'Games ({len(games)}):') - - for game in games: - print(' ' + game) diff --git a/bigfishgames_com__hidden_object/get_all_games.py b/bigfishgames_com__hidden_object/get_all_games.py deleted file mode 100644 index d754ca9cb..000000000 --- a/bigfishgames_com__hidden_object/get_all_games.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List - -import requests -from bs4 import BeautifulSoup - - -DEFAULT_URL = 'https://www.bigfishgames.com/download-games/genres/15/hidden-object.html' - - -def get_all_games(url=DEFAULT_URL, prefix='', postfix='') -> List[str]: - def is_valid(game: str) -> bool: - game = game.upper() - return game.startswith(prefix.upper()) and game.endswith(postfix.upper()) - - rs = requests.get(url) - root = BeautifulSoup(rs.content, 'html.parser') - - return [a.text.strip() for a in root.select('#genre_bottom a') if is_valid(a.text.strip())] - - -if __name__ == '__main__': - games = get_all_games() - print(f'Games ({len(games)}): {games}') - - import json - json.dump(games, open('games.json', 'w', encoding='utf-8'), ensure_ascii=False) diff --git a/bin2str/bin2str.py b/bin2str/bin2str.py index 682b64887..93e85d504 100644 --- a/bin2str/bin2str.py +++ b/bin2str/bin2str.py @@ -1,43 +1,51 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def bin2str(bin_str: str) -> str: - bin_str = bin_str.replace(' ', '') - length = len(bin_str) +def bin2str(bin_str: str, encoding: str = "utf-8") -> str: + # Удаление всех символов кроме 0 и 1 + bin_str = "".join(c for c in bin_str if c in "01") - if length % 8 != 0: - raise Exception('Bad length') + h = hex(int(bin_str, 2))[2:] + return bytes.fromhex(h).decode(encoding) - text = '' - for i in range(length // 8): - end = i * 8 + 8 +def str2bin(text: str, sep=" ", encoding: str = "utf-8") -> str: + data: bytes = text.encode(encoding) - b = bin_str[i * 8: end] - c = chr(int(b, 2)) - text += c + # Example: 'You' -> '10110010110111101110101' + bin_str = f"{int(data.hex(), 16):08b}" - return text + # Example: '10110010110111101110101 -> '01011001 01101111 01110101' + bin_str = bin_str[::-1] + items = [ + bin_str[i: i + 8][::-1].zfill(8) + for i in range(0, len(bin_str), 8) + ] + items.reverse() + return sep.join(items) -def str2bin(text: str) -> str: - bin_words = [bin(ord(c))[2:].rjust(8, '0') for c in text] - return ' '.join(bin_words) +if __name__ == "__main__": + assert str2bin("You") == "01011001 01101111 01110101" + assert ( + str2bin(bin2str("01011001 01101111 01110101")) == "01011001 01101111 01110101" + ) + assert str2bin(bin2str("010110010110111101110101")) == "01011001 01101111 01110101" + + assert bin2str("01011001 01101111 01110101") == "You" + assert bin2str("010110010110111101110101") == "You" + assert bin2str("01011001-01101111-01110101") == "You" + assert bin2str("BIN: 01011001-01101111-01110101") == "You" -if __name__ == '__main__': - assert bin2str('01011001 01101111 01110101') == 'You' - assert bin2str('010110010110111101110101') == 'You' - assert str2bin(bin2str('01011001 01101111 01110101')) == '01011001 01101111 01110101' - assert str2bin(bin2str('010110010110111101110101')) == '01011001 01101111 01110101' + assert bin2str(str2bin("You")) == "You" - assert str2bin('You') == '01011001 01101111 01110101' - assert bin2str(str2bin('You')) == 'You' + assert bin2str(str2bin("Привет")) == "Привет" - print('Bin to text:') + print("Bin to text:") text = ( "01011001 01101111 01110101 00100000 01100001 01110010 01100101 00100000 01101101 01101111 " "01110010 01100101 00100000 01110100 01101000 01100001 01101110 00100000 01101010 01110101 " @@ -47,6 +55,6 @@ def str2bin(text: str) -> str: print(bin2str(text)) print() - print('Text to bin:') + print("Text to bin:") text = "You are more than just an animal." print(str2bin(text)) diff --git a/bin2str/gui.py b/bin2str/gui.py index f5fe7da6a..808c22891 100644 --- a/bin2str/gui.py +++ b/bin2str/gui.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import traceback +import sys + try: from PyQt5.QtWidgets import * from PyQt5.QtCore import * @@ -18,30 +21,31 @@ from PySide.QtCore import * -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +from bin2str import bin2str, str2bin + + +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('bin2str') + self.setWindowTitle("bin2str") - button_bin2str = QPushButton('bin -> str') + button_bin2str = QPushButton("bin -> str") button_bin2str.clicked.connect(self._bin2str) - button_str2bin = QPushButton('str -> bin') + button_str2bin = QPushButton("str -> bin") button_str2bin.clicked.connect(self._str2bin) self.plain_text_bin = QPlainTextEdit() @@ -53,37 +57,35 @@ def __init__(self): layout = QVBoxLayout() layout.addLayout(h_layout) - layout.addWidget(QLabel('Bin:')) + layout.addWidget(QLabel("Bin:")) layout.addWidget(self.plain_text_bin) layout.addSpacing(10) - layout.addWidget(QLabel('Text:')) + layout.addWidget(QLabel("Text:")) layout.addWidget(self.plain_text_str) self.setLayout(layout) - def _bin2str(self): + def _bin2str(self) -> None: text = self.plain_text_bin.toPlainText() - - from bin2str import bin2str text = bin2str(text) self.plain_text_str.setPlainText(text) - def _str2bin(self): + def _str2bin(self) -> None: text = self.plain_text_str.toPlainText() - - from bin2str import str2bin text = str2bin(text) self.plain_text_bin.setPlainText(text) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() - w.plain_text_bin.setPlainText('01001000 01100101 01101100 01101100 01101111 00100000 ' - '01010111 01101111 01110010 01101100 01100100 00100001') + w.plain_text_bin.setPlainText( + "01001000 01100101 01101100 01101100 01101111 00100000 " + "01010111 01101111 01110010 01101100 01100100 00100001" + ) w.show() app.exec() diff --git a/bitstruct__examples__parse_binary_files/parse_ts__print_packet_transport_stream_header/main.py b/bitstruct__examples__parse_binary_files/parse_ts__print_packet_transport_stream_header/main.py index b91d10b87..852e46af1 100644 --- a/bitstruct__examples__parse_binary_files/parse_ts__print_packet_transport_stream_header/main.py +++ b/bitstruct__examples__parse_binary_files/parse_ts__print_packet_transport_stream_header/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://en.wikipedia.org/wiki/MPEG_transport_stream#Packet @@ -17,7 +17,7 @@ # SOURCE: https://nypromo2019.hb.bizmrg.com/video/man/ny2019__000.ts # from m3u8 https://github.com/gil9red/SimplePyScripts/blob/ad33a95c3f0487555e9d8a0a0c5aef00613c9bbf/html_parsing/https_newyear_mail_ru/download__m3u8.py -with open('ny2019__000.ts', 'rb') as file: +with open("ny2019__000.ts", "rb") as file: while True: data = file.read(188) if not data: @@ -26,27 +26,31 @@ f = io.BytesIO(data) # 4-BYTE TRANSPORT STREAM HEADER - print('4-BYTE TRANSPORT STREAM HEADER') + print("4-BYTE TRANSPORT STREAM HEADER") sync_byte = f.read(1) - print('sync_byte:', sync_byte) + print("sync_byte:", sync_byte) data = f.read(2) - transport_error_indicator, \ - payload_unit_start_indicator, \ - transport_priority, \ - pid = bitstruct.unpack('>b1b1b1u13', data) - print('transport_error_indicator:', transport_error_indicator) - print('payload_unit_start_indicator:', payload_unit_start_indicator) - print('transport_priority:', transport_priority) - print('pid:', pid) + ( + transport_error_indicator, + payload_unit_start_indicator, + transport_priority, + pid, + ) = bitstruct.unpack(">b1b1b1u13", data) + print("transport_error_indicator:", transport_error_indicator) + print("payload_unit_start_indicator:", payload_unit_start_indicator) + print("transport_priority:", transport_priority) + print("pid:", pid) data = f.read(1) - transport_scrambling_control, \ - adaptation_field_control, \ - continuity_counter = bitstruct.unpack('>u2u2u4', data) - print('transport_scrambling_control:', transport_scrambling_control) - print('adaptation_field_control:', adaptation_field_control) - print('continuity_counter:', continuity_counter) + ( + transport_scrambling_control, + adaptation_field_control, + continuity_counter, + ) = bitstruct.unpack(">u2u2u4", data) + print("transport_scrambling_control:", transport_scrambling_control) + print("adaptation_field_control:", adaptation_field_control) + print("continuity_counter:", continuity_counter) print() # 184-BYTE + diff --git a/bleach__examples__remove_tags__clear_html/class_Cleaner.py b/bleach__examples__remove_tags__clear_html/class_Cleaner.py index d67d44b54..5291f1e2c 100644 --- a/bleach__examples__remove_tags__clear_html/class_Cleaner.py +++ b/bleach__examples__remove_tags__clear_html/class_Cleaner.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mozilla/bleach @@ -12,10 +12,10 @@ from bleach.sanitizer import Cleaner -html = ''' +html = """

is not allowed

Hello World!

-'''.strip() +""".strip() cleaner = Cleaner() @@ -26,7 +26,7 @@ print() -cleaner = Cleaner(tags=['b', 'span'], strip=True) +cleaner = Cleaner(tags=["b", "span"], strip=True) for text in html.splitlines(): print(cleaner.clean(text)) # is not allowed diff --git a/bleach__examples__remove_tags__clear_html/class_LinkifyFilter.py b/bleach__examples__remove_tags__clear_html/class_LinkifyFilter.py index 2f7a09cad..c867ed9b3 100644 --- a/bleach__examples__remove_tags__clear_html/class_LinkifyFilter.py +++ b/bleach__examples__remove_tags__clear_html/class_LinkifyFilter.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mozilla/bleach @@ -15,24 +15,26 @@ from bleach.linkifier import LinkifyFilter -html = '
http://example.com
' +html = "
http://example.com
" -cleaner = Cleaner(tags=['pre']) +cleaner = Cleaner(tags=["pre"]) print(cleaner.clean(html)) #
http://example.com
-cleaner = Cleaner(tags=['pre'], filters=[LinkifyFilter]) +cleaner = Cleaner(tags=["pre"], filters=[LinkifyFilter]) print(cleaner.clean(html)) #
http://example.com
-print('\n' + '-' * 100 + '\n') +print("\n" + "-" * 100 + "\n") # skip_tags (list) – list of tags that you don’t want to linkify # the contents of; for example, you could set this to ['pre'] # to skip linkifying contents of pre tags cleaner = Cleaner( - tags=['pre'], - filters=[partial(LinkifyFilter, skip_tags=['pre'])] + tags=["pre"], + filters=[ + partial(LinkifyFilter, skip_tags=["pre"]), + ] ) print(cleaner.clean(html)) #
http://example.com
diff --git a/bleach__examples__remove_tags__clear_html/custom_filter.py b/bleach__examples__remove_tags__clear_html/custom_filter.py index 7f8df2b91..072c5501f 100644 --- a/bleach__examples__remove_tags__clear_html/custom_filter.py +++ b/bleach__examples__remove_tags__clear_html/custom_filter.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mozilla/bleach @@ -16,15 +16,15 @@ class MooFilter(Filter): def __iter__(self): for token in super().__iter__(): - if token['type'] in ['StartTag', 'EmptyTag'] and token['data']: - for attr, value in token['data'].items(): - token['data'][attr] = 'moo' + if token["type"] in ["StartTag", "EmptyTag"] and token["data"]: + for attr, value in token["data"].items(): + token["data"][attr] = "moo" yield token -TAGS = ['img'] +TAGS = ["img"] ATTRS = { - 'img': ['rel', 'src'] + "img": ["rel", "src"], } html = 'this is cute! ' diff --git a/bleach__examples__remove_tags__clear_html/escape_attributes.py b/bleach__examples__remove_tags__clear_html/escape_attributes.py index 1ee454c7b..a2ac829e0 100644 --- a/bleach__examples__remove_tags__clear_html/escape_attributes.py +++ b/bleach__examples__remove_tags__clear_html/escape_attributes.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mozilla/bleach @@ -15,67 +15,71 @@ # Map of allowed attributes by tag: -print('Map of allowed attributes by tag:', bleach.sanitizer.ALLOWED_ATTRIBUTES) +print("Map of allowed attributes by tag:", bleach.sanitizer.ALLOWED_ATTRIBUTES) # {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']} # As a list print( bleach.clean( '

blah blah blah

', - tags=['p'], - attributes=['style'], - styles=['color'], + tags=["p"], + attributes=["style"], + styles=["color"], ) ) #

blah blah blah

# As a dict attrs = { - '*': ['class'], # Any tag with class - 'a': ['href', 'rel'], - 'img': ['alt'], + "*": ["class"], # Any tag with class + "a": ["href", "rel"], + "img": ["alt"], } print( bleach.clean( 'an example', - tags=['img'], - attributes=attrs + tags=["img"], + attributes=attrs, ) ) # an example print() + # # Using functions # def allow_h(tag, name, value): - return name[0] == 'h' + return name[0] == "h" + print( bleach.clean( 'link', - tags=['a'], + tags=["a"], attributes=allow_h, ) ) # link + def allow_src(tag, name, value): - if name in ('alt', 'height', 'width'): + if name in ("alt", "height", "width"): return True - if name == 'src': + if name == "src": p = urlparse(value) - return not p.netloc or p.netloc == 'mydomain.com' + return not p.netloc or p.netloc == "mydomain.com" return False + print( bleach.clean( 'an example', - tags=['img'], + tags=["img"], attributes={ - 'img': allow_src - } + "img": allow_src + }, ) ) # an example diff --git a/bleach__examples__remove_tags__clear_html/escape_protocols.py b/bleach__examples__remove_tags__clear_html/escape_protocols.py index 7f60d515f..5093945d5 100644 --- a/bleach__examples__remove_tags__clear_html/escape_protocols.py +++ b/bleach__examples__remove_tags__clear_html/escape_protocols.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mozilla/bleach @@ -13,7 +13,7 @@ # List of allowed protocols -print('List of allowed protocols:', bleach.sanitizer.ALLOWED_PROTOCOLS) +print("List of allowed protocols:", bleach.sanitizer.ALLOWED_PROTOCOLS) # ['http', 'https', 'mailto'] print( @@ -26,7 +26,7 @@ print( bleach.clean( 'allowed protocol', - protocols=['http', 'https', 'smb'] + protocols=["http", "https", "smb"], ) ) # allowed protocol @@ -34,7 +34,7 @@ print( bleach.clean( 'allowed protocol', - protocols=bleach.ALLOWED_PROTOCOLS + ['smb'] + protocols=bleach.ALLOWED_PROTOCOLS + ["smb"], ) ) # allowed protocol diff --git a/bleach__examples__remove_tags__clear_html/escape_styles.py b/bleach__examples__remove_tags__clear_html/escape_styles.py index 3174df761..1a937a452 100644 --- a/bleach__examples__remove_tags__clear_html/escape_styles.py +++ b/bleach__examples__remove_tags__clear_html/escape_styles.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mozilla/bleach @@ -13,15 +13,15 @@ # List of allowed styles -print('List of allowed styles:', bleach.sanitizer.ALLOWED_STYLES) +print("List of allowed styles:", bleach.sanitizer.ALLOWED_STYLES) # [] -tags = ['p', 'em', 'strong'] +tags = ["p", "em", "strong"] attrs = { # Any tag with style - '*': ['style'] + "*": ["style"] } -styles = ['color', 'font-weight'] +styles = ["color", "font-weight"] html = '

my html

' print( @@ -29,7 +29,7 @@ html, tags=tags, attributes=attrs, - styles=styles + styles=styles, ) ) #

my html

diff --git a/bleach__examples__remove_tags__clear_html/escape_tags.py b/bleach__examples__remove_tags__clear_html/escape_tags.py index dc287179f..8857dbf43 100644 --- a/bleach__examples__remove_tags__clear_html/escape_tags.py +++ b/bleach__examples__remove_tags__clear_html/escape_tags.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mozilla/bleach @@ -13,17 +13,17 @@ # List of allowed tags -print('List of allowed tags:', bleach.sanitizer.ALLOWED_TAGS) +print("List of allowed tags:", bleach.sanitizer.ALLOWED_TAGS) # ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'strong', 'ul'] -print(bleach.clean('an example')) +print(bleach.clean("an example")) # an <script>evil()</script> example # Allowed tags print( bleach.clean( - 'an example', - tags=['b'], + "an example", + tags=["b"], ) ) # <i>an example</i> diff --git a/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_altering_attributes.py b/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_altering_attributes.py index f95ba4921..f0cb80729 100644 --- a/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_altering_attributes.py +++ b/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_altering_attributes.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mozilla/bleach @@ -16,39 +16,47 @@ # Altering Attributes + def shorten_url(attrs, new=False): """Shorten overly-long URLs in the text.""" # Only adjust newly-created links if not new: return attrs # _text will be the same as the URL for new links - text = attrs['_text'] + text = attrs["_text"] if len(text) > 25: - attrs['_text'] = text[:22] + '...' + attrs["_text"] = text[:22] + "..." return attrs + linker = Linker(callbacks=[shorten_url]) -print(linker.linkify('http://example.com/longlonglonglonglongurl')) +print(linker.linkify("http://example.com/longlonglonglonglongurl")) # http://example.com/lon... -print(linker.linkify('abc http://example.com/longlonglonglonglongurl def')) +print( + linker.linkify( + 'abc http://example.com/longlonglonglonglongurl def' + ) +) # abc http://example.com/longlonglonglonglongurl def print() + def outgoing_bouncer(attrs, new=False): """Send outgoing links through a bouncer.""" - href_key = (None, 'href') + href_key = (None, "href") p = urlparse(attrs.get(href_key)) - if p.netloc not in ['example.com', 'www.example.com', '']: + if p.netloc not in ["example.com", "www.example.com", ""]: url = attrs[href_key] - bouncer = 'http://bn.ce/?destination=%s' + bouncer = "http://bn.ce/?destination=%s" attrs[href_key] = bouncer % quote(url) return attrs + linker = Linker(callbacks=[outgoing_bouncer]) -print(linker.linkify('http://example.com')) +print(linker.linkify("http://example.com")) # http://example.com -print(linker.linkify('http://foo.com')) +print(linker.linkify("http://foo.com")) # http://foo.com diff --git a/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_preventing_links.py b/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_preventing_links.py index 6b4b8ca4d..8e70d8b18 100644 --- a/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_preventing_links.py +++ b/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_preventing_links.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mozilla/bleach @@ -14,14 +14,15 @@ # Preventing Links + def dont_linkify_python(attrs, new=False): # This is an existing link, so leave it be if not new: return attrs # If the TLD is '.py', make sure it starts with http: or https:. # Use _text because that's the original text - link_text = attrs['_text'] - if link_text.endswith('.py') and not link_text.startswith(('http:', 'https:')): + link_text = attrs["_text"] + if link_text.endswith(".py") and not link_text.startswith(("http:", "https:")): # This looks like a Python file, not a URL. Don't make a link. return None # Everything checks out, keep going to the next callback. @@ -29,62 +30,62 @@ def dont_linkify_python(attrs, new=False): linker = Linker(callbacks=[dont_linkify_python]) -print(linker.linkify('abc http://example.com def')) +print(linker.linkify("abc http://example.com def")) # abc http://example.com def -print(linker.linkify('abc models.py def')) +print(linker.linkify("abc models.py def")) # abc models.py def -print('\n' + '-' * 100 + '\n') +print("\n" + "-" * 100 + "\n") -linker = Linker(skip_tags=['pre']) -print(linker.linkify('a b c http://example.com d e f')) +linker = Linker(skip_tags=["pre"]) +print(linker.linkify("a b c http://example.com d e f")) # a b c http://example.com d e f -print(linker.linkify('
http://example.com
')) +print(linker.linkify("
http://example.com
")) #
http://example.com
-print('\n' + '-' * 100 + '\n') +print("\n" + "-" * 100 + "\n") -only_fish_tld_url_re = build_url_re(tlds=['fish']) +only_fish_tld_url_re = build_url_re(tlds=["fish"]) linker = Linker(url_re=only_fish_tld_url_re) -print(linker.linkify('com TLD does not link https://example.com')) +print(linker.linkify("com TLD does not link https://example.com")) # com TLD does not link https://example.com -print(linker.linkify('fish TLD links https://example.fish')) +print(linker.linkify("fish TLD links https://example.fish")) # fish TLD links https://example.fish -print('\n' + '-' * 100 + '\n') +print("\n" + "-" * 100 + "\n") -only_https_url_re = build_url_re(protocols=['https']) +only_https_url_re = build_url_re(protocols=["https"]) linker = Linker(url_re=only_https_url_re) -print(linker.linkify('gopher does not link gopher://example.link')) +print(linker.linkify("gopher does not link gopher://example.link")) # gopher does not link gopher://example.link -print(linker.linkify('https links https://example.com/')) +print(linker.linkify("https links https://example.com/")) # https links https://example.com/ -print('\n' + '-' * 100 + '\n') +print("\n" + "-" * 100 + "\n") -html = 'https://xn--80aaksdi3bpu.xn--p1ai/ https://дайтрафик.рф/' +html = "https://xn--80aaksdi3bpu.xn--p1ai/ https://дайтрафик.рф/" -linker = Linker(url_re=build_url_re(tlds=['рф'])) +linker = Linker(url_re=build_url_re(tlds=["рф"])) print(linker.linkify(html)) # https://xn--80aaksdi3bpu.xn--p1ai/ https://дайтрафик.рф/ -puny_linker = Linker(url_re=build_url_re(tlds=['рф', 'xn--p1ai'])) +puny_linker = Linker(url_re=build_url_re(tlds=["рф", "xn--p1ai"])) print(puny_linker.linkify(html)) # https://xn--80aaksdi3bpu.xn--p1ai/ https://дайтрафик.рф/ -print('\n' + '-' * 100 + '\n') +print("\n" + "-" * 100 + "\n") -only_fish_tld_url_re = build_email_re(tlds=['fish']) +only_fish_tld_url_re = build_email_re(tlds=["fish"]) linker = Linker(email_re=only_fish_tld_url_re, parse_email=True) -print(linker.linkify('does not link email: foo@example.com')) +print(linker.linkify("does not link email: foo@example.com")) # does not link email: foo@example.com -print(linker.linkify('links email foo@example.fish')) +print(linker.linkify("links email foo@example.fish")) # links email foo@example.fish diff --git a/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_removing_attributes.py b/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_removing_attributes.py index fcc8cd143..2d21bb56a 100644 --- a/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_removing_attributes.py +++ b/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_removing_attributes.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mozilla/bleach @@ -14,14 +14,15 @@ # Removing Attributes + def allowed_attrs(attrs, new=False): """Only allow href, target, rel and title.""" allowed = [ - (None, 'href'), - (None, 'target'), - (None, 'rel'), - (None, 'title'), - '_text', + (None, "href"), + (None, "target"), + (None, "rel"), + (None, "title"), + "_text", ] return dict((k, v) for k, v in attrs.items() if k in allowed) @@ -34,10 +35,12 @@ def allowed_attrs(attrs, new=False): print() + def remove_title(attrs, new=False): - attrs.pop((None, 'title'), None) + attrs.pop((None, "title"), None) return attrs + linker = Linker(callbacks=[remove_title]) print(linker.linkify('link')) # link diff --git a/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_removing_links.py b/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_removing_links.py index 5fa74a432..2fd82cd76 100644 --- a/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_removing_links.py +++ b/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_removing_links.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mozilla/bleach @@ -14,8 +14,9 @@ # Removing Links + def remove_mailto(attrs, new=False): - if attrs[(None, 'href')].startswith('mailto:'): + if attrs[(None, "href")].startswith("mailto:"): return None return attrs @@ -23,10 +24,10 @@ def remove_mailto(attrs, new=False): linker = Linker(callbacks=[remove_mailto]) -html = ''' +html = """ mail janet! abc http://example.com def -'''.strip() +""".strip() print(linker.linkify(html)) # mail janet! # abc http://example.com def diff --git a/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_setting_attributes.py b/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_setting_attributes.py index 7381bafd6..10c8d7901 100644 --- a/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_setting_attributes.py +++ b/bleach__examples__remove_tags__clear_html/linkify__callbacks_for_setting_attributes.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mozilla/bleach @@ -16,30 +16,34 @@ # Setting Attributes + def set_title(attrs, new=False): - attrs[(None, 'title')] = 'link in user text' + attrs[(None, "title")] = "link in user text" return attrs + linker = Linker(callbacks=[set_title]) -print(linker.linkify('abc http://example.com def')) +print(linker.linkify("abc http://example.com def")) # abc http://example.com def print() + def set_target(attrs, new=False): - p = urlparse(attrs[(None, 'href')]) - if p.netloc not in ['my-domain.com', 'other-domain.com']: - attrs[(None, 'target')] = '_blank' - attrs[(None, 'class')] = 'external' + p = urlparse(attrs[(None, "href")]) + if p.netloc not in ["my-domain.com", "other-domain.com"]: + attrs[(None, "target")] = "_blank" + attrs[(None, "class")] = "external" else: - attrs.pop((None, 'target'), None) + attrs.pop((None, "target"), None) return attrs -html = ''' + +html = """ abc http://example.com def 123 https://my-domain.com 456 !!! https://my-domain.com ### -'''.strip() +""".strip() linker = Linker(callbacks=[set_target]) print(linker.linkify(html)) # abc http://example.com def diff --git a/bleach__examples__remove_tags__clear_html/linkify__url_email.py b/bleach__examples__remove_tags__clear_html/linkify__url_email.py index 1c62ef42d..00557d99b 100644 --- a/bleach__examples__remove_tags__clear_html/linkify__url_email.py +++ b/bleach__examples__remove_tags__clear_html/linkify__url_email.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mozilla/bleach @@ -12,10 +12,10 @@ import bleach -html = ''' +html = """ http://example.com hello_world@example.com -'''.strip() +""".strip() # The default callback adds rel="nofollow" print(bleach.linkify(html, parse_email=True)) diff --git a/bleach__examples__remove_tags__clear_html/remove_comments.py b/bleach__examples__remove_tags__clear_html/remove_comments.py index 948e7a0ee..d1e739893 100644 --- a/bleach__examples__remove_tags__clear_html/remove_comments.py +++ b/bleach__examples__remove_tags__clear_html/remove_comments.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mozilla/bleach @@ -12,7 +12,7 @@ import bleach -html = 'my html' +html = "my html" print(bleach.clean(html)) # my html diff --git a/bleach__examples__remove_tags__clear_html/remove_tags.py b/bleach__examples__remove_tags__clear_html/remove_tags.py index 4a59077b5..e746856dc 100644 --- a/bleach__examples__remove_tags__clear_html/remove_tags.py +++ b/bleach__examples__remove_tags__clear_html/remove_tags.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/mozilla/bleach @@ -12,7 +12,7 @@ import bleach -html = '

is not allowed

' +html = "

is not allowed

" print(bleach.clean(html)) # <p><span>is not <span>allowed</span></span></p> diff --git a/broke_cyrillic_text_and_recovery.py b/broke_cyrillic_text_and_recovery.py index 04112bb14..0551df92b 100644 --- a/broke_cyrillic_text_and_recovery.py +++ b/broke_cyrillic_text_and_recovery.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - text = 'Создание таблицы' +if __name__ == "__main__": + text = "Создание таблицы" print(text) - print(text.encode('cp1251').decode('utf-8')) + print(text.encode("cp1251").decode("utf-8")) diff --git a/brute_force__examples/multiprocess.py b/brute_force__examples/multiprocess.py index 736f08b30..9c69a83d5 100644 --- a/brute_force__examples/multiprocess.py +++ b/brute_force__examples/multiprocess.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/a/776977/201445 @@ -11,9 +11,11 @@ import hashlib import itertools import multiprocessing +import string + from functools import partial -import string + alphabet = (string.ascii_letters + string.digits).encode() @@ -35,12 +37,14 @@ def brute_force(mask, target_sha256, n_cutoff=4): target_sha256 = binascii.unhexlify(target_sha256) - bytes_format = mask.replace(b'%', b'%%').replace(b'*', b'%c') - mp_check = partial(check_sha256, - bytes_format=bytes_format, - n=min(n_cutoff, mask.count(b'*')), - target_sha256=target_sha256) - n = max(0, mask.count(b'*') - n_cutoff) + bytes_format = mask.replace(b"%", b"%%").replace(b"*", b"%c") + mp_check = partial( + check_sha256, + bytes_format=bytes_format, + n=min(n_cutoff, mask.count(b"*")), + target_sha256=target_sha256, + ) + n = max(0, mask.count(b"*") - n_cutoff) all_repls_parent = itertools.product(alphabet, repeat=n) with multiprocessing.Pool() as pool: for data in pool.imap_unordered(mp_check, all_repls_parent): @@ -48,9 +52,9 @@ def brute_force(mask, target_sha256, n_cutoff=4): return data -if __name__ == '__main__': +if __name__ == "__main__": # Password: qwe12y - sha256_hex = b'b8cc184b3f4aeb6adb6601ea3e39ef5ac098791acc2a505a66746f43d0c7ed85' + sha256_hex = b"b8cc184b3f4aeb6adb6601ea3e39ef5ac098791acc2a505a66746f43d0c7ed85" - passw_bytes = brute_force(b'qw****', sha256_hex) + passw_bytes = brute_force(b"qw****", sha256_hex) print(passw_bytes.decode()) diff --git a/brute_force__examples/single.py b/brute_force__examples/single.py index 5b2ab6907..54029d840 100644 --- a/brute_force__examples/single.py +++ b/brute_force__examples/single.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/a/776780/201445 @@ -13,28 +13,28 @@ def brute_force(mask, hsh, alphabet): # заменяем '*' на '{}' для последующей подстановки в 'str.format()' - pwd_pat = mask.replace('*', '{}') + pwd_pat = mask.replace("*", "{}") # число звездочек - будем использовать в качестве `product(.., repeat)` - N = mask.count('*') + N = mask.count("*") i = 0 for chars in product(alphabet, repeat=N): i += 1 if i % 10000 == 0: - print('Iterations: {}'.format(i)) + print(f"Iterations: {i}") if hsh == hashlib.sha256(pwd_pat.format(*chars).encode()).hexdigest(): text = pwd_pat.format(*chars) - print('Found: ' + text) + print(f"Found: {text}") return text return None -if __name__ == '__main__': +if __name__ == "__main__": from string import ascii_letters, digits alphabet = ascii_letters + digits # Password: qwe12y - hsh = 'b8cc184b3f4aeb6adb6601ea3e39ef5ac098791acc2a505a66746f43d0c7ed85' - print(brute_force('qw****', hsh, alphabet)) + hsh = "b8cc184b3f4aeb6adb6601ea3e39ef5ac098791acc2a505a66746f43d0c7ed85" + print(brute_force("qw****", hsh, alphabet)) diff --git a/bs4__BeautifulSoup__examples/about_builders.py b/bs4__BeautifulSoup__examples/about_builders.py index ebf817572..e3479b7b5 100644 --- a/bs4__BeautifulSoup__examples/about_builders.py +++ b/bs4__BeautifulSoup__examples/about_builders.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install lxml @@ -13,20 +13,20 @@ text = "" -root = BeautifulSoup(text, 'html.parser') +root = BeautifulSoup(text, "html.parser") print(root.builder) # bs4.builder._htmlparser.HTMLParserTreeBuilder -root = BeautifulSoup(text, 'html5lib') +root = BeautifulSoup(text, "html5lib") print(root.builder) # bs4.builder._html5lib.HTML5TreeBuilder -root = BeautifulSoup(text, 'xml') +root = BeautifulSoup(text, "xml") print(root.builder) # bs4.builder._lxml.LXMLTreeBuilderForXML -root = BeautifulSoup(text, 'lxml-xml') +root = BeautifulSoup(text, "lxml-xml") print(root.builder) # bs4.builder._lxml.LXMLTreeBuilderForXML -root = BeautifulSoup(text, 'lxml') +root = BeautifulSoup(text, "lxml") print(root.builder) # bs4.builder._lxml.LXMLTreeBuilder -root = BeautifulSoup(text, 'lxml-html') +root = BeautifulSoup(text, "lxml-html") print(root.builder) # bs4.builder._lxml.LXMLTreeBuilder diff --git a/bs4__book_authors.py b/bs4__book_authors.py index bfb0db8d1..9c31119c6 100644 --- a/bs4__book_authors.py +++ b/bs4__book_authors.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +from bs4 import BeautifulSoup text = """ @@ -173,17 +176,17 @@ def get_title_book(item) -> str: def get_author_full_name(author_node) -> str: - return author_node.lastname.text + ' ' + author_node.initials.text + return author_node.lastname.text + " " + author_node.initials.text def get_authors(item) -> list: - authors = item.select('authors > author') + authors = item.select("authors > author") # Словарь для хранения номера автора и списка на разных языках num_author_by_authors = dict() for author in authors: - num = author['num'] + num = author["num"] if num not in num_author_by_authors: num_author_by_authors[num] = [] @@ -203,7 +206,7 @@ def get_authors(item) -> list: for author in authors: # Приоритетный язык - if author['lang'] == "RU": + if author["lang"] == "RU": full_name = get_author_full_name(author) break @@ -212,10 +215,9 @@ def get_authors(item) -> list: return authors_full_name -from bs4 import BeautifulSoup -root = BeautifulSoup(text, 'html.parser') +root = BeautifulSoup(text, "html.parser") -for item in root.select('item'): +for item in root.select("item"): title_book = get_title_book(item) print(title_book) diff --git a/build_exe/pyinstaller_example/adding_an_icon/main.py b/build_exe/pyinstaller_example/adding_an_icon/main.py index f7eb998d6..88708e355 100644 --- a/build_exe/pyinstaller_example/adding_an_icon/main.py +++ b/build_exe/pyinstaller_example/adding_an_icon/main.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': +if __name__ == "__main__": import os os.system("echo Hello World!") diff --git a/build_exe/pyinstaller_example/compress_exe/main.py b/build_exe/pyinstaller_example/compress_exe/main.py index a3bf04cfc..f1e929469 100644 --- a/build_exe/pyinstaller_example/compress_exe/main.py +++ b/build_exe/pyinstaller_example/compress_exe/main.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': +if __name__ == "__main__": import sys from PySide.QtGui import * @@ -13,11 +13,11 @@ app = QApplication(sys.argv) - line_edit = QLineEdit('New line') - button = QPushButton('Add') + line_edit = QLineEdit("New line") + button = QPushButton("Add") text_edit = QTextEdit() - def add_to_text(): + def add_to_text() -> None: text = line_edit.text() text_edit.append(text) @@ -29,7 +29,7 @@ def add_to_text(): layout.addWidget(text_edit) w = QWidget() - w.setWindowTitle('Example') + w.setWindowTitle("Example") w.setLayout(layout) w.show() diff --git a/build_exe/pyinstaller_example/console/hello_world/main.py b/build_exe/pyinstaller_example/console/hello_world/main.py index f7eb998d6..88708e355 100644 --- a/build_exe/pyinstaller_example/console/hello_world/main.py +++ b/build_exe/pyinstaller_example/console/hello_world/main.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': +if __name__ == "__main__": import os os.system("echo Hello World!") diff --git a/build_exe/pyinstaller_example/console/print_current_script_dir/main.py b/build_exe/pyinstaller_example/console/print_current_script_dir/main.py index 8b5b0dfac..854782ed9 100644 --- a/build_exe/pyinstaller_example/console/print_current_script_dir/main.py +++ b/build_exe/pyinstaller_example/console/print_current_script_dir/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # FROM: https://github.com/gil9red/SimplePyScripts/blob/927af3d3f05a0129338b21d074ca241c8a055118/get_current_script_dir.py @@ -14,7 +14,7 @@ def get_current_script_dir(follow_symlinks=True, normcase=False) -> str: # py2exe, PyInstaller, cx_Freeze - if getattr(sys, 'frozen', False): + if getattr(sys, "frozen", False): path = os.path.abspath(sys.executable) else: # Analog inspect.getabsfile without os.path.normcase @@ -30,5 +30,5 @@ def get_current_script_dir(follow_symlinks=True, normcase=False) -> str: return os.path.dirname(path) -if __name__ == '__main__': +if __name__ == "__main__": print(get_current_script_dir()) diff --git a/build_exe/pyinstaller_example/gui/qt_pyqt5/main.py b/build_exe/pyinstaller_example/gui/qt_pyqt5/main.py index c855caf64..f504fb661 100644 --- a/build_exe/pyinstaller_example/gui/qt_pyqt5/main.py +++ b/build_exe/pyinstaller_example/gui/qt_pyqt5/main.py @@ -1,21 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys -from PyQt5.QtWidgets import QApplication, QLineEdit, QPushButton, QTextEdit, QVBoxLayout, QWidget +from PyQt5.QtWidgets import ( + QApplication, + QLineEdit, + QPushButton, + QTextEdit, + QVBoxLayout, + QWidget, +) app = QApplication(sys.argv) -line_edit = QLineEdit('New line') -button = QPushButton('Add') +line_edit = QLineEdit("New line") +button = QPushButton("Add") text_edit = QTextEdit() -def add_to_text(): +def add_to_text() -> None: text = line_edit.text() text_edit.append(text) @@ -28,7 +35,7 @@ def add_to_text(): layout.addWidget(text_edit) w = QWidget() -w.setWindowTitle('Example') +w.setWindowTitle("Example") w.setLayout(layout) w.show() diff --git a/build_exe/pyinstaller_example/gui/qt_pyside/main.py b/build_exe/pyinstaller_example/gui/qt_pyside/main.py index a3bf04cfc..f1e929469 100644 --- a/build_exe/pyinstaller_example/gui/qt_pyside/main.py +++ b/build_exe/pyinstaller_example/gui/qt_pyside/main.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': +if __name__ == "__main__": import sys from PySide.QtGui import * @@ -13,11 +13,11 @@ app = QApplication(sys.argv) - line_edit = QLineEdit('New line') - button = QPushButton('Add') + line_edit = QLineEdit("New line") + button = QPushButton("Add") text_edit = QTextEdit() - def add_to_text(): + def add_to_text() -> None: text = line_edit.text() text_edit.append(text) @@ -29,7 +29,7 @@ def add_to_text(): layout.addWidget(text_edit) w = QWidget() - w.setWindowTitle('Example') + w.setWindowTitle("Example") w.setLayout(layout) w.show() diff --git a/build_exe/pyinstaller_example/no_console__without_console/main.py b/build_exe/pyinstaller_example/no_console__without_console/main.py index ef6d21d52..400b0f33c 100644 --- a/build_exe/pyinstaller_example/no_console__without_console/main.py +++ b/build_exe/pyinstaller_example/no_console__without_console/main.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -print('Start!') +print("Start!") -with open('hello.txt', 'w', encoding='utf-8') as f: - f.write('Hello World!') +with open("hello.txt", "w", encoding="utf-8") as f: + f.write("Hello World!") diff --git a/build_fake_image__build_exe/__injected_code.py b/build_fake_image__build_exe/__injected_code.py index ccab49c37..a029e4a3b 100644 --- a/build_fake_image__build_exe/__injected_code.py +++ b/build_fake_image__build_exe/__injected_code.py @@ -1,5 +1,7 @@ import os.path from pathlib import Path -file_name = Path(os.path.expanduser("~/Desktop")).resolve() / "README_YOU_WERE_HACKED.txt" + +DIR = Path(os.path.expanduser("~/Desktop")).resolve() +file_name = DIR / "README_YOU_WERE_HACKED.txt" file_name.touch(exist_ok=True) diff --git a/build_fake_image__build_exe/build_exe.py b/build_fake_image__build_exe/build_exe.py index eb0736a55..389339342 100644 --- a/build_fake_image__build_exe/build_exe.py +++ b/build_fake_image__build_exe/build_exe.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://habr.com/post/126198/ @@ -19,19 +19,19 @@ from pathlib import Path -FILE_NAME = 'image.jpg' -FILE_NAME_ICO = 'icon.ico' +FILE_NAME = "image.jpg" +FILE_NAME_ICO = "icon.ico" ICON_SIZES = [(16, 16), (32, 32), (48, 48), (64, 64)] -OUT_FILE_NAME = 'main.exe' +OUT_FILE_NAME = "main.exe" # Юникодная последовательсть \u202E нужна чтобы превратить: picgpj.exe -> pic‮gpj.exe -NEW_FILE_NAME = 'pic\u202Egpj.exe' +NEW_FILE_NAME = "pic\u202Egpj.exe" -FILE_NAME_ARCHIVE = 'pic.zip' +FILE_NAME_ARCHIVE = "pic.zip" -INJECT_FILE_NAME = '__injected_code.py' -INJECT_CODE = Path(INJECT_FILE_NAME).read_text(encoding='utf-8') +INJECT_FILE_NAME = "__injected_code.py" +INJECT_CODE = Path(INJECT_FILE_NAME).read_text(encoding="utf-8") # Создадим ico файл для иконки приложения @@ -41,20 +41,27 @@ generator.generate(FILE_NAME, INJECT_CODE) # Analog build_exe.bat -subprocess.call([ - "pyinstaller", "--onefile", "--noconsole", - "--icon=" + FILE_NAME_ICO, "--name=" + OUT_FILE_NAME, - generator.FILE_NAME -]) - -shutil.copy('dist/' + OUT_FILE_NAME, 'dist/' + NEW_FILE_NAME) +subprocess.call( + [ + "pyinstaller", + "--onefile", + "--noconsole", + "--icon=" + FILE_NAME_ICO, + "--name=" + OUT_FILE_NAME, + generator.FILE_NAME, + ] +) + +shutil.copy("dist/" + OUT_FILE_NAME, "dist/" + NEW_FILE_NAME) # Добавляем сгенерированный файл в архив -with zipfile.ZipFile('dist/' + FILE_NAME_ARCHIVE, mode='w', compression=zipfile.ZIP_DEFLATED) as f: - f.write('dist/' + NEW_FILE_NAME, NEW_FILE_NAME) +with zipfile.ZipFile( + "dist/" + FILE_NAME_ARCHIVE, mode="w", compression=zipfile.ZIP_DEFLATED +) as f: + f.write("dist/" + NEW_FILE_NAME, NEW_FILE_NAME) # Подчистим за собой, удалив ненужные файлы os.remove(FILE_NAME_ICO) os.remove(generator.FILE_NAME) -os.remove('main.exe.spec') -shutil.rmtree('build') +os.remove("main.exe.spec") +shutil.rmtree("build") diff --git a/build_fake_image__build_exe/generator.py b/build_fake_image__build_exe/generator.py index 7ef7e4f3e..8b0eabda3 100644 --- a/build_fake_image__build_exe/generator.py +++ b/build_fake_image__build_exe/generator.py @@ -1,12 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import base64 +from PIL import Image # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/7cebedb16a5ac81333ebf62af410fdb53a690d29/convert_image_to_ico/main.py -def convert_image_to_ico(file_name, file_name_ico, icon_sizes=None): - from PIL import Image +def convert_image_to_ico(file_name, file_name_ico, icon_sizes=None) -> None: img = Image.open(file_name) if icon_sizes: @@ -15,19 +18,18 @@ def convert_image_to_ico(file_name, file_name_ico, icon_sizes=None): img.save(file_name_ico) -FILE_NAME = 'image_png.py' +FILE_NAME = "image_png.py" # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/aec64c1749d4f6f3176e3222c7e7c554f40c693f/generator_py_with_inner_image_with_open/main.py -def generate(file_name, inject_code: str): - with open(file_name, 'rb') as f: +def generate(file_name, inject_code: str) -> None: + with open(file_name, "rb") as f: img_bytes = f.read() - import base64 - img_base64 = base64.b64encode(img_bytes).decode('utf-8') + img_base64 = base64.b64encode(img_bytes).decode("utf-8") - with open(FILE_NAME, 'w', encoding='utf-8') as f: - f.write('''\ + with open(FILE_NAME, "w", encoding="utf-8") as f: + f.write("""\ #!/usr/bin/env python3 # -*- coding: utf-8 -*- @@ -54,11 +56,11 @@ def save_and_run(): # INJECTED CODE {} -'''.format(file_name, img_base64, inject_code)) +""".format(file_name, img_base64, inject_code)) -if __name__ == '__main__': - file_name = 'image.jpg' +if __name__ == "__main__": + file_name = "image.jpg" inject_code = """ import os.path from pathlib import Path diff --git a/caesar_code/caesar_code.py b/caesar_code/caesar_code.py index a98f5176b..a2dec9261 100644 --- a/caesar_code/caesar_code.py +++ b/caesar_code/caesar_code.py @@ -1,13 +1,14 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" """Реализация алгоритма Код Цезаря.""" import typing - from string import ascii_lowercase, ascii_uppercase -ru_lowercase = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя' + + +ru_lowercase = "абвгдеёжзийклмнопрстуфхцчшщъыьэюя" ru_uppercase = ru_lowercase.upper() alphabet_list = [ @@ -17,7 +18,7 @@ ru_uppercase, # Грузинский язык - 'აბგდევზთიკლმნოპჟრსტჳუფქღყშჩცძწჭხჴჯჰ', + "აბგდევზთიკლმნოპჟრსტჳუფქღყშჩცძწჭხჴჯჰ", ] @@ -33,7 +34,7 @@ def caesar_code(text: str, shift: int) -> str: """Функция принимает текстовую строку и возвращает, новую строку символы которой сдвинуты по алфавиту.""" - shift_text = '' + shift_text = "" for c in text: alphabet = get_alp_by_char(c) @@ -47,7 +48,7 @@ def caesar_code(text: str, shift: int) -> str: return shift_text -if __name__ == '__main__': +if __name__ == "__main__": text = "Hello World!" print(caesar_code(text, shift=0)) print(caesar_code(text, shift=2)) @@ -57,10 +58,10 @@ def caesar_code(text: str, shift: int) -> str: print(caesar_code(text, shift=78)) assert caesar_code(text, shift=0) == text - assert caesar_code(text, shift=2) == 'Jgnnq Yqtnf!' - assert caesar_code(text, shift=-4) == 'Dahhk Sknhz!' + assert caesar_code(text, shift=2) == "Jgnnq Yqtnf!" + assert caesar_code(text, shift=-4) == "Dahhk Sknhz!" assert caesar_code(text, shift=26) == text - assert caesar_code(text, shift=50) == 'Fcjjm Umpjb!' + assert caesar_code(text, shift=50) == "Fcjjm Umpjb!" assert caesar_code(text, shift=78) == text print() @@ -74,10 +75,10 @@ def caesar_code(text: str, shift: int) -> str: print(caesar_code(text, shift=99)) assert caesar_code(text, shift=0) == text - assert caesar_code(text, shift=2) == 'Сткджф окт!' - assert caesar_code(text, shift=-4) == 'Лмеюбо ием!' + assert caesar_code(text, shift=2) == "Сткджф окт!" + assert caesar_code(text, shift=-4) == "Лмеюбо ием!" assert caesar_code(text, shift=33) == text - assert caesar_code(text, shift=50) == 'Абщтхг эщб!' + assert caesar_code(text, shift=50) == "Абщтхг эщб!" assert caesar_code(text, shift=99) == text print() @@ -87,22 +88,22 @@ def caesar_code(text: str, shift: int) -> str: print(caesar_code(text, shift=-4)) assert caesar_code(text, shift=0) == text - assert caesar_code(text, shift=2) == 'Jgnnq окт!' - assert caesar_code(text, shift=-4) == 'Dahhk ием!' + assert caesar_code(text, shift=2) == "Jgnnq окт!" + assert caesar_code(text, shift=-4) == "Dahhk ием!" print() - text = 'გამარჯობა მსოფლიოში!' + text = "გამარჯობა მსოფლიოში!" print(caesar_code(text, shift=0)) print(caesar_code(text, shift=2)) print(caesar_code(text, shift=-4)) assert caesar_code(text, shift=0) == text - assert caesar_code(text, shift=2) == 'ეგოგტაჟდგ ოჳჟღნლჟცლ!' - assert caesar_code(text, shift=-4) == 'ჯხთხნწკჴხ თოკსზეკფე!' + assert caesar_code(text, shift=2) == "ეგოგტაჟდგ ოჳჟღნლჟცლ!" + assert caesar_code(text, shift=-4) == "ჯხთხნწკჴხ თოკსზეკფე!" # Hint: see shift=23 print() print() - TEXT = 'VWDQ LV QRW ZKDW KH VHHPV' + TEXT = "VWDQ LV QRW ZKDW KH VHHPV" for i in range(len(ascii_uppercase)): print(i, caesar_code(TEXT, i)) diff --git a/caesar_code/rot13.py b/caesar_code/rot13.py index f729b3ca1..cccc270fd 100644 --- a/caesar_code/rot13.py +++ b/caesar_code/rot13.py @@ -1,13 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import codecs def rot13(message): - import codecs - return codecs.encode(message, 'rot13') + return codecs.encode(message, "rot13") -assert rot13("test") == "grfg" -assert rot13("Test") == "Grfg" +if __name__ == '__main__': + assert rot13("test") == "grfg" + assert rot13("Test") == "Grfg" diff --git a/calc_statistic_anime_Naruto__by_fillers/main.py b/calc_statistic_anime_Naruto__by_fillers/main.py index b1e08c715..83b27bd89 100644 --- a/calc_statistic_anime_Naruto__by_fillers/main.py +++ b/calc_statistic_anime_Naruto__by_fillers/main.py @@ -1,26 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def get_html_by_url__from_cache(url, cache_dir='cache'): - import os +import os +import re + +import requests + + +def get_html_by_url__from_cache(url, cache_dir="cache"): os.makedirs(cache_dir, exist_ok=True) file_name = os.path.basename(url) + file_name = re.sub(r"\W", "_", file_name) - import re - file_name = re.sub(r'[^\w\d]', '_', file_name) - - file_name = cache_dir + '/' + file_name + '.html' + file_name = cache_dir + "/" + file_name + ".html" if os.path.exists(file_name): - with open(file_name, 'rb') as f: + with open(file_name, "rb") as f: return f.read() - with open(file_name, 'wb') as f: - import requests + with open(file_name, "wb") as f: rs = requests.get(url) f.write(rs.content) @@ -30,9 +32,9 @@ def get_html_by_url__from_cache(url, cache_dir='cache'): def get_ep_chapters_s1(): def get_urls_of_season(): return [ - 'https://ru.wikipedia.org/wiki/Список_серий_«Наруто»_(сезоны_1—3)', - 'https://ru.wikipedia.org/wiki/Список_серий_«Наруто»_(сезоны_4—6)', - 'https://ru.wikipedia.org/wiki/Список_серий_«Наруто»_(сезоны_7—9)', + "https://ru.wikipedia.org/wiki/Список_серий_«Наруто»_(сезоны_1—3)", + "https://ru.wikipedia.org/wiki/Список_серий_«Наруто»_(сезоны_4—6)", + "https://ru.wikipedia.org/wiki/Список_серий_«Наруто»_(сезоны_7—9)", ] from bs4 import BeautifulSoup @@ -42,10 +44,10 @@ def get_urls_of_season(): for url in get_urls_of_season(): html_content = get_html_by_url__from_cache(url) - root = BeautifulSoup(html_content, 'html.parser') + root = BeautifulSoup(html_content, "html.parser") - for td in root.select('td'): - if td.has_attr('id') and td['id'].startswith('ep'): + for td in root.select("td"): + if td.has_attr("id") and td["id"].startswith("ep"): episode = td.text.strip() manga_chapters = td.next_sibling.next_sibling.text.strip() @@ -57,13 +59,13 @@ def get_urls_of_season(): def get_ep_chapters_s2(): def get_urls_of_season(): return [ - 'https://ru.wikipedia.org/wiki/Список_серий_«Наруто:_Ураганные_хроники»_(сезоны_1—4)', - 'https://ru.wikipedia.org/wiki/Список_серий_«Наруто:_Ураганные_хроники»_(сезоны_5—8)', - 'https://ru.wikipedia.org/wiki/Список_серий_«Наруто:_Ураганные_хроники»_(сезоны_9—12)', - 'https://ru.wikipedia.org/wiki/Список_серий_«Наруто:_Ураганные_хроники»_(сезоны_13—16)', - 'https://ru.wikipedia.org/wiki/Список_серий_«Наруто:_Ураганные_хроники»_(сезоны_17—20)', - 'https://ru.wikipedia.org/wiki/Список_серий_«Наруто:_Ураганные_хроники»_(сезоны_21—24)', - 'https://ru.wikipedia.org/wiki/Список_серий_«Наруто:_Ураганные_хроники»_(сезоны_25—28)', + "https://ru.wikipedia.org/wiki/Список_серий_«Наруто:_Ураганные_хроники»_(сезоны_1—4)", + "https://ru.wikipedia.org/wiki/Список_серий_«Наруто:_Ураганные_хроники»_(сезоны_5—8)", + "https://ru.wikipedia.org/wiki/Список_серий_«Наруто:_Ураганные_хроники»_(сезоны_9—12)", + "https://ru.wikipedia.org/wiki/Список_серий_«Наруто:_Ураганные_хроники»_(сезоны_13—16)", + "https://ru.wikipedia.org/wiki/Список_серий_«Наруто:_Ураганные_хроники»_(сезоны_17—20)", + "https://ru.wikipedia.org/wiki/Список_серий_«Наруто:_Ураганные_хроники»_(сезоны_21—24)", + "https://ru.wikipedia.org/wiki/Список_серий_«Наруто:_Ураганные_хроники»_(сезоны_25—28)", ] from bs4 import BeautifulSoup @@ -73,10 +75,10 @@ def get_urls_of_season(): for url in get_urls_of_season(): html_content = get_html_by_url__from_cache(url) - root = BeautifulSoup(html_content, 'html.parser') + root = BeautifulSoup(html_content, "html.parser") - for td in root.select('td'): - if td.has_attr('id') and td['id'].startswith('ep'): + for td in root.select("td"): + if td.has_attr("id") and td["id"].startswith("ep"): episode = td.text.strip() manga_chapters = td.next_sibling.next_sibling.next_sibling.text.strip() @@ -87,28 +89,40 @@ def get_urls_of_season(): def calc_statistic(ep_chapters: list) -> (int, int, int, int): number_all_ep = len(ep_chapters) - number_filler_ep = len([x for x in ep_chapters if 'Нет (филлер)' in x[1]]) + number_filler_ep = len([x for x in ep_chapters if "Нет (филлер)" in x[1]]) number_manga_ep = number_all_ep - number_filler_ep precent_filler = number_filler_ep / number_all_ep * 100 return number_all_ep, number_manga_ep, number_filler_ep, precent_filler -if __name__ == '__main__': +if __name__ == "__main__": ep_chapters_s1 = get_ep_chapters_s1() - number_all_ep_s1, _, number_filler_ep_s1, precent_filler_s1 = calc_statistic(ep_chapters_s1) - print(f'Season 1.\n' - f' Total: {number_all_ep_s1}, fillers: {number_filler_ep_s1} ({precent_filler_s1:.1f}%)\n') + number_all_ep_s1, _, number_filler_ep_s1, precent_filler_s1 = calc_statistic( + ep_chapters_s1 + ) + print( + f"Season 1.\n" + f" Total: {number_all_ep_s1}, fillers: {number_filler_ep_s1} ({precent_filler_s1:.1f}%)\n" + ) ep_chapters_s2 = get_ep_chapters_s2() - number_all_ep_s2, _, number_filler_ep_s2, precent_filler_s2 = calc_statistic(ep_chapters_s2) - print(f'Season 2.\n' - f' Total: {number_all_ep_s2}, fillers: {number_filler_ep_s2} ({precent_filler_s2:.1f}%)\n') + number_all_ep_s2, _, number_filler_ep_s2, precent_filler_s2 = calc_statistic( + ep_chapters_s2 + ) + print( + f"Season 2.\n" + f" Total: {number_all_ep_s2}, fillers: {number_filler_ep_s2} ({precent_filler_s2:.1f}%)\n" + ) ep_chapters_s1_2 = ep_chapters_s1 + ep_chapters_s2 - number_all_ep_s1_2, _, number_filler_ep_s1_2, precent_filler_s1_2 = calc_statistic(ep_chapters_s1_2) - print(f'Season 1+2.\n' - f' Total: {number_all_ep_s1_2}, fillers: {number_filler_ep_s1_2} ({precent_filler_s1_2:.1f}%)\n') + number_all_ep_s1_2, _, number_filler_ep_s1_2, precent_filler_s1_2 = calc_statistic( + ep_chapters_s1_2 + ) + print( + f"Season 1+2.\n" + f" Total: {number_all_ep_s1_2}, fillers: {number_filler_ep_s1_2} ({precent_filler_s1_2:.1f}%)\n" + ) # Season 1. # Total: 220, fillers: 93 (42.3%) diff --git a/calculator/calculator.py b/calculator/calculator.py index a29b6a375..fb3da2435 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -1,38 +1,38 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" while True: oper = input("Enter 'add', 'substract', 'multiply', 'divide', 'quit', 'power': ") - if oper == 'quit': + if oper == "quit": break - + try: - if oper == 'add': + if oper == "add": x = int(input("Enter 1st num: ")) y = int(input("Enter 2nd num: ")) print(x + y) - elif oper == 'substract': + elif oper == "substract": x = int(input("Enter 1st num: ")) y = int(input("Enter 2nd num: ")) print(x - y) - elif oper == 'multiply': + elif oper == "multiply": x = int(input("Enter 1st num: ")) y = int(input("Enter 2nd num: ")) print(x * y) - elif oper == 'divide': + elif oper == "divide": x = int(input("Enter 1st num: ")) y = int(input("Enter 2nd num: ")) print(x / y) - elif oper == 'power': + elif oper == "power": x = int(input("Enter the base: ")) y = int(input("Enter the exponent: ")) - print(x ** y) + print(x**y) else: print("Unknown command!") - + except ValueError: print("Invalid value!") diff --git a/calculator/use__ast_parse_eval.py b/calculator/use__ast_parse_eval.py index c38e6e840..4ed6dd3d5 100644 --- a/calculator/use__ast_parse_eval.py +++ b/calculator/use__ast_parse_eval.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/9558001/5909792 @@ -20,7 +20,7 @@ ast.FloorDiv: op.floordiv, ast.Pow: op.pow, ast.BitXor: op.xor, - ast.USub: op.neg + ast.USub: op.neg, } @@ -48,13 +48,13 @@ def eval_expr(expr): -5.0 """ - return eval_(ast.parse(expr, mode='eval').body) + return eval_(ast.parse(expr, mode="eval").body) -if __name__ == '__main__': - print(eval_expr('2 + 2 * 2')) # 6 - print(eval_expr('(2 + 2) * 2')) # 8 - print(eval_expr('10 * 10')) # 100 - print(eval_expr('100 / 10')) # 10.0 - print(eval_expr('100 // 10')) # 10 - print(eval_expr('2 ** 3')) # 8 +if __name__ == "__main__": + print(eval_expr("2 + 2 * 2")) # 6 + print(eval_expr("(2 + 2) * 2")) # 8 + print(eval_expr("10 * 10")) # 100 + print(eval_expr("100 / 10")) # 10.0 + print(eval_expr("100 // 10")) # 10 + print(eval_expr("2 ** 3")) # 8 diff --git a/calculator/use__numeric_string_parser__module/main.py b/calculator/use__numeric_string_parser__module/main.py index ab105b079..102f34f3e 100644 --- a/calculator/use__numeric_string_parser__module/main.py +++ b/calculator/use__numeric_string_parser__module/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # from math import * @@ -22,8 +22,8 @@ from numeric_string_parser import NumericStringParser -if __name__ == '__main__': +if __name__ == "__main__": nsp = NumericStringParser() - print(nsp.eval('2^4')) - print(nsp.eval('(2+2)*2^4')) - print(nsp.eval('sin(2+2)')) + print(nsp.eval("2^4")) + print(nsp.eval("(2+2)*2^4")) + print(nsp.eval("sin(2+2)")) diff --git a/calculator/use__numeric_string_parser__module/numeric_string_parser.py b/calculator/use__numeric_string_parser__module/numeric_string_parser.py index 8fa13ce0b..673a5e664 100644 --- a/calculator/use__numeric_string_parser__module/numeric_string_parser.py +++ b/calculator/use__numeric_string_parser__module/numeric_string_parser.py @@ -1,32 +1,47 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from pyparsing import (Literal,CaselessLiteral,Word,Combine,Group,Optional, - ZeroOrMore,Forward,nums,alphas,oneOf) +from pyparsing import ( + Literal, + CaselessLiteral, + Word, + Combine, + Group, + Optional, + ZeroOrMore, + Forward, + nums, + alphas, + oneOf, +) import math import operator -__author__='Paul McGuire' -__version__ = '$Revision: 0.0 $' -__date__ = '$Date: 2009-03-20 $' -__source__='''http://pyparsing.wikispaces.com/file/view/fourFn.py +__author__ = "Paul McGuire" +__version__ = "$Revision: 0.0 $" +__date__ = "$Date: 2009-03-20 $" +__source__ = """http://pyparsing.wikispaces.com/file/view/fourFn.py http://pyparsing.wikispaces.com/message/view/home/15549426 -''' -__note__=''' +""" +__note__ = """ All I've done is rewrap Paul McGuire's fourFn.py as a class, so I can use it more easily in other places. -''' +""" + class NumericStringParser(object): - ''' + """ Most of this code comes from the fourFn.py pyparsing example - ''' - def pushFirst(self, strg, loc, toks ): - self.exprStack.append( toks[0] ) - def pushUMinus(self, strg, loc, toks ): - if toks and toks[0]=='-': - self.exprStack.append( 'unary -' ) + """ + + def pushFirst(self, strg, loc, toks): + self.exprStack.append(toks[0]) + + def pushUMinus(self, strg, loc, toks): + if toks and toks[0] == "-": + self.exprStack.append("unary -") + def __init__(self): """ expop :: '^' @@ -38,72 +53,84 @@ def __init__(self): term :: factor [ multop factor ]* expr :: term [ addop term ]* """ - point = Literal( "." ) - e = CaselessLiteral( "E" ) - fnumber = Combine( Word( "+-"+nums, nums ) + - Optional( point + Optional( Word( nums ) ) ) + - Optional( e + Word( "+-"+nums, nums ) ) ) - ident = Word(alphas, alphas+nums+"_$") - plus = Literal( "+" ) - minus = Literal( "-" ) - mult = Literal( "*" ) - div = Literal( "/" ) - lpar = Literal( "(" ).suppress() - rpar = Literal( ")" ).suppress() - addop = plus | minus + point = Literal(".") + e = CaselessLiteral("E") + fnumber = Combine( + Word("+-" + nums, nums) + + Optional(point + Optional(Word(nums))) + + Optional(e + Word("+-" + nums, nums)) + ) + ident = Word(alphas, alphas + nums + "_$") + plus = Literal("+") + minus = Literal("-") + mult = Literal("*") + div = Literal("/") + lpar = Literal("(").suppress() + rpar = Literal(")").suppress() + addop = plus | minus multop = mult | div - expop = Literal( "^" ) - pi = CaselessLiteral( "PI" ) + expop = Literal("^") + pi = CaselessLiteral("PI") expr = Forward() - atom = ((Optional(oneOf("- +")) + - (pi|e|fnumber|ident+lpar+expr+rpar).setParseAction(self.pushFirst)) - | Optional(oneOf("- +")) + Group(lpar+expr+rpar) - ).setParseAction(self.pushUMinus) + atom = ( + ( + Optional(oneOf("- +")) + + (pi | e | fnumber | ident + lpar + expr + rpar).setParseAction( + self.pushFirst + ) + ) + | Optional(oneOf("- +")) + Group(lpar + expr + rpar) + ).setParseAction(self.pushUMinus) # by defining exponentiation as "atom [ ^ factor ]..." instead of # "atom [ ^ atom ]...", we get right-to-left exponents, instead of left-to-right # that is, 2^3^2 = 2^(3^2), not (2^3)^2. factor = Forward() - factor << atom + ZeroOrMore( ( expop + factor ).setParseAction( self.pushFirst ) ) - term = factor + ZeroOrMore( ( multop + factor ).setParseAction( self.pushFirst ) ) - expr << term + ZeroOrMore( ( addop + term ).setParseAction( self.pushFirst ) ) + factor << atom + ZeroOrMore((expop + factor).setParseAction(self.pushFirst)) + term = factor + ZeroOrMore((multop + factor).setParseAction(self.pushFirst)) + expr << term + ZeroOrMore((addop + term).setParseAction(self.pushFirst)) # addop_term = ( addop + term ).setParseAction( self.pushFirst ) # general_term = term + ZeroOrMore( addop_term ) | OneOrMore( addop_term) # expr << general_term self.bnf = expr # map operator symbols to corresponding arithmetic operations epsilon = 1e-12 - self.opn = { "+" : operator.add, - "-" : operator.sub, - "*" : operator.mul, - "/" : operator.truediv, - "^" : operator.pow } - self.fn = { "sin" : math.sin, - "cos" : math.cos, - "tan" : math.tan, - "abs" : abs, - "trunc" : lambda a: int(a), - "round" : round, - "sgn" : lambda a: abs(a)>epsilon and cmp(a,0) or 0} - def evaluateStack(self, s ): + self.opn = { + "+": operator.add, + "-": operator.sub, + "*": operator.mul, + "/": operator.truediv, + "^": operator.pow, + } + self.fn = { + "sin": math.sin, + "cos": math.cos, + "tan": math.tan, + "abs": abs, + "trunc": lambda a: int(a), + "round": round, + "sgn": lambda a: abs(a) > epsilon and cmp(a, 0) or 0, + } + + def evaluateStack(self, s): op = s.pop() - if op == 'unary -': - return -self.evaluateStack( s ) + if op == "unary -": + return -self.evaluateStack(s) if op in "+-*/^": - op2 = self.evaluateStack( s ) - op1 = self.evaluateStack( s ) - return self.opn[op]( op1, op2 ) + op2 = self.evaluateStack(s) + op1 = self.evaluateStack(s) + return self.opn[op](op1, op2) elif op == "PI": - return math.pi # 3.1415926535 + return math.pi # 3.1415926535 elif op == "E": return math.e # 2.718281828 elif op in self.fn: - return self.fn[op]( self.evaluateStack( s ) ) + return self.fn[op](self.evaluateStack(s)) elif op[0].isalpha(): return 0 else: - return float( op ) - def eval(self,num_string,parseAll=True): - self.exprStack=[] - results=self.bnf.parseString(num_string,parseAll) - val=self.evaluateStack( self.exprStack[:] ) + return float(op) + + def eval(self, num_string, parseAll=True): + self.exprStack = self.bnf.parseString(num_string, parseAll) + val = self.evaluateStack(self.exprStack[:]) return val diff --git a/calculator/use__numexpr__module/main.py b/calculator/use__numexpr__module/main.py index 1470bfae3..c8def08db 100644 --- a/calculator/use__numexpr__module/main.py +++ b/calculator/use__numexpr__module/main.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import numexpr -print(numexpr.evaluate('2 + 2 * 2')) -print(numexpr.evaluate('10 ** 3')) -print(numexpr.evaluate('2 ** 10')) -print(numexpr.evaluate('sin(2 ** 10)')) -print(numexpr.evaluate('(0xFF + 255) / 0b1010')) -print(numexpr.evaluate('(0xFF + 255) // 0b1010')) + + +print(numexpr.evaluate("2 + 2 * 2")) +print(numexpr.evaluate("10 ** 3")) +print(numexpr.evaluate("2 ** 10")) +print(numexpr.evaluate("sin(2 ** 10)")) +print(numexpr.evaluate("(0xFF + 255) / 0b1010")) +print(numexpr.evaluate("(0xFF + 255) // 0b1010")) diff --git a/calculator/use__simpleeval__module.py b/calculator/use__simpleeval__module.py index 5572ecb37..96bcb731e 100644 --- a/calculator/use__simpleeval__module.py +++ b/calculator/use__simpleeval__module.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install simpleeval @@ -10,6 +10,6 @@ print(simple_eval("21 + 21")) # 42 print(simple_eval("2 + 2 * 2")) # 6 -print(simple_eval('10 ** 123')) # 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +print(simple_eval("10 ** 123")) # 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 print(simple_eval("21 + 19 / 7 + (8 % 3) ** 9")) # 535.7142857142857 print(simple_eval("square(11)", functions={"square": lambda x: x * x})) # 121 diff --git a/calculator/very_very_simple_calc.py b/calculator/very_very_simple_calc.py index cfcd0bbff..c8815a6b6 100644 --- a/calculator/very_very_simple_calc.py +++ b/calculator/very_very_simple_calc.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Very-very simple calc def calc(expr): operator_function = { - '-': lambda x, y: x - y, - '+': lambda x, y: x + y, - '*': lambda x, y: x * y, - '/': lambda x, y: x / y, + "-": lambda x, y: x - y, + "+": lambda x, y: x + y, + "*": lambda x, y: x * y, + "/": lambda x, y: x / y, } nums = list() opers = list() - tokens = list('(' + expr + ')') + tokens = list("(" + expr + ")") while tokens: token = tokens.pop(0) @@ -24,9 +24,9 @@ def calc(expr): if token.isdecimal(): nums.append(float(token)) else: - if token == ')': + if token == ")": oper = opers.pop() - while opers and oper != '(': + while opers and oper != "(": b, a = nums.pop(), nums.pop() f = operator_function[oper] nums.append(f(a, b)) @@ -39,7 +39,7 @@ def calc(expr): return nums[0] -if __name__ == '__main__': +if __name__ == "__main__": print(calc("((1+(2*3))*2)+4")) # 18.0 print(calc("(1*(3/2))+4")) # 5.5 print(calc("(2*(3/2))+4")) # 7.0 diff --git a/cbr_ru/XML_bic/main.py b/cbr_ru/XML_bic/main.py index b41f9166d..77ab64f54 100644 --- a/cbr_ru/XML_bic/main.py +++ b/cbr_ru/XML_bic/main.py @@ -1,21 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - from urllib.request import urlopen - from urllib.parse import quote +from urllib.request import urlopen +from urllib.parse import quote - query = 'сбер'.encode('windows-1251') - url = 'http://www.cbr.ru/scripts/XML_bic.asp?name=' + quote(query) - with urlopen(url) as f: - from bs4 import BeautifulSoup - root = BeautifulSoup(f.read(), "xml") +from bs4 import BeautifulSoup - for i, record in enumerate(root.find_all('Record'), 1): - name = record.find('ShortName').text - bic = record.find('Bic').text - print('{}. "{}", {}, {}'.format(i, name, bic, record['DU'])) +query = "сбер".encode("windows-1251") +url = "http://www.cbr.ru/scripts/XML_bic.asp?name=" + quote(query) +with urlopen(url) as f: + root = BeautifulSoup(f.read(), "xml") + + for i, record in enumerate(root.find_all("Record"), 1): + name = record.find("ShortName").text + bic = record.find("Bic").text + + print(f'{i}. "{name}", {bic}, {record["DU"]}') diff --git a/cbr_ru/XML_dynamic/current_usd_euro_rate_with_delta.py b/cbr_ru/XML_dynamic/current_usd_euro_rate_with_delta.py index 6e27bc9f3..c41b74398 100644 --- a/cbr_ru/XML_dynamic/current_usd_euro_rate_with_delta.py +++ b/cbr_ru/XML_dynamic/current_usd_euro_rate_with_delta.py @@ -1,7 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +from datetime import date, timedelta +from urllib.request import urlopen + +from bs4 import BeautifulSoup def current_rate(ccy_rq_id): @@ -15,38 +21,33 @@ def current_rate(ccy_rq_id): # Т.к. курс назначается не каждый день, разница может быть в несколько дней, # поэтому на всякий случай берем разницу от текущего дня и на 7 дней назад - from datetime import date, timedelta - date_req1 = (date.today() - timedelta(days=7)).strftime('%d/%m/%Y') - date_req2 = date.today().strftime('%d/%m/%Y') + date_req1 = (date.today() - timedelta(days=7)).strftime("%d/%m/%Y") + date_req2 = date.today().strftime("%d/%m/%Y") # date_req2 = (date.today() + timedelta(days=1)).strftime('%d/%m/%Y') - url = 'http://www.cbr.ru/scripts/XML_dynamic.asp?date_req1={}&date_req2={}&VAL_NM_RQ={}'.format( - date_req1, date_req2, ccy_rq_id - ) + url = f"http://www.cbr.ru/scripts/XML_dynamic.asp?date_req1={date_req1}&date_req2={date_req2}&VAL_NM_RQ={ccy_rq_id}" - from urllib.request import urlopen with urlopen(url) as f: - from bs4 import BeautifulSoup root = BeautifulSoup(f.read(), "xml") # Получаем список курсов - values = root.select('Record > Value') + values = root.select("Record > Value") if len(values) < 2: - raise Exception('Что-то пошло не так. Не хватает значений.\nurl: {}\nroot:\n{}'.format(url, root)) + raise Exception(f"Что-то пошло не так. Не хватает значений.\nurl: {url}\nroot:\n{root}" ) # Вытаскиваем последние два элемента и преобразуем в число values = [values[-2], values[-1]] - values = [float(price.text.replace(',', '.')) for price in values] + values = [float(price.text.replace(",", ".")) for price in values] delta = values[1] - values[0] return values[1], delta -if __name__ == '__main__': +if __name__ == "__main__": # R01235 -- USD, доллары, 840 - price, delta = current_rate('R01235') - print('USD: {} ({}{:.4f})'.format(price, ('+' if delta > 0 else ''), delta)) + price, delta = current_rate("R01235") + print(f"USD: {price} ({('+' if delta > 0 else '')}{delta:.4f})") # R01239 -- EUR, евро, 978 - price, delta = current_rate('R01239') - print('EUR: {} ({}{:.4f})'.format(price, ('+' if delta > 0 else ''), delta)) + price, delta = current_rate("R01239") + print(f"EUR: {price} ({('+' if delta > 0 else '')}{delta:.4f})") diff --git a/cbr_ru/xml_metall/main_old.py b/cbr_ru/xml_metall/main_old.py index 619d5694f..2cf28d1d1 100644 --- a/cbr_ru/xml_metall/main_old.py +++ b/cbr_ru/xml_metall/main_old.py @@ -1,22 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def gen_plot(dates, numbers, label=None, file_name='plot.png', show_plot=True): - # pip install matplotlib - import matplotlib.pyplot as plt +import os.path - from matplotlib import rcParams - rcParams['font.family'] = 'Times New Roman', 'Arial', 'Tahoma' - rcParams['font.fantasy'] = 'Times New Roman' - rcParams['axes.labelsize'] = 'large' - rcParams['savefig.dpi'] = 200 +# pip install matplotlib +import matplotlib.pyplot as plt + +import requests + +from bs4 import BeautifulSoup +from matplotlib import rcParams +from matplotlib.dates import DateFormatter + + +def gen_plot(dates, numbers, label=None, file_name="plot.png", show_plot=True): + rcParams["font.family"] = "Times New Roman", "Arial", "Tahoma" + rcParams["font.fantasy"] = "Times New Roman" + rcParams["axes.labelsize"] = "large" + rcParams["savefig.dpi"] = 200 rcParams["axes.grid"] = True - from matplotlib.dates import DateFormatter - x_axis_date_formatter = DateFormatter('%d/%m/%y') + x_axis_date_formatter = DateFormatter("%d/%m/%y") # ax = plt.subplot(211) ax = plt.subplot(111) @@ -42,40 +49,37 @@ def gen_plot(dates, numbers, label=None, file_name='plot.png', show_plot=True): return file_name -if __name__ == '__main__': +if __name__ == "__main__": from datetime import date, datetime - date_req1 = '01.01.2000' - date_req2 = date.today().strftime('%d.%m.%Y') + date_req1 = "01.01.2000" + date_req2 = date.today().strftime("%d.%m.%Y") - file_name = 'metall_{}-{}.xml'.format(date_req1, date_req2) + file_name = f"metall_{date_req1}-{date_req2}.xml" - # Кеширование. Если файла нет, то скачиваем его. - import os.path + # Кеширование. Если файла нет, то скачиваем его if not os.path.exists(file_name): - url = 'http://www.cbr.ru/scripts/xml_metall.asp?date_req1={}&date_req2={}'.format(date_req1, date_req2) + url = f"http://www.cbr.ru/scripts/xml_metall.asp?date_req1={date_req1}&date_req2={date_req2}" - with open(file_name, 'w') as f: - import requests + with open(file_name, "w") as f: rs = requests.get(url) f.write(rs.text) - with open(file_name, 'rb') as f: - from bs4 import BeautifulSoup + with open(file_name, "rb") as f: root = BeautifulSoup(f, "xml") # Code="1" -- Золото # Code="2" -- Серебро # Code="3" -- Платина # Code="4" -- Палладий - records = root.find_all('Record', attrs=dict(Code="1")) + records = root.find_all("Record", attrs=dict(Code="1")) dates = list() prices = list() for record in records: - date = datetime.strptime(record["Date"], '%d.%m.%Y').date() - price = float(record.findChild("Buy").text.replace(',', '.')) + date = datetime.strptime(record["Date"], "%d.%m.%Y").date() + price = float(record.findChild("Buy").text.replace(",", ".")) dates.append(date) prices.append(price) @@ -84,9 +88,13 @@ def gen_plot(dates, numbers, label=None, file_name='plot.png', show_plot=True): for i, record in enumerate((records[0], records[-1]), 1): # b'231,94246,67\n\n' # record["Code"] - print('{}. {}: {}'.format(i, record["Date"], record.findChild("Buy").text)) - - metall = root.findChild('Metall') - from_date = datetime.strptime(metall['FromDate'], '%Y%m%d').strftime('%d.%m.%Y') - to_date = datetime.strptime(metall['ToDate'], '%Y%m%d').strftime('%d.%m.%Y') - gen_plot(dates, prices, "Стоимость грамма золота в рублях за {} - {}".format(from_date, to_date)) + print(f"{i}. {record['Date']}: {record.findChild('Buy').text}") + + metall = root.findChild("Metall") + from_date = datetime.strptime(metall["FromDate"], "%Y%m%d").strftime("%d.%m.%Y") + to_date = datetime.strptime(metall["ToDate"], "%Y%m%d").strftime("%d.%m.%Y") + gen_plot( + dates, + prices, + f"Стоимость грамма золота в рублях за {from_date} - {to_date}", + ) diff --git a/chardet__examples/hello_world.py b/chardet__examples/hello_world.py index eec9012df..3e128b58a 100644 --- a/chardet__examples/hello_world.py +++ b/chardet__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pathlib import Path @@ -13,7 +13,7 @@ DIR = Path(__file__).resolve().parent -for path in DIR.glob('*.txt'): +for path in DIR.glob("*.txt"): raw_data = path.read_bytes() print(path.name, chardet.detect(raw_data)) diff --git a/check_internet.py b/check_internet.py new file mode 100644 index 000000000..121b3848a --- /dev/null +++ b/check_internet.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://stackoverflow.com/a/33117579/5909792 + + +import socket + + +DEFAULT_HOST: str = "8.8.8.8" +DEFAULT_PORT: int = 53 +DEFAULT_TIMEOUT: int = 3 + + +def check_internet( + host: str = DEFAULT_HOST, + port: int = DEFAULT_PORT, + timeout: int = DEFAULT_TIMEOUT, +) -> bool: + """ + Host: 8.8.8.8 (google-public-dns-a.google.com) + OpenPort: 53/tcp + Service: domain (DNS/TCP) + """ + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(timeout) + s.connect((host, port)) + return True + except socket.error as e: + print(f"Error: {e}") + return False + + +if __name__ == "__main__": + import argparse + import time + + parser = argparse.ArgumentParser( + description="Check internet", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "--host", + default=DEFAULT_HOST, + help="Host", + ) + parser.add_argument( + "--port", + type=int, + default=DEFAULT_PORT, + help="Port", + ) + parser.add_argument( + "--network_timeout_secs", + type=int, + default=DEFAULT_TIMEOUT, + help="Network timeout in seconds", + ) + parser.add_argument( + "--attempts", + type=int, + default=3, + help="Attempts to check", + ) + parser.add_argument( + "--delay_between_attempts_secs", + type=int, + default=30, + help="Delay between attempts in seconds", + ) + + args = parser.parse_args() + + for attempt in range(args.attempts): + prefix: str = f"[{attempt + 1}] " + + print(f"{prefix}Check internet") + result: bool = check_internet( + host=args.host, + port=args.port, + timeout=args.network_timeout_secs, + ) + print(f"{prefix}Result: {result}") + if result: + break + + delay: int = args.delay_between_attempts_secs + + print(f"{prefix}The next attempt through {delay} seconds\n") + time.sleep(delay) diff --git a/class_with_my_instances_property.py b/class_with_my_instances_property.py index 0841a0399..e79699e6a 100644 --- a/class_with_my_instances_property.py +++ b/class_with_my_instances_property.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" class MyMeta(type): @@ -29,4 +29,5 @@ class MyClass(object, metaclass=MyMeta): print(MyClass.instances) # [<__main__.MyClass object at 0x00514AF0>] b = MyClass() c = MyClass() -print(MyClass.instances) # [<__main__.MyClass object at 0x00514AF0>, <__main__.MyClass object at 0x00736DF0>, <__main__.MyClass object at 0x00736C30>] +print(MyClass.instances) +# [<__main__.MyClass object at 0x00514AF0>, <__main__.MyClass object at 0x00736DF0>, <__main__.MyClass object at 0x00736C30>] diff --git a/clipboard_echo_with_clip.py b/clipboard_echo_with_clip.py index 69eea8d3f..b541a2017 100644 --- a/clipboard_echo_with_clip.py +++ b/clipboard_echo_with_clip.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os from tkinter import Tk # Python 3 -os.system('echo 123|clip') + +os.system("echo 123|clip") print(repr(Tk().clipboard_get())) # '123\n' diff --git a/closure__examples/counter.py b/closure__examples/counter.py index b26f68708..cdfe22053 100644 --- a/closure__examples/counter.py +++ b/closure__examples/counter.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/141426 -def make_counter(start=0, step=1): +def make_counter(start: int | float = 0, step: int | float = 1): i = start def counter(): # counter() is a closure @@ -19,10 +19,10 @@ def counter(): # counter() is a closure return counter -def make_counter_adder(start=0): +def make_counter_adder(start: int | float = 0): i = start - def counter(step=0): # counter() is a closure + def counter(step: int | float = 0): # counter() is a closure nonlocal i i += step @@ -31,7 +31,7 @@ def counter(step=0): # counter() is a closure return counter -if __name__ == '__main__': +if __name__ == "__main__": c = make_counter() print(c(), c(), c()) # 1 2 3 diff --git a/closure__examples/hello_world.py b/closure__examples/hello_world.py index 84781ecce..f608cfadc 100644 --- a/closure__examples/hello_world.py +++ b/closure__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.wikipedia.org/wiki/Замыкание_(программирование) diff --git a/codewars/Error_correction_1__Hamming_Code.py b/codewars/Error_correction_1__Hamming_Code.py new file mode 100644 index 000000000..8e169fa12 --- /dev/null +++ b/codewars/Error_correction_1__Hamming_Code.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://www.codewars.com/kata/5ef9ca8b76be6d001d5e1c3e/train/python + + +def chunks(l, n): + """Yield successive n-sized chunks from l.""" + for i in range(0, len(l), n): + yield l[i : i + n] + + +# Task 1: Encode function +# +# Implement the encode function, using the following steps: +# - convert every letter of the text to its ASCII value; +# - convert ASCII values to 8-bit binary; +# - triple every bit; +# - concatenate the result; +# +# For example: +# input: "hey" +# --> 104, 101, 121 // ASCII values +# --> 01101000, 01100101, 01111001 // binary +# --> 000111111000111000000000 000111111000000111000111 000111111111111000000111 // tripled +# --> "000111111000111000000000000111111000000111000111000111111111111000000111" // concatenated +def encode(text: str) -> str: + return "".join( + "".join(b * 3 for b in f"{ord(c):08b}") for c in text # Tripled bits + ) + + +# Task 2: Decode function: +# Check if any errors happened and correct them. Errors will be only bit flips, and not a loss of bits: +# - 111 --> 101 : this can and will happen +# - 111 --> 11 : this cannot happen +# +# Note: the length of the input string is also always divisible by 24 so that you can convert it to an ASCII value. +# +# Steps: +# - Split the input into groups of three characters; +# - Check if an error occurred: replace each group with the character that occurs most often, +# e.g. 010 --> 0, 110 --> 1, etc; +# - Take each group of 8 characters and convert that binary number; +# - Convert the binary values to decimal (ASCII); +# - Convert the ASCII values to characters and concatenate the result +# +# For example: +# input: "100111111000111001000010000111111000000111001111000111110110111000010111" +# --> 100, 111, 111, 000, 111, 001, ... // triples +# --> 0, 1, 1, 0, 1, 0, ... // corrected bits +# --> 01101000, 01100101, 01111001 // bytes +# --> 104, 101, 121 // ASCII values +# --> "hey" +def decode(bits: str) -> str: + bit_items = [] + for tripled_bits in chunks(bits, 3): + sums = sum(map(int, tripled_bits)) + + # Example: 110 or 111 -> 1 and 000 -> 0 or 001 -> 0 + bit_items.append("1" if sums == 2 or sums == 3 else "0") + + binary = "".join(bit_items) + + items = [] + for byte in chunks(binary, 8): + items.append(chr(int(byte, 2))) + + return "".join(items) + + +if __name__ == "__main__": + text = "hey" + encoded = encode(text) + print(encoded) + assert ( + encoded + == "000111111000111000000000000111111000000111000111000111111111111000000111" + ) + + decoded = decode(encoded) + print(decoded) + assert text == decoded + + invalid_encoded = "100111111000111001000010000111111000000111001111000111110110111000010111" + decoded = decode(invalid_encoded) + print(decoded) + assert text == decoded diff --git a/codewars/parse_molecule__Molecule to atoms.py b/codewars/parse_molecule__Molecule to atoms.py new file mode 100644 index 000000000..3484bc9eb --- /dev/null +++ b/codewars/parse_molecule__Molecule to atoms.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +""" +My solution from codewars. + +""" + +import re +from collections import defaultdict + + +def get_sub_formula(formula): + """ + "(SO3)2" -> ("(SO3)2", "(SO3)", "2") + "(SO3)" -> ("(SO3)", "(SO3)", "") + + """ + + match = re.search(r"(\([a-zA-Z\d]+\))(\d*)", formula) + if not match: + return + + full_sub_formula = match.group(0) + sub_formula = match.group(1) + multiplier = match.group(2) + + return full_sub_formula, sub_formula, multiplier + + +def split_by_full_tokens(formula): + """ + 'Fe7S10FBO3' -> ['Fe7', 'S10', 'F', 'B', 'O3'] + + """ + + return re.findall(r"([A-Z][a-z]*\d*)", formula) + + +def split_by_tokens(formula): + """ + 'Fe7S10F1B1O3' -> ['Fe', '7', 'S', '10', 'F', '1', 'B', '1', 'O', '3'] + + """ + + return re.findall(r"([A-Z][a-z]*|\d+)", formula) + + +def complete_element_atom_number_with_one_atom(formula): + """ + Append '1' for elements without atom number: 'FeK' -> 'Fe1K1' + + """ + + tokens = split_by_full_tokens(formula) + tokens = [token if token[-1] in "0123456789" else token + "1" for token in tokens] + return "".join(tokens) + + +def parse_molecule(formula): + formula = formula.replace("[", "(").replace("]", ")") + formula = formula.replace("{", "(").replace("}", ")") + + while "(" in formula: + match = get_sub_formula(formula) + if not match: + break + + full_sub_formula, sub_formula, multiplier = match + new_sub_formula = complete_element_atom_number_with_one_atom(sub_formula) + + if multiplier: + multiplier = int(multiplier) + + tokens = split_by_tokens(new_sub_formula) + + # ['K', '4', 'O', '2'] * 2 -> ['K', '8', 'O', '4'] + tokens = [ + str(int(token) * multiplier) if token.isdigit() else token + for token in tokens + ] + new_sub_formula = "".join(tokens) + + formula = formula.replace(full_sub_formula, new_sub_formula) + + formula = complete_element_atom_number_with_one_atom(formula) + tokens = split_by_tokens(formula) + + element_by_number_atom = defaultdict(int) + for i in range(0, len(tokens), 2): + element_by_number_atom[tokens[i]] += int(tokens[i + 1]) + + return element_by_number_atom + + +if __name__ == "__main__": + def equals_atomically(obj1, obj2) -> bool: + if len(obj1) != len(obj2): + return False + for k in obj1: + if obj1[k] != obj2[k]: + return False + return True + + assert equals_atomically( + parse_molecule("H2O"), {"H": 2, "O": 1} + ), "Should parse water" + assert equals_atomically( + parse_molecule("Mg(OH)2"), {"Mg": 1, "O": 2, "H": 2} + ), "Should parse magnesium hydroxide: Mg(OH)2" + assert equals_atomically( + parse_molecule("K4[ON(SO3)2]2"), {"K": 4, "O": 14, "N": 2, "S": 4} + ), "Should parse Fremy's salt: K4[ON(SO3)2]2" diff --git a/codewars_Error_correction_1__Hamming_Code.py b/codewars_Error_correction_1__Hamming_Code.py deleted file mode 100644 index b2619e5f8..000000000 --- a/codewars_Error_correction_1__Hamming_Code.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# SOURCE: https://www.codewars.com/kata/5ef9ca8b76be6d001d5e1c3e/train/python - - -def chunks(l, n): - """Yield successive n-sized chunks from l.""" - for i in range(0, len(l), n): - yield l[i: i + n] - - -# Task 1: Encode function -# -# Implement the encode function, using the following steps: -# - convert every letter of the text to its ASCII value; -# - convert ASCII values to 8-bit binary; -# - triple every bit; -# - concatenate the result; -# -# For example: -# input: "hey" -# --> 104, 101, 121 // ASCII values -# --> 01101000, 01100101, 01111001 // binary -# --> 000111111000111000000000 000111111000000111000111 000111111111111000000111 // tripled -# --> "000111111000111000000000000111111000000111000111000111111111111000000111" // concatenated -def encode(text: str) -> str: - return ''.join( - ''.join(b * 3 for b in f"{ord(c):08b}") # Tripled bits - for c in text - ) - - -# Task 2: Decode function: -# Check if any errors happened and correct them. Errors will be only bit flips, and not a loss of bits: -# - 111 --> 101 : this can and will happen -# - 111 --> 11 : this cannot happen -# -# Note: the length of the input string is also always divisible by 24 so that you can convert it to an ASCII value. -# -# Steps: -# - Split the input into groups of three characters; -# - Check if an error occurred: replace each group with the character that occurs most often, -# e.g. 010 --> 0, 110 --> 1, etc; -# - Take each group of 8 characters and convert that binary number; -# - Convert the binary values to decimal (ASCII); -# - Convert the ASCII values to characters and concatenate the result -# -# For example: -# input: "100111111000111001000010000111111000000111001111000111110110111000010111" -# --> 100, 111, 111, 000, 111, 001, ... // triples -# --> 0, 1, 1, 0, 1, 0, ... // corrected bits -# --> 01101000, 01100101, 01111001 // bytes -# --> 104, 101, 121 // ASCII values -# --> "hey" -def decode(bits: str) -> str: - bit_items = [] - for tripled_bits in chunks(bits, 3): - sums = sum(map(int, tripled_bits)) - - # Example: 110 or 111 -> 1 and 000 -> 0 or 001 -> 0 - bit_items.append('1' if sums == 2 or sums == 3 else '0') - - binary = ''.join(bit_items) - - items = [] - for byte in chunks(binary, 8): - items.append(chr(int(byte, 2))) - - return ''.join(items) - - -if __name__ == '__main__': - text = 'hey' - encoded = encode(text) - print(encoded) - assert encoded == '000111111000111000000000000111111000000111000111000111111111111000000111' - - decoded = decode(encoded) - print(decoded) - assert text == decoded - - invalid_encoded = '100111111000111001000010000111111000000111001111000111110110111000010111' - decoded = decode(invalid_encoded) - print(decoded) - assert text == decoded diff --git a/codingame/easy/ASCII Art.py b/codingame/easy/ASCII Art.py index 31dfb87ce..c0a2e55b0 100644 --- a/codingame/easy/ASCII Art.py +++ b/codingame/easy/ASCII Art.py @@ -1,14 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys -import math - from string import ascii_lowercase + # Auto-generated code below aims at helping you parse # the standard input according to the problem statement. @@ -19,27 +18,27 @@ print(l, file=sys.stderr) print(h, file=sys.stderr) print(t, file=sys.stderr) -print('\n', file=sys.stderr) +print("\n", file=sys.stderr) ascii_rows = [input() for i in range(h)] -print('\n'.join(ascii_rows), file=sys.stderr) -print('\n', file=sys.stderr) +print("\n".join(ascii_rows), file=sys.stderr) +print("\n", file=sys.stderr) -letter_list = list(ascii_lowercase) + ['?'] +letter_list = list(ascii_lowercase) + ["?"] result_rows = list() for row in ascii_rows: - new_row = '' + new_row = "" for c in t.lower(): if c not in letter_list: - c = '?' + c = "?" index = letter_list.index(c) pos = index * l - new_row += row[pos: pos + l] + new_row += row[pos : pos + l] result_rows.append(new_row) -print('\n'.join(result_rows)) +print("\n".join(result_rows)) diff --git a/codingame/easy/Chuck Norris.py b/codingame/easy/Chuck Norris.py index fd97082da..3293d6034 100644 --- a/codingame/easy/Chuck Norris.py +++ b/codingame/easy/Chuck Norris.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys -import math + # Auto-generated code below aims at helping you parse # the standard input according to the problem statement. @@ -21,25 +21,27 @@ def Chuck_Norris_encode(text): last_c = text[0] count = 1 - result = '00 ' if last_c == '0' else '0 ' + result = "00 " if last_c == "0" else "0 " for i in range(len(text))[1:]: c = text[i] if c != last_c: - result += '0' * count + ' ' + result += "0" * count + " " count = 1 - result += '00 ' if c == '0' else '0 ' + result += "00 " if c == "0" else "0 " else: count += 1 last_c = c - result += '0' * count + result += "0" * count return result -# Бинарная строка должна содержать 7 символов, а bin в может вернуть -# строку меньшего размера, убрав ненужные нули -text = ''.join([bin(ord(c))[2:].rjust(7, '0') for c in message]) -print(Chuck_Norris_encode(text)) + +if __name__ == '__main__': + # Бинарная строка должна содержать 7 символов, а bin в может вернуть + # строку меньшего размера, убрав ненужные нули + text = "".join([bin(ord(c))[2:].rjust(7, "0") for c in message]) + print(Chuck_Norris_encode(text)) diff --git a/codingame/easy/Horse-racing Duals.py b/codingame/easy/Horse-racing Duals.py index 5b47f8166..d792a6a9a 100644 --- a/codingame/easy/Horse-racing Duals.py +++ b/codingame/easy/Horse-racing Duals.py @@ -1,12 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import sys -import math - # Auto-generated code below aims at helping you parse # the standard input according to the problem statement. @@ -25,4 +22,3 @@ last = i print(min_strength) - diff --git a/codingame/easy/MIME Type.py b/codingame/easy/MIME Type.py index f0327f613..9a4e15de7 100644 --- a/codingame/easy/MIME Type.py +++ b/codingame/easy/MIME Type.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys -import math + # Auto-generated code below aims at helping you parse # the standard input according to the problem statement. @@ -36,8 +36,8 @@ for file in files: try: - i = file.rindex('.') - ext = file[i + 1:].lower() + i = file.rindex(".") + ext = file[i + 1 :].lower() print(ext_mime_dict[ext]) except: print("UNKNOWN") diff --git a/codingame/easy/Mars Lander - Episode 1.py b/codingame/easy/Mars Lander - Episode 1.py index 534852efe..049b90086 100644 --- a/codingame/easy/Mars Lander - Episode 1.py +++ b/codingame/easy/Mars Lander - Episode 1.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys -import math + # Auto-generated code below aims at helping you parse # the standard input according to the problem statement. @@ -26,12 +26,16 @@ # rotate: the rotation angle in degrees (-90 to 90). # power: the thrust power (0 to 4). x, y, h_speed, v_speed, fuel, rotate, power = [int(i) for i in input().split()] - print("x={}, y={}, h_speed={}, v_speed={}, fuel={}, rotate={}, power={}".format(x, y, h_speed, v_speed, fuel, rotate, power), file=sys.stderr) + print( + f"x={x}, y={y}, h_speed={h_speed}, v_speed={v_speed}, fuel={fuel}, rotate={rotate}, power={power}", + file=sys.stderr, + ) # Write an action using print # To debug: print("Debug messages...", file=sys.stderr) - # 2 integers: rotate power. rotate is the desired rotation angle (should be 0 for level 1), power is the desired thrust power (0 to 4). + # 2 integers: rotate power. rotate is the desired rotation angle + # (should be 0 for level 1), power is the desired thrust power (0 to 4). if v_speed < -40: print("0 4") else: diff --git a/codingame/easy/Onboarding.py b/codingame/easy/Onboarding.py index c0cd45016..b3d3920c7 100644 --- a/codingame/easy/Onboarding.py +++ b/codingame/easy/Onboarding.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # game loop diff --git a/codingame/easy/Power of Thor - Episode 1.py b/codingame/easy/Power of Thor - Episode 1.py index df07b9e88..35f920cbf 100644 --- a/codingame/easy/Power of Thor - Episode 1.py +++ b/codingame/easy/Power of Thor - Episode 1.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -19,15 +19,15 @@ light_x, light_y, initial_tx, initial_ty = [int(i) for i in input().split()] DIRECTION_DICT = { - (-1, 0): 'W', - (0, -1): 'N', - (1, 0): 'E', - (0, 1): 'S', - - (-1, -1): 'NW', - (1, -1): 'NE', - (1, 1): 'SE', - (-1, 1): 'SW', + (-1, 0): "W", + (0, -1): "N", + (1, 0): "E", + (0, 1): "S", + + (-1, -1): "NW", + (1, -1): "NE", + (1, 1): "SE", + (-1, 1): "SW", } x, y = initial_tx, initial_ty @@ -36,7 +36,8 @@ # game loop while True: - remaining_turns = int(input()) # The remaining amount of turns Thor can move. Do not remove this line. + # The remaining amount of turns Thor can move. Do not remove this line. + remaining_turns = int(input()) # Write an action using print # To debug: print("Debug messages...", file=sys.stderr) diff --git a/codingame/easy/Temperatures.py b/codingame/easy/Temperatures.py index 94ec65529..aa3f60402 100644 --- a/codingame/easy/Temperatures.py +++ b/codingame/easy/Temperatures.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys -import math + # Auto-generated code below aims at helping you parse # the standard input according to the problem statement. @@ -14,7 +14,7 @@ temps = input() # the n temperatures expressed as integers ranging from -273 to 5526 temps = [int(i) for i in temps.strip().split()] -print('temps:', temps, file=sys.stderr) +print("temps:", temps, file=sys.stderr) # Проверка того, что 0 уже есть в списке или список температор пустой if 0 in temps or not temps: diff --git a/codingame/easy/The Descent.py b/codingame/easy/The Descent.py index 8b69326cd..b78b1c7ca 100644 --- a/codingame/easy/The Descent.py +++ b/codingame/easy/The Descent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # game loop diff --git a/codingame/easy/defibrillators.py b/codingame/easy/defibrillators.py index 033d88267..c3feb2513 100644 --- a/codingame/easy/defibrillators.py +++ b/codingame/easy/defibrillators.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import sys import math + # Auto-generated code below aims at helping you parse # the standard input according to the problem statement. -str_to_float = lambda x: float(x.replace(',', '.')) +str_to_float = lambda x: float(x.replace(",", ".")) lon = str_to_float(input()) lat = str_to_float(input()) @@ -22,7 +22,7 @@ n = int(input()) for i in range(n): - defib = input().split(';') + defib = input().split(";") name = defib[1] lon_defib = str_to_float(defib[-2]) diff --git a/codingame/medium/Bender - Episode 1.py b/codingame/medium/Bender - Episode 1.py index 835682990..bd97edcc7 100644 --- a/codingame/medium/Bender - Episode 1.py +++ b/codingame/medium/Bender - Episode 1.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys -import math + # Auto-generated code below aims at helping you parse # the standard input according to the problem statement. @@ -19,38 +19,38 @@ city_map.append(list(row)) -print('city_map:', city_map, file=sys.stderr) +print("city_map:", city_map, file=sys.stderr) # Write an action using print # To debug: print("Debug messages...", file=sys.stderr) DIRECTION_DICT = { - 'SOUTH': (1, 0), - 'EAST': (0, 1), - 'NORTH': (-1, 0), - 'WEST': (0, -1), - - (1, 0): 'SOUTH', - (0, 1): 'EAST', - (-1, 0): 'NORTH', - (0, -1): 'WEST', - - 'S': 'SOUTH', - 'E': 'EAST', - 'N': 'NORTH', - 'W': 'WEST', + "SOUTH": (1, 0), + "EAST": (0, 1), + "NORTH": (-1, 0), + "WEST": (0, -1), + + (1, 0): "SOUTH", + (0, 1): "EAST", + (-1, 0): "NORTH", + (0, -1): "WEST", + + "S": "SOUTH", + "E": "EAST", + "N": "NORTH", + "W": "WEST", } DEBUG = False -def log(*args, **kwargs): +def log(*args, **kwargs) -> None: DEBUG and print(*args, **kwargs) class Bender: - def __init__(self, city_map): + def __init__(self, city_map) -> None: self.objects_map = dict() # Соберем все объекты на карте в словарь, исключаются пустые места и стенки @@ -59,18 +59,18 @@ def __init__(self, city_map): for j in range(len(row)): cell = city_map[i][j] - if cell == '@': + if cell == "@": self.objects_map[cell] = i, j # Под Бендером пустая клетка - self.objects_map[i, j] = ' ' + self.objects_map[i, j] = " " else: self.objects_map[i, j] = cell - log('Objects map:', self.objects_map) - log('Bender pos: {}x{}'.format(self.pos_i, self.pos_j)) + log("Objects map:", self.objects_map) + log(f"Bender pos: {self.pos_i}x{self.pos_j}") - self.direction_name = 'SOUTH' + self.direction_name = "SOUTH" self.invert = False self.breaker = False @@ -81,7 +81,7 @@ def __init__(self, city_map): self.steps = list() # self.steps_log_list = list() - def _set_pos_i(self, value): + def _set_pos_i(self, value) -> None: # Устанавливаем i, и старое j self.pos = value, self.pos[1] @@ -90,7 +90,7 @@ def _get_pos_i(self): pos_i = property(_get_pos_i, _set_pos_i) - def _set_pos_j(self, value): + def _set_pos_j(self, value) -> None: # Устанавливаем старое i и j self.pos = self.pos[0], value @@ -99,11 +99,11 @@ def _get_pos_j(self): pos_j = property(_get_pos_j, _set_pos_j) - def _set_pos(self, value): - self.objects_map['@'] = value + def _set_pos(self, value) -> None: + self.objects_map["@"] = value def _get_pos(self): - return self.objects_map['@'] + return self.objects_map["@"] pos = property(_get_pos, _set_pos) @@ -116,17 +116,17 @@ def city_map(self): # Добавляем Бендера i, j = self.pos - map[i][j] = '@' + map[i][j] = "@" return map - def print_city_map(self): + def print_city_map(self) -> None: log() for row in self.city_map(): - log(*row, sep='') + log(*row, sep="") log() - def _set_direction_name(self, name): + def _set_direction_name(self, name) -> None: self._direction_name = name def _get_direction_name(self): @@ -144,40 +144,40 @@ def get_direction(direction_name): def look_around(self): return { - 'SOUTH': self.objects_map[self.pos_i + 1, self.pos_j], - 'EAST': self.objects_map[self.pos_i, self.pos_j + 1], - 'NORTH': self.objects_map[self.pos_i - 1, self.pos_j], - 'WEST': self.objects_map[self.pos_i, self.pos_j - 1], + "SOUTH": self.objects_map[self.pos_i + 1, self.pos_j], + "EAST": self.objects_map[self.pos_i, self.pos_j + 1], + "NORTH": self.objects_map[self.pos_i - 1, self.pos_j], + "WEST": self.objects_map[self.pos_i, self.pos_j - 1], } def step(self): self.print_city_map() - log('Objects map:', self.objects_map) + log("Objects map:", self.objects_map) look_around = self.look_around() - log("look_around: {} {}x{}".format(look_around, self.pos_i, self.pos_j)) - log('Current direction:', self.direction_name) + log(f"look_around: {look_around} {self.pos_i}x{self.pos_j}") + log("Current direction:", self.direction_name) # Приоритеты смены движения при встрече с препятствием: # invert=False: SOUTH -> EAST -> NORTH -> WEST # invert=True: WEST -> NORTH -> EAST -> SOUTH - priorities = ['SOUTH', 'EAST', 'NORTH', 'WEST'] + priorities = ["SOUTH", "EAST", "NORTH", "WEST"] if self.invert: priorities.reverse() - log('Current priorities:', priorities) + log("Current priorities:", priorities) priorities.remove(self.direction_name) while look_around: next_cell = look_around.pop(self.direction_name) - log('while look_around: {} "{}" {}'.format(self.direction_name, next_cell, look_around)) + log(f'while look_around: {self.direction_name} "{next_cell}" {look_around}') # Если следующий шаг не в препятствие, или препятствие и Бендер в режиме breaker - if next_cell not in ['#', 'X'] or (next_cell == 'X' and self.breaker): + if next_cell not in ["#", "X"] or (next_cell == "X" and self.breaker): break new_direction_name = priorities.pop(0) - log('look_around change direction: {} -> {}.'.format(self.direction_name, new_direction_name)) + log(f"look_around change direction: {self.direction_name} -> {new_direction_name}.") self.direction_name = new_direction_name di, dj = self.get_direction(self.direction_name) @@ -186,8 +186,8 @@ def step(self): self.pos_j += dj # Ломаем препятствие - if next_cell == 'X' and self.breaker: - self.objects_map[self.pos] = ' ' + if next_cell == "X" and self.breaker: + self.objects_map[self.pos] = " " current_cell = self.objects_map[self.pos] @@ -195,24 +195,26 @@ def step(self): self.steps.append(step) # Проверяем, что наступили на изменение шага - if current_cell in ['S', 'E', 'N', 'W']: + if current_cell in ["S", "E", "N", "W"]: # Указываем следующее направление движения self.direction_name = DIRECTION_DICT[current_cell] # Если попали на инвертирование приоритетов - elif current_cell == 'I': + elif current_cell == "I": self.invert = not self.invert # Если нашли пиво - elif current_cell == 'B': + elif current_cell == "B": self.breaker = not self.breaker - log('breaker:', self.breaker) + log("breaker:", self.breaker) # Если нашли телепорт - elif current_cell == 'T': + elif current_cell == "T": # Найдем положение другого телепорта - other_teleport_pos = [k for k, v in self.objects_map.items() if v == 'T' and k != self.pos][0] - log('teleport: {} -> {}'.format(self.pos, other_teleport_pos)) + other_teleport_pos = [ + k for k, v in self.objects_map.items() if v == "T" and k != self.pos + ][0] + log(f"teleport: {self.pos} -> {other_teleport_pos}") # Телепортируемся self.pos = other_teleport_pos @@ -225,7 +227,9 @@ def step(self): # hack for LOOP max_step_number = 200 -steps = ['LOOP'] +steps = [ + "LOOP", +] # Ходим, пока не встретим символ '$' while True: @@ -233,8 +237,8 @@ def step(self): if max_step_number <= 0: break - if bender.step() == '$': + if bender.step() == "$": steps = bender.steps break -print('\n'.join(steps)) +print("\n".join(steps)) diff --git a/codingame/medium/There is no Spoon - Episode 1.py b/codingame/medium/There is no Spoon - Episode 1.py index 88e32752e..1e69eeb9e 100644 --- a/codingame/medium/There is no Spoon - Episode 1.py +++ b/codingame/medium/There is no Spoon - Episode 1.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys -import math + # Don't let the machines win. You are humanity's last hope... @@ -18,17 +18,17 @@ for i in range(height): line = input() # width characters, each either 0 or . - print("line: {}".format(line), file=sys.stderr) + print(f"line: {line}", file=sys.stderr) matrix.append(list(line)) -print("matrix:\n{}".format(matrix), file=sys.stderr) +print(f"matrix:\n{matrix}", file=sys.stderr) # Ищем соседей справа def right_neighbor(matrix, i, j): for _j in range(j + 1, width): - if matrix[i][_j] == '0': + if matrix[i][_j] == "0": return _j, i return -1, -1 @@ -37,7 +37,7 @@ def right_neighbor(matrix, i, j): # Ищем соседей снизу def bottom_neighbor(matrix, i, j): for _i in range(i + 1, height): - if matrix[_i][j] == '0': + if matrix[_i][j] == "0": return j, _i return -1, -1 @@ -45,7 +45,7 @@ def bottom_neighbor(matrix, i, j): for i in range(height): for j in range(width): - if matrix[i][j] == '.': + if matrix[i][j] == ".": continue x2, y2 = right_neighbor(matrix, i, j) diff --git a/collector_bash_im/collector_bash_im.py b/collector_bash_im/collector_bash_im.py index c82b373d8..eddb6ae3c 100644 --- a/collector_bash_im/collector_bash_im.py +++ b/collector_bash_im/collector_bash_im.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт собирает цитаты сайта bash.im""" @@ -9,26 +9,21 @@ import logging import sys -import time -import traceback - -from datetime import datetime - import requests -import sqlalchemy.exc +from bs4 import BeautifulSoup from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, DateTime from sqlalchemy.orm import mapper, sessionmaker -from bs4 import BeautifulSoup - -def get_logger(name, file='log.txt', encoding='utf8'): +def get_logger(name, file="log.txt", encoding="utf8"): log = logging.getLogger(name) log.setLevel(logging.DEBUG) - formatter = logging.Formatter('[%(asctime)s] %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s') + formatter = logging.Formatter( + "[%(asctime)s] %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s" + ) fh = logging.FileHandler(file, encoding=encoding) fh.setLevel(logging.DEBUG) @@ -44,41 +39,45 @@ def get_logger(name, file='log.txt', encoding='utf8'): return log -logger = get_logger('collector_bash_im') +logger = get_logger("collector_bash_im") class Quote: - def __init__(self, id, date, rating, text): + def __init__(self, id, date, rating, text) -> None: self.id = id self.date = date self.rating = rating self.text = text - def __repr__(self): - return "".format(len(self.text), **self.__dict__) + def __repr__(self) -> str: + return ( + "".format(len(self.text), **self.__dict__) + ) @property def url(self): - return 'http://bash.im/quote/' + str(self.id) + return "http://bash.im/quote/" + str(self.id) def get_session_factory(): # Создаем базу, включаем логирование и автообновление подключения каждые 2 часа (7200 секунд) engine = create_engine( # 'sqlite:///:memory:', - 'sqlite:///quotes.db', + "sqlite:///quotes.db", # echo=True, - pool_recycle=7200 + pool_recycle=7200, ) metadata = MetaData() - quotes_table = Table('Quote', metadata, - Column('id', Integer, primary_key=True), - Column('date', DateTime), - Column('rating', Integer), - Column('text', String), + quotes_table = Table( + "Quote", + metadata, + Column("id", Integer, primary_key=True), + Column("date", DateTime), + Column("rating", Integer), + Column("text", String), ) mapper(Quote, quotes_table) @@ -93,14 +92,14 @@ def get_session_factory(): query = session.query(Quote) -if __name__ == '__main__': - rs = requests.get('http://bash.im') +if __name__ == "__main__": + rs = requests.get("http://bash.im") soup = BeautifulSoup(rs.content, "lxml") # print(soup) i = 0 for quote in soup.find_all(attrs={"class": "quote"}): text = quote.find(attrs={"class": "text"}) - if text is None: + if not text: continue i += 1 diff --git a/colorama__examples/hello_world.py b/colorama__examples/hello_world.py new file mode 100644 index 000000000..a224b7fd2 --- /dev/null +++ b/colorama__examples/hello_world.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install colorama +from colorama import Fore, Back, Style + + +print(Fore.RED + "some red text") +print(Back.GREEN + "and with a green background") +print(Style.DIM + "and in dim text") +print(Style.RESET_ALL) +print("back to normal now") diff --git a/combinatorics.py b/combinatorics.py index f3df6512a..9941b8cdf 100644 --- a/combinatorics.py +++ b/combinatorics.py @@ -1,19 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -"""У нас есть список сил и возможно комбинировать одновременно только две разные силы, -причем повторов быть не должно -- ('Огонь', 'Молния') и ('Молния', 'Огонь') -- повторы.""" +""" +У нас есть список сил и возможно комбинировать одновременно только две разные силы, +причем повторов быть не должно -- ('Огонь', 'Молния') и ('Молния', 'Огонь') -- повторы. +""" + import itertools + # Комбинации сил, максимум за раз могут две учавствовать, плюс возможны только разные -powers = ['Огонь', 'Молния', 'Лед', 'Воздух'] +powers = ["Огонь", "Молния", "Лед", "Воздух"] # Все комбинации без повторов # Если нужны комбинации с повторами, используется itertools.product(powers, repeat=2) all_combo = itertools.combinations(powers, 2) for i, combo in enumerate(all_combo, 1): - print('{}. {} и {}'.format(i, *combo)) + print("{}. {} и {}".format(i, *combo)) diff --git a/combinatorics__generate__repeat__itertools__from_000_to_111.py b/combinatorics__generate__repeat__itertools__from_000_to_111.py index 3817668b3..261ab88f8 100644 --- a/combinatorics__generate__repeat__itertools__from_000_to_111.py +++ b/combinatorics__generate__repeat__itertools__from_000_to_111.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import itertools -items = [''.join(i) for i in itertools.product('01', repeat=3)] + + +items = ["".join(i) for i in itertools.product("01", repeat=3)] print(items) # ['000', '001', '010', '011', '100', '101', '110', '111'] -items = [i for i in items if '11' not in i] +items = [i for i in items if "11" not in i] print(items) # ['000', '001', '010', '100', '101'] diff --git a/compassplus_employees/config.py b/compassplus_employees/config.py deleted file mode 100644 index 088bed65b..000000000 --- a/compassplus_employees/config.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -URL = 'https://portal.compassplus.com/Employees/Pages/OfficeReferenceBook.aspx' -URL_GET_EMPLOYEES_LIST = 'https://portal.compassplus.com/_layouts/15/tbi/employees.ashx?' \ - 'p=50&c=1&s={}&fl=MPhotoUrl;NameLink;JobTitle;Department;WorkPhone' - -URL_GET_EMPLOYEE_INFO = 'https://portal.compassplus.com/_layouts/15/tbi/ui.ashx?u={}' \ - '&ctrl=TBI.SharePoint.Employees.WebParts/EmployeeFlyout' - -SETTINGS_FILE_NAME = 'settings' - -PERSON_PLACEHOLDER_PHOTO = "iVBORw0KGgoAAAANSUhEUgAAAMgAAACWCAYAAACb3McZAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAkbSURBVHhe7Z0LUxNJFIX3//8bRPDBW1FUFA0g+AAEBN+KBQHxgeOe1LBbu7nTmSQzSd/Od6q+Klezydx0n8x09+3bf42NjWUAYINBAAJgEIAAGAQgAAYBCIBBAAJgEIAAGAQgAAYBCIBBAAJgEIAAGAQgAAYBCIBBAAJgEIAAGAQgAAYBCIBBAAJgEIAAGAQgAAYBCIBBAAJgEIAAGAQgAAYBCIBBBsjU1FS2srKSbW9vZ+/fv8+Ojo5afP/+/Z8/v3v3rvXvep1eb70PDA4MUjPXr1/PXrx4kZ2enma9SP/fy5cvs9nZWfP9oV4wSE3Mz89n+/v72cXFRd7V+9eXL1+ypaUl8/OgHjBIxczNzWUfP37Mu3Q9klFkQOvzoVowSEWMj49nz58/r/SOEdLv37+zra2t7OrVq+b1QDVgkAq4ceNG61d9GNLnapxjXRf0Dwbpk4WFhez8/DzvrsPR2dlZ69HOuj7oDwzSB7dv385+/vyZd9PhStfBAL56MEiP3Lp1K/v161fePeOQxiWPHj0yrxd6A4P0wPT0dGtxL0ZpkuDu3bvmdUP3YJAumZiYyE5OTvLuGKd0Z2NhsRowSJfs7u7m3TBuHR8fMwVcARikC5aXl/Pu50N7e3tmHFAeDFISPVp9+/Yt73o+pEE7K+79gUFKooRDj/r8+bMZD5QDg5Tg2rVr0ax39KIHDx6YcUFnMEgJvN49LtVsNlu5YlZsEAaDdEBjD893j0utrq6a8UEYDNKBx48f513MtzTta8UHYTBIB4aVpVuHFhcXzRihGAwS4ObNm3nXSkPa4WjFCcVgkACNRiPvWmlI+WNXrlwxYwUbDBLg7du3eddKR0rRt2IFGwwSINaM3X707NkzM1awwSAFpDb+uJQKSljxgg0GKeD+/ft5l0pLP378MOMFGwxSwPr6et6l0pOKTFgxQzsYpACV/0xVd+7cMWOGdjBIAYeHh3l3Sk+q+2vFDO1gkAJURDpVra2tmTFDOxikAO2jSFWbm5tmzNAOBikgZYMofd+KGdrBIAXo/I5UpeMUrJihHQxSQMqDdBXZtmKGdjBIAcp8TVUbGxtmzNAOBilAv7Kp6smTJ2bM0A4GKUA1blMVpUnLg0EKUHHqVDUzM2PGDO1gkAJUtlOF11KT6vayaao8GCSAjmVOTdpjb8UKNhgkQIoJi4rJihVsMEiAe/fu5d0qHSkmK1awwSABNA5Jadutxh+Tk5NmrGCDQTqQ0oKhilBYMUIxGKQDKU33UsS6ezBICVKYzaImVm9gkBLol9e7KPfTGxikJJ73h6g6vc44seKCMBikJDrKzOvKOuntvYNBuuD169d5l/MjnauoM06seKAzGKQLVE/K22E6ykq2YoFyYJAuUckcLzo4ODBjgPJgkB7w8Kh1dnbGwLwCMEgPKAUl5pOnlFKiBU7r2qE7MEiP6Nf55OQk75LxSDNtKrxtXTN0DwbpAw3aYzKJzEFZ0WrBIH2iO0kMqSgXFxfZ8vKyeY3QOxikAjQmGebA/fz8nKPVagKDVIjK6eiAmkFKJ0Zx3kd9YJCK0SPXq1evak9LkRGpb1U/GKQmZmdna6nvq9QRnX7FzsDBgEFqRusRe3t7faeofP36tZU2wp6OwYJBCtBzvY4q02OMsmHVOZXRa722DBrILy0ttSqra5ExZBg9nuk1qkCiz52enjbfs1sWFhZa8ezs7GS7u7utuDQtrLud9XrAIP9BFT+0b1sr0UXSukdVCYAyoTrt/7Fe2w8aF3WaZdOOQ+2/13fAXepfRt4gU1NTrQNlNFXajT59+tQ6S916z5jQ3afZbOZXXU76Lp4+fUou19+MpEH0C6m7gDp5P9JMUsyFEBRjP2Mf3Uk1I6cfEev9R4GRM4g69PHxcd4FqpGe5zXGsD5vGGiGq+qFS71fVWMhT4yMQdS4/d4xQtLYZG5uzvzsQaKJAE0F1yGls+iOMkqPXiNhEM3UDGInoB5JVldXzWuoG22r1Z1sENKAflTOGEnaIBprDKMyombCBvkru7i4mJ2enuafPjg1Gg3zelIiWYOMj49nb968yZty8NKvbFXTwUXIhIO6axRJs13WtaVCkgbRnUO/4jFIC35Vz3RdTk0POjHSkhY1U64Yn6RBtFIcm7RHfGtrq6dndxleq/qqjhhjATsZNdUZruQMok1DsUuPX0pT1+OROr3SP7Sf43IlXf+tv9e/6w6k2aPYpanzFBMokzKInsm7XRFH1SnF4xWSMojm6NFwVffExKBJxiAauNa9SQl1lqabNYNotZFHkjGI9lygOLS2tma2kUeSMIgGh6EUdTRYKe3GaiePJGEQreiiuJRKlZUkDBJzGdBRlR55rbbyhnuDKIMWxadUzkR0bxDtq0ZxKoUC2u4NUuceD9SfNjY2zDbzhGuD6Bbu7cSnUZLqglnt5gnXBtHiIIpXGodY7eYJ1wbRORgobnnP8nVtEG3WQXHr4cOHZtt5wbVBhr2bDnWW9zPaXRuEBcL4pW3PVtt5wbVB2PsRv7SRymo7L7g1iAq1ofil3ZBW+3nBrUFUkRz5kOcTsNwaRJU0kA95TjlxaxBVMEQ+5Pn0XbcG0UE0yIc87zB0a5DDw8P860exy/NaiFuDsAbiR6o2Y7WhB9wahDUQPzo4ODDb0AMuDaKyMpT48aMPHz6Y7egBlwYhzd2Xjo6OzHb0gEuDqGIG8iMVk7Pa0QMuDaLylsiPtOvTakcPuDTI+vp6/tUjL4rpkNNucGmQ7e3t/GtHXuQ1H8ulQVgk9Kf5+XmzLWPHpUFiPGUJhaUTsqy2jB2XBmk2m/nXjrzIa8KiS4NQyd2fdKyc1Zax484gOmYN+dPm5qbZnrHjziAUq/YpnfBrtWfsuDOIjlFG/rS/v2+2Z+y4M8jKykr+lSNP8noCrjuDqGI48idV4bfaM3bcGWRnZyf/ypEnea2P5c4gqtSH/Ekb3Kz2jB13BmGrrU95LSDnziCsovvVxMSE2aYx484grKL7lceMXlcGmZyczL9q5FEzMzNmu8aMK4PoC0Z+5bEEqSuDsBfdtzymvLsyiFKmkV/pTEmrXWPGlUFU4xX5lYptWO0aM64MohqvyK8ajYbZrvEylv0BRxk2BoRZQcsAAAAASUVORK5CYII=" diff --git a/compassplus_employees/db.py b/compassplus_employees/db.py deleted file mode 100644 index 8d63f10ae..000000000 --- a/compassplus_employees/db.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from sqlalchemy import Column, String -from sqlalchemy.ext.declarative import declarative_base -Base = declarative_base() - - -import base64 -from lxml import etree -from urllib.parse import urljoin - -import config - - -# TODO: добавить модуль logging - -class Employee(Base): - """ - Класс описывает сотрудника. - - """ - - __tablename__ = 'Employees' - - id = Column(String, primary_key=True) - url = Column(String) - name = Column(String) - short_name = Column(String) - birthday = Column(String) - job = Column(String) - department = Column(String) - photo = Column(String) - work_phone = Column(String) - mobile_phone = Column(String) - email = Column(String) - - @staticmethod - def parse(xml, session): - """Функция парсит строку xml с информацией о сотруднике и возвращает заполненный объект Employee.""" - - root = etree.HTML(xml) - - employee = Employee() - - # Если одно из них не будет определено, прекратить парсинг - try: - employee.id = root.xpath('//node()[@class="lookup-item"]/@id')[0].replace('user-', '') - employee.short_name = root.xpath('//node()[@class="lookup-item"]/@value')[0] - employee.name = root.xpath('//node()[@class="lookup-item"]/@name')[0] - employee.url = root.xpath('//node()[@class="employee-name"]//a/@href')[0] - except IndexError as e: - print('error', 'employee.id/short_name/name/url', e) - raise Exception("Bad parsing!") - - try: - # Загрузка страницы пользователя и вытаскивание дня его рождения - rs = session.get(employee.url) - user_root = etree.HTML(rs.text) - rs = user_root.xpath('//node()[contains(@id, "Birthday")]' - '/node()[@class="ms-tableCell ms-profile-detailsValue"]/text()') - employee.birthday = rs[0] - except Exception as e: - print('error', 'employee.birthday', e, employee.url) - employee.birthday = '' - - try: - photo_url = root.xpath('//node()[@class="employee-photo"]/img/@src')[0] - - # Относительный адрес делаем абсолютным - if photo_url.startswith('/'): - photo_url = urljoin(config.URL, photo_url) - - rs = session.get(photo_url) - if rs.ok: - employee.photo = base64.b64encode(rs.content).decode() - - except Exception as e: - print('warn', 'employee.photo', e) - employee.photo = "" - - try: - employee.job = root.xpath('//node()[@class="employee-jobtitle"]/span[2]/text()')[0].strip() - except IndexError as e: - print('warn', 'employee.job', e) - employee.job = "" - - try: - employee.department = root.xpath('//node()[@class="employee-department"]/span[2]/text()')[0].strip() - except IndexError as e: - print('warn', 'employee.department', e) - employee.department = "" - - try: - employee.work_phone = "" - employee.mobile_phone = "" - - for div_phone in root.xpath('//node()[@class="employee-workphone"]'): - title, text = [el.text.strip() for el in div_phone.getchildren()] - if "Work" in title: - employee.work_phone = text - - elif "Mobile" in title: - employee.mobile_phone = text - - except Exception as e: - print('warn', 'employee.work_phone/mobile_phone', e) - pass - - try: - employee.email = root.xpath('//node()[@class="employee-email"]/a/span/text()')[0].strip() - except IndexError as e: - print('warn', 'employee.email', e) - employee.email = "" - - return employee - - def __str__(self): - return ''.format( - self.name, self.short_name, self.job, self.department) - - def __repr__(self): - return self.__str__() - - -def get_session(): - import os - DIR = os.path.dirname(__file__) - DB_FILE_NAME = 'sqlite:///' + os.path.join(DIR, 'database') - # DB_FILE_NAME = 'sqlite:///:memory:' - - # Создаем базу, включаем логирование и автообновление подключения каждые 2 часа (7200 секунд) - from sqlalchemy import create_engine - engine = create_engine( - DB_FILE_NAME, - # echo=True, - pool_recycle=7200 - ) - - Base.metadata.create_all(engine) - - from sqlalchemy.orm import sessionmaker - Session = sessionmaker(bind=engine) - return Session() - - -db_session = get_session() - - -def exists(employee_id): - return db_session.query(Employee).filter(Employee.id == employee_id).scalar() is not None diff --git a/compassplus_employees/main.py b/compassplus_employees/main.py deleted file mode 100644 index 299d11e04..000000000 --- a/compassplus_employees/main.py +++ /dev/null @@ -1,456 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# Для отлова всех исключений, которые в слотах Qt могут "затеряться" и привести к тихому падению -def log_uncaught_exceptions(ex_cls, ex, tb): - import traceback - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - text += ''.join(traceback.format_tb(tb)) - - print('Error: ', text) - QMessageBox.critical(None, 'Error', text) - sys.exit(1) - - -import sys -sys.excepthook = log_uncaught_exceptions - - -import config - - -def get_url(page): - return config.URL_GET_EMPLOYEES_LIST.format((page - 1) * 50) - - -from db import * - -import requests -from requests_ntlm import HttpNtlmAuth - - -# # TODO: показывать короткое имя пользователя: ipetrash, ypaliy и т.п. -# -# if __name__ == '__main__': -# fill_db() - -# TODO: -import os -if 'QT_API' not in os.environ: - # os.environ['QT_API'] = 'pyqt4' - os.environ['QT_API'] = 'pyqt5' - -from qtpy.QtGui import * -from qtpy.QtWidgets import * -from qtpy.QtCore import * - -# from PyQt4.QtGui import * -# from PyQt4.QtCore import * - - -import base64 - - -def pixmap_from_base64(base64_text): - pixmap = QPixmap() - pixmap.loadFromData(base64.b64decode(base64_text)) - pixmap = pixmap.scaledToWidth(192, Qt.SmoothTransformation) - - return pixmap - - -class EmployeeInfo(QWidget): - def __init__(self): - super().__init__() - - self.photo = QLabel() - self.name = QLabel() - self.short_name = QLabel() - self.job = QLabel() - self.department = QLabel() - # TODO: показывать также сколько осталось до др - self.birthday = QLabel() - - self.url = QLabel() - self.url.setOpenExternalLinks(True) - - self.work_phone = QLabel() - self.mobile_phone = QLabel() - self.id = QLabel() - - self.email = QLabel() - self.email.setOpenExternalLinks(True) - - form_layout = QFormLayout() - form_layout.addRow("Name:", self.name) - form_layout.addRow("Short Name:", self.short_name) - form_layout.addRow("Job:", self.job) - form_layout.addRow("Department:", self.department) - form_layout.addRow("Birthday:", self.birthday) - form_layout.addRow("Url:", self.url) - form_layout.addRow("Work Phone:", self.work_phone) - form_layout.addRow("Mobile Phone:", self.mobile_phone) - form_layout.addRow("Id:", self.id) - form_layout.addRow("Email:", self.email) - - layout = QVBoxLayout() - layout.addWidget(self.photo) - layout.addLayout(form_layout) - layout.addStretch() - - scroll_area_widget = QWidget() - scroll_area_widget.setLayout(layout) - - scroll_area = QScrollArea() - scroll_area.setWidgetResizable(True) - scroll_area.setWidget(scroll_area_widget) - - layout = QVBoxLayout() - layout.addWidget(scroll_area) - self.setLayout(layout) - - # Все QLabel на форме умеет поддержку выделения и при наличии ссылок на них можно кликать - for label in self.findChildren(QLabel): - label.setTextInteractionFlags(Qt.TextBrowserInteraction) - - def set_employee(self, employee): - if not employee: - self.photo.setPixmap(pixmap_from_base64(config.PERSON_PLACEHOLDER_PHOTO)) - - self.name.setText("None") - self.short_name.setText("None") - self.job.setText("None") - self.department.setText("None") - self.birthday.setText("None") - self.url.setText("None") - self.work_phone.setText("None") - self.mobile_phone.setText("None") - self.id.setText("None") - self.email.setText("None") - return - - self.photo.setPixmap(pixmap_from_base64(employee.photo)) - - self.name.setText(employee.name) - self.short_name.setText(employee.short_name) - self.job.setText(employee.job) - self.department.setText(employee.department) - self.birthday.setText(employee.birthday) - self.url.setText('{0}'.format(employee.url)) - self.work_phone.setText(employee.work_phone) - self.mobile_phone.setText(employee.mobile_phone) - self.id.setText(employee.id) - self.email.setText('{0}'.format(employee.email)) - - -# TODO: ввод с клавы при фокусе на таблицу меняет редактор фильтра -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() - - self.setWindowTitle('Compass Plus Employees') - self.setContextMenuPolicy(Qt.NoContextMenu) - - # TODO: окно с информацией о выделенном сотруднике умеет показывать его переработку/недоработку и прочее - self.filter_line_edit = QLineEdit() - # При изменении окна происходит вызов run_filter и отображение/скрытие кнопки очистки текста - self.filter_line_edit.textChanged.connect(self.run_filter) - self.filter_line_edit.installEventFilter(self) - - try: - # Добавление в редактор фильтра кнопки очищения содержимого - clear_icon = self.style().standardIcon(QStyle.SP_LineEditClearButton) - - clear_action = self.filter_line_edit.addAction(clear_icon, QLineEdit.TrailingPosition) - clear_action.setVisible(len(self.filter_line_edit.text()) > 0) - clear_action.triggered.connect(self.filter_line_edit.clear) - - self.filter_line_edit.textChanged.connect(lambda text: clear_action.setVisible(len(text) > 0)) - - # Если SP_LineEditClearButton не найден - except AttributeError: - # TODO: сделать реализацию для Qt4 - # clear_icon = pixmap_from_base64(config.LINE_EDIT_CLEAR_BUTTON_32x32) - pass - - self.employees_table = QTableWidget() - self.employees_table.setSelectionBehavior(QTableWidget.SelectRows) - self.employees_table.setSelectionMode(QTableWidget.SingleSelection) - self.employees_table.currentItemChanged.connect(lambda item, _: self._item_click(item)) - - layout_filter = QHBoxLayout() - layout_filter.addWidget(QLabel('Filter:')) - layout_filter.addWidget(self.filter_line_edit) - - layout = QVBoxLayout() - layout.addLayout(layout_filter) - layout.addWidget(self.employees_table) - - central_widget = QWidget() - central_widget.setLayout(layout) - self.setCentralWidget(central_widget) - - employee_info_dock_widget = QDockWidget("Employee Info") - employee_info_dock_widget.setObjectName("employee_info_dock_widget") - employee_info_dock_widget.setFeatures(QDockWidget.NoDockWidgetFeatures) - - self.employee_info = EmployeeInfo() - employee_info_dock_widget.setWidget(self.employee_info) - self.addDockWidget(Qt.RightDockWidgetArea, employee_info_dock_widget) - - tool_bar = self.addToolBar("General") - tool_bar.setObjectName("General") - action_refill = tool_bar.addAction("Parse and refill") - action_refill.setToolTip("Clear database, parse site with employees, and fill database") - action_refill.setStatusTip(action_refill.toolTip()) - action_refill.triggered.connect(self.refill) - - self.setStatusBar(QStatusBar()) - - def refill(self): - # TODO: можно в отдельный класс вынести - dialog = QDialog() - dialog.setWindowTitle('Auth and refill database') - - info = QLabel() - info.setText("""When you click on OK, you will be cleansing a database of employees, -start parsing for the collection of employees and populate the database.""") - - login = QLineEdit("CP\\") - password = QLineEdit() - password.setEchoMode(QLineEdit.Password) - - button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - button_box.accepted.connect(dialog.accept) - button_box.rejected.connect(dialog.reject) - - form_layout = QFormLayout() - form_layout.addRow("Login:", login) - form_layout.addRow("Password:", password) - - layout = QVBoxLayout() - layout.addWidget(info) - layout.addLayout(form_layout) - layout.addStretch() - layout.addWidget(button_box) - - dialog.setLayout(layout) - if not dialog.exec(): - return - - login, password = login.text(), password.text() - - # Сначала пытаемся авторизоваться - session = requests.Session() - session.auth = HttpNtlmAuth(login, password, session) - - # Авторизация - rs = session.get(config.URL) - if not rs.ok: - QMessageBox.information(self, "Info", "Failed to login") - print('Не удалось авторизоваться') - print('rs.status_code = {}'.format(rs.status_code)) - print('rs.headers = {}'.format(rs.headers)) - return - - # TODO: move to db.py - # Очищение базы данных - for i in db_session.query(Employee): - db_session.delete(i) - db_session.commit() - - self.run_filter() - self.fill_db(session) - self.run_filter() - - def fill_db(self, session): - page = 1 - - rs = session.get(get_url(page)) - data = rs.json() - - max_page = data['Pages'] - - # TODO: наверное тоже нужно в QProgressDialog обернуть, хоть сбор и быстрый - employee_list = list() - employee_list += data['Properties'] - - while page < max_page: - page += 1 - - rs = session.get(get_url(page)) - data = rs.json() - - employee_list += data['Properties'] - - # Для отображения диалога парсинга и заполнения базы - progress = QProgressDialog("Operation in progress...", "Cancel", 0, len(employee_list), self) - progress.setWindowTitle("Parsing") - progress.setWindowModality(Qt.WindowModal) - - for i, row in enumerate(employee_list, 1): - progress.setValue(i) - - if progress.wasCanceled(): - break - - employee_id = row['Id'] - - if exists(employee_id): - print('Employee with id = {} already exist.'.format(employee_id)) - continue - - rs = session.get(config.URL_GET_EMPLOYEE_INFO.format(employee_id)) - if not rs.ok: - print("Request getting employee info (id={}) not ok.".format(employee_id)) - print("rs.status_code = {}".format(rs.status_code)) - print("rs.headers = {}".format(rs.headers)) - continue - - employee = Employee.parse(rs.text, session) - print(i, employee) - - db_session.add(employee) - - db_session.commit() - - progress.setValue(len(employee_list)) - - def _item_click(self, item): - employee = None - - if item and self.employees_table.rowCount() > 0: - item = self.employees_table.item(item.row(), 0) - employee = item.data(Qt.UserRole) - - self.employee_info.set_employee(employee) - - def run_filter(self): - # TODO: лучше использовать модель - # TODO: лучше использовать стандартный фильтр qt - # TODO: поиграться с делегатами для красивого отображения описания + ссылки на гист - - self.employees_table.clear() - self._item_click(None) - - # TODO: db.py - from sqlalchemy import or_ - filter_text = self.filter_line_edit.text() - filter_text = "%{}%".format(filter_text) - sql_filter = or_( - Employee.name.like(filter_text), - Employee.short_name.like(filter_text), - Employee.job.like(filter_text), - Employee.department.like(filter_text), - Employee.birthday.like(filter_text), - Employee.work_phone.like(filter_text), - Employee.mobile_phone.like(filter_text), - Employee.id.like(filter_text), - Employee.email.like(filter_text), - ) - - query = db_session.query(Employee).filter(sql_filter) - rows = query.count() - - self.employees_table.setRowCount(rows) - - headers = [ - "Name", "Short Name", "Job", "Department", - "Birthday", "Url", "Work Phone", "Mobile Phone", "Id", "Email", "Photo" - ] - self.employees_table.setColumnCount(len(headers)) - self.employees_table.setHorizontalHeaderLabels(headers) - - row = 0 - for employee in query: - self.employees_table.setItem(row, 0, QTableWidgetItem(employee.name)) - self.employees_table.setItem(row, 1, QTableWidgetItem(employee.short_name)) - self.employees_table.setItem(row, 2, QTableWidgetItem(employee.job)) - self.employees_table.setItem(row, 3, QTableWidgetItem(employee.department)) - self.employees_table.setItem(row, 4, QTableWidgetItem(employee.birthday)) - self.employees_table.setItem(row, 5, QTableWidgetItem(employee.url)) - self.employees_table.setItem(row, 6, QTableWidgetItem(employee.work_phone)) - self.employees_table.setItem(row, 7, QTableWidgetItem(employee.mobile_phone)) - self.employees_table.setItem(row, 8, QTableWidgetItem(employee.id)) - self.employees_table.setItem(row, 9, QTableWidgetItem(employee.email)) - self.employees_table.setItem(row, 10, QTableWidgetItem(employee.photo)) - - self.employees_table.item(row, 0).setData(Qt.UserRole, employee) - - row += 1 - - # Запрет редактирования ячеек таблицы - for row in range(self.employees_table.rowCount()): - for column in range(self.employees_table.columnCount()): - self.employees_table.item(row, column).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) - - # Показываем информацию о первом сотруднике - if self.employees_table.rowCount() > 0: - item = self.employees_table.item(0, 0) - self.employees_table.setCurrentItem(item) - - def read_settings(self): - ini = QSettings(config.SETTINGS_FILE_NAME, QSettings.IniFormat) - - state = ini.value('MainWindow_State') - if state: - self.restoreState(state) - - geometry = ini.value('MainWindow_Geometry') - if geometry: - self.restoreGeometry(geometry) - - def write_settings(self): - ini = QSettings(config.SETTINGS_FILE_NAME, QSettings.IniFormat) - ini.setValue('MainWindow_State', self.saveState()) - ini.setValue('MainWindow_Geometry', self.saveGeometry()) - - def eventFilter(self, object, event): - # В окне вводе при клике на стрелку вниз фокус переходит в таблицу - if object == self.filter_line_edit: - if event.type() == QEvent.KeyRelease and event.key() == Qt.Key_Down: - self.employees_table.setFocus() - return False - - return super().eventFilter(object, event) - - def closeEvent(self, _): - self.write_settings() - - QApplication.instance().quit() - - -if __name__ == '__main__': - app = QApplication([]) - - - # TODO: согласовать с os.environ['QT_API'] - # def load_PyQt4_plugins(): - # """ - # Функция загружает Qt плагины. - # - # """ - # - # import PyQt4 - # import os - # - # qApp = PyQt4.QtGui.QApplication.instance() - # - # for plugins_dir in [os.path.join(p, "plugins") for p in PyQt4.__path__]: - # qApp.addLibraryPath(plugins_dir) - # - # - # load_PyQt4_plugins() - - mw = MainWindow() - mw.resize(1000, 750) - mw.read_settings() - mw.show() - mw.run_filter() - # TODO: - mw.employees_table.setFocus() - - app.exec_() diff --git a/complex.py b/complex.py index 7342e2fe5..2b7d5b283 100644 --- a/complex.py +++ b/complex.py @@ -1,7 +1,7 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': +if __name__ == "__main__": # Комплексные числа (complex) # В Python встроены также и комплексные числа: @@ -28,4 +28,4 @@ print(abs(3 + 4j)) # Модуль комплексного числа print(pow(3 + 4j, 2)) # Возведение в степень - # Также для работы с комплексными числами используется также модуль cmath . \ No newline at end of file + # Также для работы с комплексными числами используется также модуль cmath . diff --git a/compress__decompress__bz2__bzip2.py b/compress__decompress__bz2__bzip2.py index 7f003256d..65130fa0a 100644 --- a/compress__decompress__bz2__bzip2.py +++ b/compress__decompress__bz2__bzip2.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://docs.python.org/3/library/bz2.html#one-shot-de-compression +import bz2 + + s_in = "HelloWorld!" * 1000000 -s_in = bytes(s_in, encoding='utf-8') +s_in = bytes(s_in, encoding="utf-8") print(len(s_in)) # 11000000 -import bz2 s_out = bz2.compress(s_in) assert bz2.decompress(s_out) == s_in print(len(s_out)) # 927 diff --git a/compress__decompress__gzip.py b/compress__decompress__gzip.py index 063edf2da..957a8cac4 100644 --- a/compress__decompress__gzip.py +++ b/compress__decompress__gzip.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import gzip s_in = "HelloWorld!" * 1000000 -s_in = bytes(s_in, encoding='utf-8') +s_in = bytes(s_in, encoding="utf-8") print(len(s_in)) # 11000000 -import gzip s_out = gzip.compress(s_in) assert gzip.decompress(s_out) == s_in print(len(s_out)) # 21396 diff --git a/compress__decompress__lzma.py b/compress__decompress__lzma.py index d02865aa6..9e41c115d 100644 --- a/compress__decompress__lzma.py +++ b/compress__decompress__lzma.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://docs.python.org/3/library/lzma.html#lzma.compress +import lzma + + s_in = "HelloWorld!" * 1000000 -s_in = bytes(s_in, encoding='utf-8') +s_in = bytes(s_in, encoding="utf-8") print(len(s_in)) # 11000000 -import lzma s_out = lzma.compress(s_in) assert lzma.decompress(s_out) == s_in print(len(s_out)) # 1744 diff --git a/compress__decompress__zlib.py b/compress__decompress__zlib.py index 938ccfc8e..949315103 100644 --- a/compress__decompress__zlib.py +++ b/compress__decompress__zlib.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import zlib s_in = "HelloWorld!" * 1000000 -s_in = bytes(s_in, encoding='utf-8') +s_in = bytes(s_in, encoding="utf-8") print(len(s_in)) # 11000000 -import zlib s_out = zlib.compress(s_in) assert zlib.decompress(s_out) == s_in print(len(s_out)) # 21384 diff --git a/concurrency_in_python__threading_processing/about_1__single_thread.py b/concurrency_in_python__threading_processing/about_1__single_thread.py index f5a2bcb38..b03abd482 100644 --- a/concurrency_in_python__threading_processing/about_1__single_thread.py +++ b/concurrency_in_python__threading_processing/about_1__single_thread.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://towardsdatascience.com/concurrency-in-python-e770c878ab53 @@ -16,14 +16,14 @@ # A CPU heavy calculation, just as an example. This can be anything you like -def heavy(n, myid): +def heavy(n, myid) -> None: for x in range(1, n): for y in range(1, n): - x ** y + x**y print(myid, "is done") -def sequential(n): +def sequential(n) -> None: for i in range(n): heavy(N, i) diff --git a/concurrency_in_python__threading_processing/about_2__multithreading.py b/concurrency_in_python__threading_processing/about_2__multithreading.py index 0de6b23b7..4e68c7285 100644 --- a/concurrency_in_python__threading_processing/about_2__multithreading.py +++ b/concurrency_in_python__threading_processing/about_2__multithreading.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://towardsdatascience.com/concurrency-in-python-e770c878ab53 @@ -14,7 +14,7 @@ from about_1__single_thread import heavy, WORKERS, N -def threaded(n): +def threaded(n) -> None: threads = [] for i in range(n): @@ -26,7 +26,7 @@ def threaded(n): t.join() -if __name__ == '__main__': +if __name__ == "__main__": start = time.perf_counter() threaded(WORKERS) print(f"Elapsed {time.perf_counter() - start:.2f} secs") diff --git a/concurrency_in_python__threading_processing/about_3__multiprocessing.py b/concurrency_in_python__threading_processing/about_3__multiprocessing.py index 122e4c09a..6e71b2722 100644 --- a/concurrency_in_python__threading_processing/about_3__multiprocessing.py +++ b/concurrency_in_python__threading_processing/about_3__multiprocessing.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://towardsdatascience.com/concurrency-in-python-e770c878ab53 @@ -14,7 +14,7 @@ from about_1__single_thread import heavy, WORKERS, N -def multiproc(n): +def multiproc(n) -> None: processes = [] for i in range(n): @@ -26,9 +26,8 @@ def multiproc(n): p.join() -if __name__ == '__main__': +if __name__ == "__main__": start = time.perf_counter() multiproc(WORKERS) print(f"Elapsed {time.perf_counter() - start:.2f} secs") # Elapsed 4.03 secs - diff --git a/concurrency_in_python__threading_processing/about_4__multiprocessing_with_Pool.py b/concurrency_in_python__threading_processing/about_4__multiprocessing_with_Pool.py index b376f39f7..473e2ffdf 100644 --- a/concurrency_in_python__threading_processing/about_4__multiprocessing_with_Pool.py +++ b/concurrency_in_python__threading_processing/about_4__multiprocessing_with_Pool.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://towardsdatascience.com/concurrency-in-python-e770c878ab53 @@ -14,17 +14,17 @@ from about_1__single_thread import heavy, WORKERS, N -def doit(n): +def doit(n) -> None: heavy(N, n) -def pooled(n): +def pooled(n) -> None: # By default, our pool will have processes slots with multiprocessing.Pool() as pool: - pool.map(doit, range(n)) + pool.map(doit, range(n)) -if __name__ == '__main__': +if __name__ == "__main__": start = time.perf_counter() pooled(WORKERS) print(f"Elapsed {time.perf_counter() - start:.2f} secs") diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/Manager__example.py b/concurrency_in_python__threading_processing/multiprocessing__examples/Manager__example.py index 105da41ee..7695daacc 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/Manager__example.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/Manager__example.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing import Process, Manager -def f(users_data): +def f(users_data) -> None: users_data["users"][0]["coins"] += 1 -if __name__ == '__main__': +if __name__ == "__main__": manager = Manager() users_data = manager.dict() user_data = manager.dict({"id": 1, "coins": 0}) @@ -20,5 +20,5 @@ def f(users_data): p = Process(target=f, args=[users_data]) p.start() p.join() - print(users_data['users'][0]) + print(users_data["users"][0]) # {'id': 1, 'coins': 1} diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/code_execution_with_time_limit.py b/concurrency_in_python__threading_processing/multiprocessing__examples/code_execution_with_time_limit.py index 0788aa60e..e7b3623b8 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/code_execution_with_time_limit.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/code_execution_with_time_limit.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import multiprocessing -def run(): +def run() -> None: import time i = 1 @@ -20,7 +20,7 @@ def run(): time.sleep(1) -if __name__ == '__main__': +if __name__ == "__main__": p = multiprocessing.Process(target=run) p.start() diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/create_process_get_result__communication__Pipe.py b/concurrency_in_python__threading_processing/multiprocessing__examples/create_process_get_result__communication__Pipe.py index 35e196cc4..734daa4dc 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/create_process_get_result__communication__Pipe.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/create_process_get_result__communication__Pipe.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing import Process, Pipe -def f(con, name): - con.send('Hello, ' + name) +def f(con, name) -> None: + con.send("Hello, " + name) con.close() -if __name__ == '__main__': +if __name__ == "__main__": parent_conn, child_conn = Pipe() - p = Process(target=f, args=(child_conn, 'bob')) + p = Process(target=f, args=(child_conn, "bob")) p.start() print(parent_conn.recv()) # Hello, bob p.join() diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/create_process_get_result__communication__Queue.py b/concurrency_in_python__threading_processing/multiprocessing__examples/create_process_get_result__communication__Queue.py index aa56310ab..b0f74df34 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/create_process_get_result__communication__Queue.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/create_process_get_result__communication__Queue.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing import Process, Queue -def f(q, name): - q.put('Hello, ' + name) +def f(q, name) -> None: + q.put("Hello, " + name) -if __name__ == '__main__': +if __name__ == "__main__": q = Queue() - p = Process(target=f, args=(q, 'bob')) + p = Process(target=f, args=(q, "bob")) p.start() print(q.get()) # Hello, bob p.join() diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__Pool_map.py b/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__Pool_map.py index 3dc553cd3..5e9344261 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__Pool_map.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__Pool_map.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://docs.python.org/3.6/library/multiprocessing.html @@ -14,7 +14,7 @@ def f(x): return x * x -if __name__ == '__main__': +if __name__ == "__main__": with Pool(5) as p: result = p.map(f, [1, 2, 3]) print(result) # [1, 4, 9] diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__Process.py b/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__Process.py index 0efeac66d..c4504997e 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__Process.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__Process.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing import Process -def f(name): - print('Hello,', name) +def f(name) -> None: + print("Hello,", name) -if __name__ == '__main__': - p = Process(target=f, args=('bob',)) +if __name__ == "__main__": + p = Process(target=f, args=("bob",)) p.start() p.join() diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__with_daemon.py b/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__with_daemon.py index bbe07a628..2cb0f3016 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__with_daemon.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__with_daemon.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing import Process, current_process import time -def run(): +def run() -> None: print(current_process()) - for i in 'Hello World!': + for i in "Hello World!": print(i) time.sleep(0.2) -if __name__ == '__main__': +if __name__ == "__main__": p = Process(target=run, daemon=True) p.start() diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__without_daemon.py b/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__without_daemon.py index 69e7bc4e0..3317578cb 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__without_daemon.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/hello_world__without_daemon.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing import Process, current_process import time -def run(): +def run() -> None: print(current_process()) - for i in 'Hello World!': + for i in "Hello World!": print(i) time.sleep(0.2) -if __name__ == '__main__': +if __name__ == "__main__": p = Process(target=run, daemon=False) p.start() diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__QApplication__create_child_process__with__Tkinter_and_Qt.py b/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__QApplication__create_child_process__with__Tkinter_and_Qt.py index 850c80d7c..08fc65fc4 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__QApplication__create_child_process__with__Tkinter_and_Qt.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__QApplication__create_child_process__with__Tkinter_and_Qt.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing import Process @@ -9,24 +9,25 @@ from multiprocessing__Tkinter__in_other_process import go as go_tk -def create_qt(): - p = Process(target=go_qt, args=('Qt',)) +def create_qt() -> None: + p = Process(target=go_qt, args=("Qt",)) p.start() -def create_tk(): - p = Process(target=go_tk, args=('Tk',)) +def create_tk() -> None: + p = Process(target=go_tk, args=("Tk",)) p.start() -if __name__ == '__main__': +if __name__ == "__main__": from PyQt5.Qt import QApplication, QPushButton, QWidget, QVBoxLayout + app = QApplication([]) - button_qt = QPushButton('Create Qt') + button_qt = QPushButton("Create Qt") button_qt.clicked.connect(create_qt) - button_tk = QPushButton('Create Tk') + button_tk = QPushButton("Create Tk") button_tk.clicked.connect(create_tk) layout = QVBoxLayout() diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__QApplication__in_other_process.py b/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__QApplication__in_other_process.py index 7c49a0231..fb2789a80 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__QApplication__in_other_process.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__QApplication__in_other_process.py @@ -1,25 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def go(name): - from PyQt5.Qt import QApplication, Qt, QLabel +from PyQt5.Qt import QApplication, Qt, QLabel + + +def go(name) -> None: app = QApplication([]) mw = QLabel() mw.setAlignment(Qt.AlignCenter) mw.setMinimumSize(150, 50) - mw.setText('Hello, ' + name) + mw.setText("Hello, " + name) mw.show() app.exec() -if __name__ == '__main__': +if __name__ == "__main__": from multiprocessing import Process - p = Process(target=go, args=('bob',)) + + p = Process(target=go, args=("bob",)) p.start() # Необязательно в данном случае -- главный поток все-равно закроется после дочернего diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__QApplication__in_other_process_list__Pool_map.py b/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__QApplication__in_other_process_list__Pool_map.py index 7a17434fa..c27cae3ae 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__QApplication__in_other_process_list__Pool_map.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__QApplication__in_other_process_list__Pool_map.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': +if __name__ == "__main__": from multiprocessing import Pool from multiprocessing__QApplication__in_other_process import go with Pool() as p: - p.map(go, ['Alice', 'Bob', 'World']) + p.map(go, ["Alice", "Bob", "World"]) diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__Tkinter__in_other_process.py b/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__Tkinter__in_other_process.py index 2289017f3..4b2b670fd 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__Tkinter__in_other_process.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing__Tkinter__in_other_process.py @@ -1,23 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def go(name): - import tkinter as tk +import tkinter as tk + + +def go(name) -> None: app = tk.Tk() app.minsize(150, 50) - mw = tk.Label(app, text='Hello, ' + name) - mw.pack(fill='both', expand=True) + mw = tk.Label(app, text="Hello, " + name) + mw.pack(fill="both", expand=True) app.mainloop() -if __name__ == '__main__': +if __name__ == "__main__": from multiprocessing import Process - p = Process(target=go, args=('bob',)) + + p = Process(target=go, args=("bob",)) p.start() # Необязательно в данном случае -- главный поток все-равно закроется после дочернего diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing_with_QApplication_and_Tkinter__in_other_process.py b/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing_with_QApplication_and_Tkinter__in_other_process.py index f17c5243a..a32d76308 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing_with_QApplication_and_Tkinter__in_other_process.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing_with_QApplication_and_Tkinter__in_other_process.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': +if __name__ == "__main__": from multiprocessing import Process from multiprocessing__QApplication__in_other_process import go as go_qt from multiprocessing__Tkinter__in_other_process import go as go_tk - p1 = Process(target=go_qt, args=('Qt',)) + p1 = Process(target=go_qt, args=("Qt",)) p1.start() - p2 = Process(target=go_tk, args=('Tk',)) + p2 = Process(target=go_tk, args=("Tk",)) p2.start() # Необязательно в данном случае -- главный поток все-равно закроется после дочернего diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing_with_webservers__flask.py b/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing_with_webservers__flask.py index 153112347..486e8fe87 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing_with_webservers__flask.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/multiprocessing_with_webservers__flask.py @@ -1,32 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def go(port): - from flask import Flask +import logging +import time + +import requests + +from flask import Flask + + +def go(port: int) -> None: app = Flask(__name__) - import logging logging.basicConfig(level=logging.DEBUG) @app.route("/") - def index(): - return "Hello World! (port={})".format(port) + def index() -> str: + return f"Hello World! (port={port})" app.run(port=port) -def go_parser(urls): - import time - import requests - +def go_parser(urls) -> None: while True: for url in urls: try: rs = requests.get(url) - print('Parser: {}. "{}"'.format(rs, rs.text)) + print(f'Parser: {rs}. "{rs.text}"') except: pass @@ -34,7 +37,7 @@ def go_parser(urls): time.sleep(2) -if __name__ == '__main__': +if __name__ == "__main__": # NOTE: recommended to add daemon=False or call join() for each process from multiprocessing import Process @@ -50,7 +53,7 @@ def go_parser(urls): # p = Process(target=go, args=(port,)) # p.start() - urls = ['http://127.0.0.1:5001/', 'http://127.0.0.1:5002/'] + urls = ["http://127.0.0.1:5001/", "http://127.0.0.1:5002/"] p3 = Process(target=go_parser, args=(urls,)) p3.start() diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/print_process_info.py b/concurrency_in_python__threading_processing/multiprocessing__examples/print_process_info.py index 9e91062f0..260a1ecce 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/print_process_info.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/print_process_info.py @@ -1,29 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing import Process import os -def info(title): +def info(title) -> None: print(title) - print('module name:', __name__) - print('parent process:', os.getppid()) - print('process id:', os.getpid()) + print("module name:", __name__) + print("parent process:", os.getppid()) + print("process id:", os.getpid()) -def f(name): - info('function f') - print('Hello,', name) +def f(name) -> None: + info("function f") + print("Hello,", name) -if __name__ == '__main__': - info('main line') +if __name__ == "__main__": + info("main line") print() - p = Process(target=f, args=('bob',)) + p = Process(target=f, args=("bob",)) p.start() p.join() diff --git a/concurrency_in_python__threading_processing/multiprocessing__examples/synchronization_between_processes.py b/concurrency_in_python__threading_processing/multiprocessing__examples/synchronization_between_processes.py index c81bcee64..e55c96cbf 100644 --- a/concurrency_in_python__threading_processing/multiprocessing__examples/synchronization_between_processes.py +++ b/concurrency_in_python__threading_processing/multiprocessing__examples/synchronization_between_processes.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://docs.python.org/3.6/library/multiprocessing.html#synchronization-between-processes @@ -11,14 +11,14 @@ import time -def f(lock, i): +def f(lock, i) -> None: with lock: - print('hello world', i) + print("hello world", i) time.sleep(1) -if __name__ == '__main__': +if __name__ == "__main__": lock = Lock() for num in range(10): diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/ProcessPoolExecutor__examples/hello_world.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/ProcessPoolExecutor__examples/hello_world.py index fe6ca0f6a..9b6a9b408 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/ProcessPoolExecutor__examples/hello_world.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/ProcessPoolExecutor__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor-example @@ -17,11 +17,11 @@ 112272535095293, 115280095190773, 115797848077099, - 1099726899285419 + 1099726899285419, ] -def is_prime(n): +def is_prime(n) -> bool: if n < 2: return False if n == 2: @@ -36,14 +36,14 @@ def is_prime(n): return True -def main(): +def main() -> None: # NOTE: max_workers must be defined # with concurrent.futures.ProcessPoolExecutor(max_workers=MAX_WORKERS) as executor: with concurrent.futures.ProcessPoolExecutor() as executor: for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)): - print('%d is prime: %s' % (number, prime)) + print(f"{number} is prime: {prime}") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/ThreadPoolExecutor__examples/hello_world.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/ThreadPoolExecutor__examples/hello_world.py index fe6989b06..508be632f 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/ThreadPoolExecutor__examples/hello_world.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/ThreadPoolExecutor__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor-example @@ -12,11 +12,11 @@ URLS = [ - 'http://www.foxnews.com/', - 'http://www.cnn.com/', - 'http://europe.wsj.com/', - 'http://www.bbc.co.uk/', - 'http://some-made-up-domain.com/' + "http://www.foxnews.com/", + "http://www.cnn.com/", + "http://europe.wsj.com/", + "http://www.bbc.co.uk/", + "http://some-made-up-domain.com/", ] MAX_WORKERS = 5 @@ -40,6 +40,6 @@ def load_url(url, timeout): try: data = future.result() except Exception as e: - print(f'{url!r} generated an exception: {e}') + print(f"{url!r} generated an exception: {e}") else: - print(f'{url!r} page is {len(data)} bytes') + print(f"{url!r} page is {len(data)} bytes") diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/ThreadPoolExecutor__examples/time_sleep.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/ThreadPoolExecutor__examples/time_sleep.py index 2f9daa2a9..277878b81 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/ThreadPoolExecutor__examples/time_sleep.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/ThreadPoolExecutor__examples/time_sleep.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor-example @@ -15,8 +15,8 @@ MAX_WORKERS = 5 -def run(name): - print(f'name: {name}') +def run(name) -> None: + print(f"name: {name}") time.sleep(randint(1, 4)) diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/ThreadPoolExecutor__examples/time_sleep__as_completed.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/ThreadPoolExecutor__examples/time_sleep__as_completed.py index bc0952691..28ccadb7e 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/ThreadPoolExecutor__examples/time_sleep__as_completed.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/ThreadPoolExecutor__examples/time_sleep__as_completed.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor-example @@ -15,9 +15,9 @@ MAX_WORKERS = 5 -def run(name): +def run(name) -> str: time.sleep(randint(1, 4)) - return f'name: {name}' + return f"name: {name}" executor = concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/atomic_counter.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/atomic_counter.py index 77c655267..c0f4b1dc8 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/atomic_counter.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/atomic_counter.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://gist.github.com/benhoyt/8c8a8d62debe8e5aa5340373f9c509c7 @@ -40,7 +40,8 @@ class AtomicCounter: >>> counter.value 400000 """ - def __init__(self, initial=0): + + def __init__(self, initial=0) -> None: """Initialize a new atomic counter to given initial value (default 0).""" self.value = initial self._lock = threading.Lock() @@ -54,8 +55,9 @@ def increment(self, num=1): return self.value -if __name__ == '__main__': +if __name__ == "__main__": import doctest + doctest.testmod() # MORE: @@ -64,7 +66,7 @@ def increment(self, num=1): counter = AtomicCounter() - def incrementor(): + def incrementor() -> None: for i in range(NUM): counter.increment() diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/code_execution_with_time_limit.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/code_execution_with_time_limit.py index 850ed7f2e..d882d8e3b 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/code_execution_with_time_limit.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/code_execution_with_time_limit.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import threading +import time -def run(): - import time - +def run() -> None: i = 1 # Бесконечный цикл @@ -20,11 +19,11 @@ def run(): time.sleep(1) -if __name__ == '__main__': +if __name__ == "__main__": thread = threading.Thread(target=run, daemon=True) thread.start() # Wait thread.join(5) - print('Quit!') + print("Quit!") diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/hello_world.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/hello_world.py index 63929c1db..130b0808b 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/hello_world.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/hello_world.py @@ -1,11 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +from multiprocessing.dummy import Pool as ThreadPool +from urllib.request import urlopen def go(url): - from urllib.request import urlopen rs = urlopen(url) print(url, rs) @@ -13,13 +16,12 @@ def go(url): urls = [ - 'http://www.python.org', - 'http://www.python.org/about/', - 'http://www.python.org/doc/', - 'http://www.python.org/download/', + "http://www.python.org", + "http://www.python.org/about/", + "http://www.python.org/doc/", + "http://www.python.org/download/", ] -from multiprocessing.dummy import Pool as ThreadPool pool = ThreadPool() results = pool.map(go, urls) print(results) diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/hello_world_with_result__requests.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/hello_world_with_result__requests.py index 96d6642cd..f2aabafcd 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/hello_world_with_result__requests.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/hello_world_with_result__requests.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing.dummy import Pool as ThreadPool @@ -9,10 +9,10 @@ urls = [ - 'http://www.python.org', - 'http://www.python.org/about/', - 'http://www.python.org/doc/', - 'http://www.python.org/download/', + "http://www.python.org", + "http://www.python.org/about/", + "http://www.python.org/doc/", + "http://www.python.org/download/", ] pool = ThreadPool() diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/hello_world_with_result__urllib.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/hello_world_with_result__urllib.py index ba9327893..5f73d5464 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/hello_world_with_result__urllib.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/hello_world_with_result__urllib.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing.dummy import Pool as ThreadPool @@ -9,10 +9,10 @@ urls = [ - 'http://www.python.org', - 'http://www.python.org/about/', - 'http://www.python.org/doc/', - 'http://www.python.org/download/', + "http://www.python.org", + "http://www.python.org/about/", + "http://www.python.org/doc/", + "http://www.python.org/download/", ] pool = ThreadPool() diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/urls.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/urls.py index 826f9fd6f..2cf9018ae 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/urls.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/example multiprocessing.dummy/urls.py @@ -1,17 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import time + from urllib.request import urlopen from multiprocessing.dummy import Pool as ThreadPool + urls = [ - 'http://www.python.org', - 'http://www.python.org/about/', - 'http://www.python.org/doc/', - 'http://www.python.org/download/', + "http://www.python.org", + "http://www.python.org/about/", + "http://www.python.org/doc/", + "http://www.python.org/download/", # 'http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html', # 'http://www.python.org/getit/', # 'http://www.python.org/community/', @@ -22,27 +25,26 @@ # 'http://docs.python.org/devguide/', # 'http://www.python.org/community/awards/' # etc.. - ] - - -import time +] t = time.clock() results = [] for url in urls: result = urlopen(url) results.append(result) -print('Single thread: {:.3f} seconds'.format(time.clock() - t)) +print(f"Single thread: {time.clock() - t:.3f} seconds") # ------- VERSUS ------- # -def go(count=1): + +def go(count=1) -> None: t = time.clock() pool = ThreadPool(count) results = pool.map(urlopen, urls) # pool.close() # pool.join() - print('{} Pool: {:.3f} seconds'.format(count, time.clock() - t)) + print(f"{count} Pool: {time.clock() - t:.3f} seconds") + # ------- 1 Pool ------- # go() diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/flask__threaded/flask_test_script.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/flask__threaded/flask_test_script.py index fb3c257ec..ff6ab87bf 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/flask__threaded/flask_test_script.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/flask__threaded/flask_test_script.py @@ -1,20 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import requests # With threaded for _ in range(5): - rs = requests.get('http://127.0.0.1:6000/') + rs = requests.get("http://127.0.0.1:6000/") print(rs.text) print() # Without threaded for _ in range(5): - rs = requests.get('http://127.0.0.1:6001/') + rs = requests.get("http://127.0.0.1:6001/") print(rs.text) - diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/flask__threaded/threaded__with.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/flask__threaded/threaded__with.py index 77ae8bceb..31c26c6a2 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/flask__threaded/threaded__with.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/flask__threaded/threaded__with.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import threading @@ -12,9 +12,9 @@ @app.route("/") -def index(): - return "Current thread: {}".format(threading.current_thread()) +def index() -> str: + return f"Current thread: {threading.current_thread()}" -if __name__ == '__main__': +if __name__ == "__main__": app.run(port=6000) diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/flask__threaded/threaded__without.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/flask__threaded/threaded__without.py index db1096552..0e80e4203 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/flask__threaded/threaded__without.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/flask__threaded/threaded__without.py @@ -1,18 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import threading from flask import Flask + + app = Flask(__name__) @app.route("/") -def index(): - import threading - return "Current thread: {}".format(threading.current_thread()) +def index() -> str: + return f"Current thread: {threading.current_thread()}" -if __name__ == '__main__': +if __name__ == "__main__": app.run(port=6001) diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/hello_world__with_daemon.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/hello_world__with_daemon.py index f820e00bd..1b93b6521 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/hello_world__with_daemon.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/hello_world__with_daemon.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import threading import time -def run(): +def run() -> None: print(threading.current_thread()) - for i in 'Hello World!': + for i in "Hello World!": print(i) time.sleep(0.2) -if __name__ == '__main__': +if __name__ == "__main__": thread = threading.Thread(target=run, daemon=True) thread.start() diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/hello_world__without_daemon.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/hello_world__without_daemon.py index 213a22e49..94303fb54 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/hello_world__without_daemon.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/hello_world__without_daemon.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import threading import time -def run(): +def run() -> None: print(threading.current_thread()) - for i in 'Hello World!': + for i in "Hello World!": print(i) time.sleep(0.2) -if __name__ == '__main__': +if __name__ == "__main__": thread = threading.Thread(target=run, daemon=False) thread.start() diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/mini_example__with_threading.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/mini_example__with_threading.py index f098b1c74..579a59232 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/mini_example__with_threading.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/mini_example__with_threading.py @@ -1,28 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import threading +import time -def run(name='main', sleep_seconds=None): +def run(name="main", sleep_seconds=None) -> None: if sleep_seconds: - import time time.sleep(sleep_seconds) - print('start: "{}": {}'.format(name, threading.current_thread())) + print(f'start: "{name}": {threading.current_thread()}') -if __name__ == '__main__': +if __name__ == "__main__": run() - thread = threading.Thread(target=run, args=('thread #1', 3)) + thread = threading.Thread(target=run, args=("thread #1", 3)) thread.start() - thread = threading.Thread(target=run, args=('thread #2', 1)) + thread = threading.Thread(target=run, args=("thread #2", 1)) thread.start() - thread = threading.Thread(target=run, args=('thread #3', 0)) + thread = threading.Thread(target=run, args=("thread #3", 0)) thread.start() diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/race_condition.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/race_condition.py index 7538bdccc..0a09c9fc3 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/race_condition.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/race_condition.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing.dummy import Pool as ThreadPool @@ -9,46 +9,44 @@ number = 0 -DATA = { - 'number': 0 -} +DATA = {"number": 0} lock = threading.Lock() -def inc(*args): +def inc(*args) -> None: global number - DATA['number'] += 1 + DATA["number"] += 1 number += 1 -def inc_lock(*args): +def inc_lock(*args) -> None: global number with lock: number += 1 - DATA['number'] += 1 + DATA["number"] += 1 max_number = 1_000_000 # Only one thread -- main -number = DATA['number'] = 0 +number = DATA["number"] = 0 for _ in range(max_number): inc() -print(number, DATA['number']) +print(number, DATA["number"]) # 1000000 1000000 # Many threads -number = DATA['number'] = 0 +number = DATA["number"] = 0 pool = ThreadPool() pool.map(inc, range(max_number)) -print(number, DATA['number']) +print(number, DATA["number"]) # 819903 459802 # Many threads -number = DATA['number'] = 0 +number = DATA["number"] = 0 pool = ThreadPool() pool.map(inc_lock, range(max_number)) -print(number, DATA['number']) +print(number, DATA["number"]) # 1000000 1000000 diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/sync_vs_async__with_requests_and_bs4.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/sync_vs_async__with_requests_and_bs4.py index 70f20e689..a61825691 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/sync_vs_async__with_requests_and_bs4.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/sync_vs_async__with_requests_and_bs4.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing.dummy import Pool as ThreadPool @@ -13,20 +13,22 @@ def go(url): rs = requests.get(url) - root = BeautifulSoup(rs.content, 'html.parser') - return root.select_one('.user-details > a').text.strip() + root = BeautifulSoup(rs.content, "html.parser") + return root.select_one(".user-details > a").text.strip() urls = [ - 'https://ru.stackoverflow.com/questions/962400', 'https://ru.stackoverflow.com/questions/962311', - 'https://ru.stackoverflow.com/questions/962128', 'https://ru.stackoverflow.com/questions/962396', - 'https://ru.stackoverflow.com/questions/962349' + "https://ru.stackoverflow.com/questions/962400", + "https://ru.stackoverflow.com/questions/962311", + "https://ru.stackoverflow.com/questions/962128", + "https://ru.stackoverflow.com/questions/962396", + "https://ru.stackoverflow.com/questions/962349", ] t = time.time() result = [go(url) for url in urls] print(result) -print('Elapsed {:.3f} secs'.format(time.time() - t)) +print(f"Elapsed {time.time() - t:.3f} secs") # ['Streletz', 'Kromster', 'Stepan Kasyanenko', 'Kromster', 'JamesJGoodwin'] # Elapsed 6.030 secs @@ -36,6 +38,6 @@ def go(url): pool = ThreadPool() result = pool.map(go, urls) print(result) -print('Elapsed {:.3f} secs'.format(time.time() - t)) +print(f"Elapsed {time.time() - t:.3f} secs") # ['Streletz', 'Kromster', 'Stepan Kasyanenko', 'Kromster', 'JamesJGoodwin'] # Elapsed 3.203 secs diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/thread__with__callback.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/thread__with__callback.py index 8112fe8c4..15a3a15b3 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/thread__with__callback.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/thread__with__callback.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import time from threading import Thread -def go(callback_func): +def go(callback_func) -> None: while True: time.sleep(2) callback_func(":)") -def it_callback(s): +def it_callback(s) -> None: global status status = s diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/threading_with_webservers__flask.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/threading_with_webservers__flask.py index 5fc19cd3f..e6fdcdda9 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/threading_with_webservers__flask.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/threading_with_webservers__flask.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging import threading +from flask import Flask -def run(port=80): - from flask import Flask - app = Flask(__name__) - import logging +def run(port: int = 80) -> None: + app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - html = "Hello World! (port={})".format(port) + html = f"Hello World! (port={port})" print(html) return html @@ -24,13 +24,13 @@ def index(): app.run(port=port) -if __name__ == '__main__': +if __name__ == "__main__": # NOTE: recommended to add daemon=False or call join() for each thread - thread = threading.Thread(target=run, args=(5000, )) + thread = threading.Thread(target=run, args=(5000,)) thread.start() - thread = threading.Thread(target=run, args=(5001, )) + thread = threading.Thread(target=run, args=(5001,)) thread.start() thread = threading.Thread(target=run, args=(5002,)) diff --git a/concurrency_in_python__threading_processing/multithreading__threading__examples/timeout_run_function.py b/concurrency_in_python__threading_processing/multithreading__threading__examples/timeout_run_function.py index 2b3d575be..f64ab1a3a 100644 --- a/concurrency_in_python__threading_processing/multithreading__threading__examples/timeout_run_function.py +++ b/concurrency_in_python__threading_processing/multithreading__threading__examples/timeout_run_function.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import time @@ -9,28 +9,28 @@ class User: - def __init__(self, name): + def __init__(self, name) -> None: self.name = name - def post_msg(self): + def post_msg(self) -> None: time.sleep(3) # Написать пользователю - print('Hi, {}! current_thread: {}'.format(self.name, current_thread())) + print(f"Hi, {self.name}! current_thread: {current_thread()}") -user_1 = User('Vasya') +user_1 = User("Vasya") user_1.post_msg() -user_2 = User('Petya') +user_2 = User("Petya") Thread(target=user_2.post_msg).start() -def foo(name): +def foo(name) -> None: time.sleep(5) # Написать пользователю - print('Hi, {}! current_thread: {}'.format(name, current_thread())) + print(f"Hi, {name}! current_thread: {current_thread()}") -Thread(target=lambda: foo('Thread')).start() +Thread(target=lambda: foo("Thread")).start() diff --git a/confucius_quotes.py b/confucius_quotes.py index f6ff925cd..0aedfa083 100644 --- a/confucius_quotes.py +++ b/confucius_quotes.py @@ -1,25 +1,29 @@ -from urllib.parse import quote -from grab import Grab +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +__author__ = "ipetrash" -__author__ = 'ipetrash' # Скрипт качает цитаты Конфуция из викицитатника. +from urllib.parse import quote +from grab import Grab + + def get_confucius_quotes(): - url = quote('http://ru.wikiquote.org/wiki/Конфуций', safe='/:') + url = quote("http://ru.wikiquote.org/wiki/Конфуций", safe="/:") g = Grab() g.go(url) quotes = list() - for el in g.doc.select('//h2/following-sibling::ul/li'): + for el in g.doc.select("//h2/following-sibling::ul/li"): quotes.append(el.text()) return quotes -if __name__ == '__main__': +if __name__ == "__main__": for q in get_confucius_quotes(): - print("'{}'".format(q)) \ No newline at end of file + print(f"'{q}'") diff --git a/connect_and_download_file_from_ftp.py b/connect_and_download_file_from_ftp.py index d816dc627..306ad7b7b 100644 --- a/connect_and_download_file_from_ftp.py +++ b/connect_and_download_file_from_ftp.py @@ -1,29 +1,30 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from ftplib import FTP + # Connect to host, default port -ftp = FTP('ftp.debian.org') +ftp = FTP("ftp.debian.org") print(ftp.host) # User anonymous, passwd anonymous print(ftp.login()) # Change into "debian" directory -print(ftp.cwd('debian')) +print(ftp.cwd("debian")) # List directory contents -print(ftp.retrlines('LIST')) +print(ftp.retrlines("LIST")) # Show file content -print(ftp.retrbinary('RETR README', lambda data: print(repr(data)))) +print(ftp.retrbinary("RETR README", lambda data: print(repr(data)))) # Download file -print(ftp.retrbinary('RETR README', open('README', 'wb').write)) +print(ftp.retrbinary("RETR README", open("README", "wb").write)) # Send a QUIT command to the server and close the connection print(ftp.quit()) diff --git a/console__clear_line__cr_CARRIAGE_RETURN.py b/console__clear_line__cr_CARRIAGE_RETURN.py index c819c9505..d3861816a 100644 --- a/console__clear_line__cr_CARRIAGE_RETURN.py +++ b/console__clear_line__cr_CARRIAGE_RETURN.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import time import sys +import time + for ips in (10254, 1, 10, 1, 10254, 999): - sys.stdout.write('\r' + f'Current value [{ips}]' + ' ' * 100) + sys.stdout.write("\r" + f"Current value [{ips}]" + " " * 100) sys.stdout.flush() time.sleep(0.5) diff --git a/console_change_data_in_line.py b/console_change_data_in_line.py index 9346623e7..2e79600ef 100644 --- a/console_change_data_in_line.py +++ b/console_change_data_in_line.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys import time -def f1(n): +def f1(n) -> None: last_num_chars = 0 write, flush = sys.stdout.write, sys.stdout.flush while n >= 0: - write('\r' * last_num_chars) - last_num_chars = write('[1] Left: ' + str(n)) + write("\r" * last_num_chars) + last_num_chars = write("[1] Left: " + str(n)) flush() time.sleep(1) @@ -23,13 +23,13 @@ def f1(n): print() -def f2(n): +def f2(n) -> None: last_num_chars = 0 write, flush = sys.stdout.write, sys.stdout.flush while n >= 0: - write('\b' * last_num_chars) - last_num_chars = write('[2] Left: ' + str(n)) + write("\b" * last_num_chars) + last_num_chars = write("[2] Left: " + str(n)) flush() time.sleep(1) diff --git a/contextlib__redirect__stdout_stderr/catch_stdout_and_stderr__Exception.py b/contextlib__redirect__stdout_stderr/catch_stdout_and_stderr__Exception.py index f256b64af..d5c7d14b1 100644 --- a/contextlib__redirect__stdout_stderr/catch_stdout_and_stderr__Exception.py +++ b/contextlib__redirect__stdout_stderr/catch_stdout_and_stderr__Exception.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from contextlib import redirect_stdout, redirect_stderr @@ -14,8 +14,8 @@ with redirect_stdout(f), redirect_stderr(f): try: - print('123') - print('abc') + print("123") + print("abc") 1 / 0 except: print(traceback.format_exc(), file=sys.stderr) diff --git a/contextlib__redirect__stdout_stderr/redirect__stdout_to_stderr.py b/contextlib__redirect__stdout_stderr/redirect__stdout_to_stderr.py index b37366cb8..4f119e3c1 100644 --- a/contextlib__redirect__stdout_stderr/redirect__stdout_to_stderr.py +++ b/contextlib__redirect__stdout_stderr/redirect__stdout_to_stderr.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys diff --git a/contextlib__redirect__stdout_stderr/redirect__to_file.py b/contextlib__redirect__stdout_stderr/redirect__to_file.py index 90284ef2c..9f293c9c8 100644 --- a/contextlib__redirect__stdout_stderr/redirect__to_file.py +++ b/contextlib__redirect__stdout_stderr/redirect__to_file.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from contextlib import redirect_stdout # How to write help() to a file -with open('help.txt', 'w') as f, redirect_stdout(f): +with open("help.txt", "w") as f, redirect_stdout(f): help(pow) diff --git a/contextlib__redirect__stdout_stderr/redirect__to_file_like_object.py b/contextlib__redirect__stdout_stderr/redirect__to_file_like_object.py index c1d00eb7a..db0959749 100644 --- a/contextlib__redirect__stdout_stderr/redirect__to_file_like_object.py +++ b/contextlib__redirect__stdout_stderr/redirect__to_file_like_object.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from io import StringIO @@ -9,10 +9,10 @@ with StringIO() as f, redirect_stdout(f): - print('Hello ', end='') + print("Hello ", end="") - def foo(): - print('World') + def foo() -> None: + print("World") foo() diff --git a/convert_image_to_ico/main.py b/convert_image_to_ico/main.py index 8dd63a682..6fc355776 100644 --- a/convert_image_to_ico/main.py +++ b/convert_image_to_ico/main.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def convert_image_to_ico(file_name, file_name_ico, icon_sizes=None): - from PIL import Image +from PIL import Image + + +def convert_image_to_ico(file_name, file_name_ico, icon_sizes=None) -> None: img = Image.open(file_name) if icon_sizes: @@ -14,9 +16,13 @@ def convert_image_to_ico(file_name, file_name_ico, icon_sizes=None): img.save(file_name_ico) -if __name__ == '__main__': - file_name = 'image.jpg' +if __name__ == "__main__": + file_name = "image.jpg" - convert_image_to_ico(file_name, 'logo.ico') - convert_image_to_ico(file_name, 'logo2.ico', icon_sizes=[(16, 16), (32, 32), (48, 48), (64, 64)]) - convert_image_to_ico(file_name, 'logo3.ico', icon_sizes=[(16, 16), (32, 32)]) + convert_image_to_ico(file_name, "logo.ico") + convert_image_to_ico( + file_name, + "logo2.ico", + icon_sizes=[(16, 16), (32, 32), (48, 48), (64, 64)], + ) + convert_image_to_ico(file_name, "logo3.ico", icon_sizes=[(16, 16), (32, 32)]) diff --git a/convert_numpy_array_to_QImage.py b/convert_numpy_array_to_QImage.py index ddaac2189..18d905732 100644 --- a/convert_numpy_array_to_QImage.py +++ b/convert_numpy_array_to_QImage.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +from datetime import datetime + import numpy as np + from PIL import ImageGrab from PyQt5.QtGui import qRgb, QImage -import datetime as DT - GRAY_COLOR_TABLE = [qRgb(i, i, i) for i in range(256)] @@ -21,31 +22,49 @@ def convert_numpy_array_to_QImage(numpy_array): height, width = numpy_array.shape[:2] if len(numpy_array.shape) == 2: - img = QImage(numpy_array.data, width, height, numpy_array.strides[0], QImage.Format_Indexed8) + img = QImage( + numpy_array.data, + width, + height, + numpy_array.strides[0], + QImage.Format_Indexed8, + ) img.setColorTable(GRAY_COLOR_TABLE) return img elif len(numpy_array.shape) == 3: if numpy_array.shape[2] == 3: - img = QImage(numpy_array.data, width, height, numpy_array.strides[0], QImage.Format_RGB888) + img = QImage( + numpy_array.data, + width, + height, + numpy_array.strides[0], + QImage.Format_RGB888, + ) return img elif numpy_array.shape[2] == 4: - img = QImage(numpy_array.data, width, height, numpy_array.strides[0], QImage.Format_ARGB32) + img = QImage( + numpy_array.data, + width, + height, + numpy_array.strides[0], + QImage.Format_ARGB32, + ) return img -if __name__ == '__main__': +if __name__ == "__main__": print_screen_pil = ImageGrab.grab() width, height = print_screen_pil.size[0], print_screen_pil.size[1] - print_screen_numpy = np.array(print_screen_pil.getdata(), dtype='uint8') + print_screen_numpy = np.array(print_screen_pil.getdata(), dtype="uint8") print_screen_numpy = print_screen_numpy.reshape((height, width, 3)) img = convert_numpy_array_to_QImage(print_screen_numpy) print(img.size()) - file_name = f"print_screen_{DT.datetime.now():%Y-%m-%d_%H%M%S}.png" + file_name = f"print_screen_{datetime.now():%Y-%m-%d_%H%M%S}.png" print(file_name) img.save(file_name) diff --git a/converter mils - mm/main.py b/converter mils - mm/main.py index 0fc3e048d..cc9fca452 100644 --- a/converter mils - mm/main.py +++ b/converter mils - mm/main.py @@ -1,22 +1,22 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" -def mils2mm(mils): +def mils2mm(mils: float) -> float: """Функция конвертирует mils (1/1000 дюйма) в mm (миллиметры).""" mm = (mils / 1000) * 25.4 return round(mm, 2) -def mm2mils(mm): +def mm2mils(mm: float) -> float: """Функция конвертирует mm (миллиметры) в mils (1/1000 дюйма).""" mils = (mm * 1000) / 25.4 return round(mils, 2) -if __name__ == '__main__': +if __name__ == "__main__": mm = 50.0 mils = 1968.0 - print('{} mm -> {} mils'.format(mm, mm2mils(mm))) - print('{} mils -> {} mm'.format(mils, mils2mm(mils))) \ No newline at end of file + print(f"{mm} mm -> {mm2mils(mm)} mils") + print(f"{mils} mils -> {mils2mm(mils)} mm") diff --git a/copy2clipboard.py b/copy2clipboard.py index e1b073812..95f5988f2 100644 --- a/copy2clipboard.py +++ b/copy2clipboard.py @@ -1,33 +1,34 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def to(text): +try: + from PyQt5.QtGui import QGuiApplication as QApplication + +except ImportError: try: - from PyQt5.QtGui import QGuiApplication as QApplication + from PyQt4.QtGui import QApplication except ImportError: - try: - from PyQt4.QtGui import QApplication + from PySide.QtGui import QApplication - except ImportError: - from PySide.QtGui import QApplication +def to(text: str) -> None: app = QApplication([]) app.clipboard().setText(text) app = None -if __name__ == '__main__': +if __name__ == "__main__": import sys + import os + if len(sys.argv) > 1: - text = ' '.join(sys.argv[1:]) - print('Text: "{}"'.format(text)) + text = " ".join(sys.argv[1:]) + print(f'Text: "{text}"') to(text) - else: - import os file_name = os.path.basename(sys.argv[0]) - print('usage: {} [-h] text'.format(file_name)) + print(f"usage: {file_name} [-h] text") diff --git a/copy2clipboard__via_pyperclip.py b/copy2clipboard__via_pyperclip.py index 912bc9fea..7db33e437 100644 --- a/copy2clipboard__via_pyperclip.py +++ b/copy2clipboard__via_pyperclip.py @@ -1,23 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def to(text): - import pyperclip +import pyperclip + + +def to(text: str) -> None: pyperclip.copy(text) pyperclip.paste() -if __name__ == '__main__': +if __name__ == "__main__": import sys + import os + if len(sys.argv) > 1: - text = ' '.join(sys.argv[1:]) - print('Text: "{}"'.format(text)) + text = " ".join(sys.argv[1:]) + print(f'Text: "{text}"') to(text) - else: - import os file_name = os.path.basename(sys.argv[0]) - print('usage: {} [-h] text'.format(file_name)) + print(f"usage: {file_name} [-h] text") diff --git a/copy_example.py b/copy_example.py index 26d46d9c2..b511a48da 100644 --- a/copy_example.py +++ b/copy_example.py @@ -1,21 +1,45 @@ -import copy +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" -__author__ = 'ipetrash' """RU: Пример использования модуля copy.""" -# TODO: https://docs.python.org/3.4/library/copy.html -# TODO: больше примеров -if __name__ == '__main__': - a = [2, 3, [3.5, 3.6, [3.61, 3.62]], 4, 5] - print(a, type(a), hex(id(a)), sep=', ') +import copy + + +complex_data = [ + 2, + 3, + [ + 3.5, + 3.6, + [3.61, 3.62], + ], + dict( + a=1, + b="2", + c=[True, None], + ), + 4, + 5, +] + + +def _print_complex_data(data) -> None: + print(data, id(data)) + print(data[2], id(data[2])) + print(data[2][2], id(data[2][2])) + print(data[3], id(data[3])) + print(data[3]["c"], id(data[3]["c"])) + - b = copy.deepcopy(a) - print(b, type(b), hex(id(b)), sep=', ') +_print_complex_data(complex_data) +print() - c = [2, 3, 4, 5] - print(c, type(c), hex(id(c)), sep=', ') +_print_complex_data(copy.copy(complex_data)) +print() - d = copy.copy(c) - print(d, type(d), hex(id(d)), sep=', ') \ No newline at end of file +_print_complex_data(copy.deepcopy(complex_data)) diff --git a/cors-anywhere-webserver/main.py b/cors-anywhere-webserver/main.py index 840714b06..10bac939b 100644 --- a/cors-anywhere-webserver/main.py +++ b/cors-anywhere-webserver/main.py @@ -1,27 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# TODO: append logging +import re +from urllib.request import urlopen, Request import flask + + +# TODO: append logging app = flask.Flask(__name__) app.debug = True -from urllib.request import urlopen, Request - -@app.route('/') +@app.route("/") def index(): - url = flask.request.args.get('url') - print('URL:', url) - if url is None: - return "Append url, please: {}?url=<your_url>".format(flask.request.host_url) + url = flask.request.args.get("url") + print("URL:", url) + if not url: + return f"Append url, please: {flask.request.host_url}?url=<your_url>" headers = dict() - headers['Origin'] = flask.request.host_url + headers["Origin"] = flask.request.host_url request = Request(url, headers=headers) @@ -31,12 +33,11 @@ def index(): # Нужно узнать encoding, для этого вытаскиваем xml-декларацию, а из нее уже значение encoding try: - s_index = content.find(b'') + s_index = content.find(b"") if s_index != -1 and e_index != -1: - declaration = content[s_index: e_index + len(b'?>')].decode('utf-8') + declaration = content[s_index : e_index + len(b"?>")].decode("utf-8") - import re match = re.search(r'encoding="(.+)"', declaration) if match: print(match.group(1)) @@ -49,7 +50,7 @@ def index(): rs = flask.Response(content) rs.headers.extend(headers) - rs.headers['Access-Control-Allow-Origin'] = '*' + rs.headers["Access-Control-Allow-Origin"] = "*" print(rs.headers) return rs @@ -59,4 +60,4 @@ def index(): app.run() # # Public IP - # app.run(host='0.0.0.0') \ No newline at end of file + # app.run(host='0.0.0.0') diff --git a/cpu_info/hello_world__cpuinfo.py b/cpu_info/hello_world__cpuinfo.py new file mode 100644 index 000000000..702cfc600 --- /dev/null +++ b/cpu_info/hello_world__cpuinfo.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install py-cpuinfo +import cpuinfo + + +info = cpuinfo.get_cpu_info() +print(info["brand_raw"]) +# AMD Ryzen 7 3700X 8-Core Processor diff --git a/crash_on_windows.py b/crash_on_windows.py new file mode 100644 index 000000000..a22874ffe --- /dev/null +++ b/crash_on_windows.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from ctypes import wintypes, windll, c_void_p, c_size_t, POINTER, c_ubyte, cast + + +def main() -> None: + # Define constants + FILE_MAP_ALL_ACCESS = 983071 + PAGE_READWRITE = 4 + + # Configure function arguments + windll.kernel32.CreateFileMappingA.argtypes = [ + wintypes.HANDLE, + c_void_p, + wintypes.DWORD, + wintypes.DWORD, + wintypes.DWORD, + wintypes.LPCSTR, + ] + windll.kernel32.CreateFileMappingA.restype = wintypes.HANDLE + + windll.kernel32.MapViewOfFile.argtypes = [ + wintypes.HANDLE, + wintypes.DWORD, + wintypes.DWORD, + wintypes.DWORD, + c_size_t, + ] + windll.kernel32.MapViewOfFile.restypes = wintypes.LPVOID + + # Open shared-memory + handle = windll.kernel32.CreateFileMappingA( + -1, None, PAGE_READWRITE, 0, 1024 * 1024, b"TestSHMEM" + ) + + # Obtain pointer to SHMEM buffer + ptr = windll.kernel32.MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 1024 * 1024) + arr = cast(ptr, POINTER(c_ubyte)) + + print(arr[0]) + # Process finished with exit code -1073741819 (0xC0000005) + + +if __name__ == "__main__": + main() diff --git a/crc32__examples/main.py b/crc32__examples/main.py index 1e95b86d9..ac2161cba 100644 --- a/crc32__examples/main.py +++ b/crc32__examples/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import zlib @@ -16,15 +16,15 @@ def crc32_hex_from_bytes(data: bytes) -> str: def crc32_hex_from_file(filename): - with open(filename, 'rb') as f: + with open(filename, "rb") as f: buf = f.read() return crc32_hex_from_bytes(buf) -if __name__ == '__main__': - print(crc32_from_bytes(b'hello-world')) # 2983461467 - print(crc32_hex_from_bytes(b'hello-world')) # B1D4025B +if __name__ == "__main__": + print(crc32_from_bytes(b"hello-world")) # 2983461467 + print(crc32_hex_from_bytes(b"hello-world")) # B1D4025B print() - print(crc32_hex_from_file('1.csv')) # 7EB0F2B2 - print(crc32_hex_from_file('2.csv')) # 5D884C77 + print(crc32_hex_from_file("1.csv")) # 7EB0F2B2 + print(crc32_hex_from_file("2.csv")) # 5D884C77 diff --git a/create_file_without_file_name.py b/create_file_without_file_name.py index 3c03281f1..ed18c01d1 100644 --- a/create_file_without_file_name.py +++ b/create_file_without_file_name.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -open('.txt', 'wb') -open('.this', 'wb') -open('.git', 'wb') +open(".txt", "wb") +open(".this", "wb") +open(".git", "wb") diff --git a/create_massive/create_massive.py b/create_massive/create_massive.py index 302053adc..8ee290a34 100644 --- a/create_massive/create_massive.py +++ b/create_massive/create_massive.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" def create_massive(rows, cols, init_value): @@ -16,11 +16,11 @@ def create_massive_2(rows, cols, init_value): return massive -if __name__ == '__main__': +if __name__ == "__main__": rows = 5 cols = 3 massive = create_massive(rows, cols, None) massive_2 = create_massive_2(rows, cols, 1) print(massive) - print(massive_2) \ No newline at end of file + print(massive_2) diff --git a/create_thumbnail/main.py b/create_thumbnail/main.py index 38388311d..a74577e75 100644 --- a/create_thumbnail/main.py +++ b/create_thumbnail/main.py @@ -1,27 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -SIZE_THUMBS = (75, 75) -FROM_DIR = 'images' -TO_DIR = 'thumbs' +import glob +import os +from PIL import Image -if __name__ == '__main__': - import os - if not os.path.exists(TO_DIR): - os.mkdir(TO_DIR) - import glob - for filename in glob.glob(FROM_DIR + "/*.jpg"): - filename_thumbnail = os.path.join(TO_DIR, os.path.split(filename)[1]) - print('{} -> {}'.format(filename, filename_thumbnail)) +SIZE_THUMBS = 75, 75 +FROM_DIR = "images" +TO_DIR = "thumbs" - from PIL import Image - im = Image.open(filename) - im.thumbnail(SIZE_THUMBS) - im.save(filename_thumbnail) +if not os.path.exists(TO_DIR): + os.mkdir(TO_DIR) +for filename in glob.glob(FROM_DIR + "/*.jpg"): + filename_thumbnail = os.path.join(TO_DIR, os.path.split(filename)[1]) + print(f"{filename} -> {filename_thumbnail}") + + im = Image.open(filename) + im.thumbnail(SIZE_THUMBS) + im.save(filename_thumbnail) diff --git a/create_vars__use_globals.py b/create_vars__use_globals.py index 11beb5549..78c52f113 100644 --- a/create_vars__use_globals.py +++ b/create_vars__use_globals.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -globals()['abc'] = 123 +globals()["abc"] = 123 print(abc) # 123 for key, value in {"a": 1, "b": 2}.items(): @@ -18,12 +18,12 @@ number = 0 -def counter(): +def counter() -> None: # If not exists global # if 'number' not in globals(): # globals()['number'] = 0 - globals()['number'] += 1 + globals()["number"] += 1 counter() diff --git a/cron_converter__examples/from_jenkins.py b/cron_converter__examples/from_jenkins.py new file mode 100644 index 000000000..4260a40a4 --- /dev/null +++ b/cron_converter__examples/from_jenkins.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re + + +def do_convert(cron: str) -> str: + match cron: + case "@hourly": + cron = "H * * * *" + + case "@daily" | "@midnight": + cron = "H 0 * * *" + + case "@weekly": + cron = "H H * * H" + + case "@monthly": + cron = "H H H * *" + + case "@yearly" | "@annually": + cron = "0 0 1 1 *" + + # NOTE: "H(0-29)/10 * * * *" -> "0-29/10 * * * *" + cron = re.sub(r"H\((.+?)\)", r"\1", cron) + + parts: list[str] = cron.split() + + def _process(value: str, default_value: str = "0") -> str: + return value.replace("H/", "*/").replace("H", default_value) + + # Minute + parts[0] = _process(parts[0]) + + # Hour + parts[1] = _process(parts[1]) + + # Day (month). Тут диапазон начинается с 1 + parts[2] = _process(parts[2], default_value="1") + + # Month. Тут диапазон начинается с 1 + parts[3] = _process(parts[3], default_value="1") + + # Day (week) + parts[4] = _process(parts[4]) + + cron = " ".join(parts) + + return cron + + +if __name__ == "__main__": + from datetime import datetime + + # pip install cron-converter + from cron_converter import Cron + + cron = "H */8 * * *" + cron = do_convert(cron) + cron_instance = Cron(cron) + + print(f"Cron: {cron_instance}") + + start_date = datetime.now() + print(f"Start date: {start_date}") + + schedule = cron_instance.schedule(start_date) + print(f"Next: {schedule.next().isoformat()}") + print(f"Next: {schedule.next().isoformat()}") + print(f"Next: {schedule.next().isoformat()}") + print(f"Next: {schedule.next().isoformat()}") + print(f"Next: {schedule.next().isoformat()}") diff --git a/cron_converter__examples/hello_world.py b/cron_converter__examples/hello_world.py new file mode 100644 index 000000000..016e5c170 --- /dev/null +++ b/cron_converter__examples/hello_world.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime + +# pip install cron-converter +from cron_converter import Cron + + +print("Every hour") +cron = "0 * * * *" +cron_instance = Cron(cron) + +print(f"Cron: {cron_instance}") + +start_date = datetime.now() +print(f"Start date: {start_date}") + +schedule = cron_instance.schedule(start_date) +print(f"Next: {schedule.next().isoformat()}") +print(f"Next: {schedule.next().isoformat()}") +print(f"Next: {schedule.next().isoformat()}") + +print() + +print("Every 8 hours") +cron = "0 */8 * * *" +cron_instance = Cron(cron) + +print(f"Cron: {cron_instance}") + +start_date = datetime.now() +print(f"Start date: {start_date}") + +schedule = cron_instance.schedule(start_date) +print(f"Next: {schedule.next().isoformat()}") +print(f"Next: {schedule.next().isoformat()}") +print(f"Next: {schedule.next().isoformat()}") + +print() + +print("Every 00:00") +cron = "0 0 * * *" +cron_instance = Cron(cron) + +print(f"Cron: {cron_instance}") + +start_date = datetime.now() +print(f"Start date: {start_date}") + +schedule = cron_instance.schedule(start_date) +print(f"Next: {schedule.next().isoformat()}") +print(f"Next: {schedule.next().isoformat()}") +print(f"Next: {schedule.next().isoformat()}") + +print() + +print("Every 00:00 at Saturday") +cron = "0 0 * * 6" +cron_instance = Cron(cron) + +print(f"Cron: {cron_instance}") + +start_date = datetime.now() +print(f"Start date: {start_date}") + +schedule = cron_instance.schedule(start_date) +print(f"Next: {schedule.next().isoformat()}") +print(f"Next: {schedule.next().isoformat()}") +print(f"Next: {schedule.next().isoformat()}") diff --git a/cron_converter__examples/test_from_jenkins.py b/cron_converter__examples/test_from_jenkins.py new file mode 100644 index 000000000..52ce933fb --- /dev/null +++ b/cron_converter__examples/test_from_jenkins.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime +from unittest import TestCase + +from cron_converter import Cron + +from from_jenkins import do_convert + + +class Test(TestCase): + def test_do_convert_every_15_minutes(self) -> None: + # Every fifteen minutes + cron = "H/15 * * * *" + + cron = do_convert(cron) + + cron_instance = Cron(cron) + + start_date_str = "2024-01-01T12:00:00" + schedule = cron_instance.schedule(datetime.fromisoformat(start_date_str)) + + actual: list[str] = [ + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + ] + expected = ["2024-01-01T12:00:00", "2024-01-01T12:15:00", "2024-01-01T12:30:00"] + + self.assertEqual(actual, expected) + + def test_do_convert_every_1_hours(self) -> None: + cron = "H * * * *" + + cron = do_convert(cron) + + cron_instance = Cron(cron) + + start_date_str = "2024-01-01T12:00:00" + schedule = cron_instance.schedule(datetime.fromisoformat(start_date_str)) + + actual: list[str] = [ + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + ] + expected = ["2024-01-01T12:00:00", "2024-01-01T13:00:00", "2024-01-01T14:00:00"] + + self.assertEqual(actual, expected) + + def test_do_convert_every_8_hours(self) -> None: + cron = "H */8 * * *" + + cron = do_convert(cron) + + cron_instance = Cron(cron) + + start_date_str = "2024-01-01T12:00:00" + schedule = cron_instance.schedule(datetime.fromisoformat(start_date_str)) + + actual: list[str] = [ + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + ] + expected = [ + "2024-01-01T16:00:00", + "2024-01-02T00:00:00", + "2024-01-02T08:00:00", + "2024-01-02T16:00:00", + "2024-01-03T00:00:00", + ] + + self.assertEqual(actual, expected) + + def test_do_convert_every_24_hours(self) -> None: + cron = "H 0 * * *" + + cron = do_convert(cron) + + cron_instance = Cron(cron) + + start_date_str = "2024-01-01T12:00:00" + schedule = cron_instance.schedule(datetime.fromisoformat(start_date_str)) + + actual: list[str] = [ + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + ] + expected = ["2024-01-02T00:00:00", "2024-01-03T00:00:00", "2024-01-04T00:00:00"] + + self.assertEqual(actual, expected) + + def test_do_convert_hourly(self) -> None: + cron = "@hourly" + + cron = do_convert(cron) + + cron_instance = Cron(cron) + + start_date_str = "2024-01-01T12:00:00" + schedule = cron_instance.schedule(datetime.fromisoformat(start_date_str)) + + actual: list[str] = [ + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + ] + expected = ["2024-01-01T12:00:00", "2024-01-01T13:00:00", "2024-01-01T14:00:00"] + + self.assertEqual(actual, expected) + + def test_do_convert_daily(self) -> None: + cron = "@daily" + + cron = do_convert(cron) + + cron_instance = Cron(cron) + + start_date_str = "2024-01-01T12:00:00" + schedule = cron_instance.schedule(datetime.fromisoformat(start_date_str)) + + actual: list[str] = [ + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + ] + expected = ["2024-01-02T00:00:00", "2024-01-03T00:00:00", "2024-01-04T00:00:00"] + + self.assertEqual(actual, expected) + + def test_do_convert_midnight(self) -> None: + cron = "@midnight" + + cron = do_convert(cron) + + cron_instance = Cron(cron) + + start_date_str = "2024-01-01T12:00:00" + schedule = cron_instance.schedule(datetime.fromisoformat(start_date_str)) + + actual: list[str] = [ + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + ] + expected = [ + "2024-01-02T00:00:00", + "2024-01-03T00:00:00", + "2024-01-04T00:00:00", + "2024-01-05T00:00:00", + "2024-01-06T00:00:00", + "2024-01-07T00:00:00", + ] + + self.assertEqual(actual, expected) + + def test_do_convert_weekly(self) -> None: + cron = "@weekly" + + cron = do_convert(cron) + + cron_instance = Cron(cron) + + start_date_str = "2024-01-01T12:00:00" + schedule = cron_instance.schedule(datetime.fromisoformat(start_date_str)) + + actual: list[str] = [ + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + ] + expected = [ + "2024-01-07T00:00:00", + "2024-01-14T00:00:00", + "2024-01-21T00:00:00", + "2024-01-28T00:00:00", + "2024-02-04T00:00:00", + "2024-02-11T00:00:00", + ] + + self.assertEqual(actual, expected) + + def test_do_convert_monthly(self) -> None: + cron = "@monthly" + + cron = do_convert(cron) + + cron_instance = Cron(cron) + + start_date_str = "2024-01-01T12:00:00" + schedule = cron_instance.schedule(datetime.fromisoformat(start_date_str)) + + actual: list[str] = [ + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + ] + expected = [ + "2024-02-01T00:00:00", + "2024-03-01T00:00:00", + "2024-04-01T00:00:00", + "2024-05-01T00:00:00", + "2024-06-01T00:00:00", + "2024-07-01T00:00:00", + ] + + self.assertEqual(actual, expected) + + def test_do_convert_yearly(self) -> None: + cron = "@yearly" + + cron = do_convert(cron) + + cron_instance = Cron(cron) + + start_date_str = "2024-01-01T12:00:00" + schedule = cron_instance.schedule(datetime.fromisoformat(start_date_str)) + + actual: list[str] = [ + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + ] + expected = ["2025-01-01T00:00:00", "2026-01-01T00:00:00", "2027-01-01T00:00:00"] + + self.assertEqual(actual, expected) + + def test_do_convert_annually(self) -> None: + cron = "@annually" + + cron = do_convert(cron) + + cron_instance = Cron(cron) + + start_date_str = "2024-01-01T12:00:00" + schedule = cron_instance.schedule(datetime.fromisoformat(start_date_str)) + + actual: list[str] = [ + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + ] + expected = ["2025-01-01T00:00:00", "2026-01-01T00:00:00", "2027-01-01T00:00:00"] + + self.assertEqual(actual, expected) + + def test_do_convert_complex_1(self) -> None: + # Every ten minutes in the first half of every hour + cron = "H(0-29)/10 * * * *" + + cron = do_convert(cron) + + cron_instance = Cron(cron) + + start_date_str = "2024-01-01T12:00:00" + schedule = cron_instance.schedule(datetime.fromisoformat(start_date_str)) + + actual: list[str] = [ + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + ] + expected = [ + "2024-01-01T12:00:00", + "2024-01-01T12:10:00", + "2024-01-01T12:20:00", + "2024-01-01T13:00:00", + "2024-01-01T13:10:00", + "2024-01-01T13:20:00", + "2024-01-01T14:00:00", + "2024-01-01T14:10:00", + "2024-01-01T14:20:00", + ] + + self.assertEqual(actual, expected) + + def test_do_convert_complex_2(self) -> None: + # Once every two hours at 45 minutes past the hour starting at 9:45 AM and finishing at 3:45 PM every weekday + cron = "45 9-16/2 * * 1-5" + + cron = do_convert(cron) + + cron_instance = Cron(cron) + + start_date_str = "2024-01-04T12:00:00" + schedule = cron_instance.schedule(datetime.fromisoformat(start_date_str)) + + actual: list[str] = [ + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + ] + expected = [ + "2024-01-04T13:45:00", + "2024-01-04T15:45:00", + "2024-01-05T09:45:00", + "2024-01-05T11:45:00", + "2024-01-05T13:45:00", + "2024-01-05T15:45:00", + "2024-01-08T09:45:00", + "2024-01-08T11:45:00", + ] + + self.assertEqual(actual, expected) + + def test_do_convert_complex_3(self) -> None: + # Once in every two hour slot between 8 AM and 4 PM every weekday + cron = "H H(8-15)/2 * * 1-5" + + cron = do_convert(cron) + + cron_instance = Cron(cron) + + start_date_str = "2024-01-04T12:00:00" + schedule = cron_instance.schedule(datetime.fromisoformat(start_date_str)) + + actual: list[str] = [ + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + ] + expected = [ + "2024-01-04T12:00:00", + "2024-01-04T14:00:00", + "2024-01-05T08:00:00", + "2024-01-05T10:00:00", + "2024-01-05T12:00:00", + "2024-01-05T14:00:00", + "2024-01-08T08:00:00", + "2024-01-08T10:00:00", + ] + + self.assertEqual(actual, expected) + + def test_do_convert_complex_4(self) -> None: + # Once a day on the 1st and 15th of every month except December + cron = "H H 1,15 1-11 *" + + cron = do_convert(cron) + + cron_instance = Cron(cron) + + start_date_str = "2023-10-01T12:00:00" + schedule = cron_instance.schedule(datetime.fromisoformat(start_date_str)) + + actual: list[str] = [ + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + schedule.next().isoformat(), + ] + expected = [ + "2023-10-15T00:00:00", + "2023-11-01T00:00:00", + "2023-11-15T00:00:00", + "2024-01-01T00:00:00", + "2024-01-15T00:00:00", + "2024-02-01T00:00:00", + "2024-02-15T00:00:00", + "2024-03-01T00:00:00", + ] + + self.assertEqual(actual, expected) diff --git a/crypto__gost/pygost__example__gostR34.11-94.py b/crypto__gost/pygost__example__gostR34.11-94.py index 17553577d..827739f76 100644 --- a/crypto__gost/pygost__example__gostR34.11-94.py +++ b/crypto__gost/pygost__example__gostR34.11-94.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pygost import gost341194 @@ -10,16 +10,24 @@ def get_digest(data, sbox=gost341194.DEFAULT_SBOX): if isinstance(data, str): - data = data.encode('utf-8') + data = data.encode("utf-8") gost_digest = gost341194.GOST341194(data, sbox) - return gost_digest.digest(), gost_digest.hexdigest(), base64.b64encode(gost_digest.digest()) + return ( + gost_digest.digest(), + gost_digest.hexdigest(), + base64.b64encode(gost_digest.digest()), + ) + text = """RCPT00001ОтправительMNSV10000Государственная информационная система жилищно-коммунального хозяйства735111111ПолучательGISGMPGFNCREQUEST2018-02-28T16:30:39.877+05:0061234567890""" -text = "RCPT00001ОтправительMNSV10000Государственная информационная система жилищно-коммунального хозяйства735111111ПолучательGISGMPGFNCREQUEST2018-02-28T16:30:39.877+05:0061234567890" +text = 'RCPT00001ОтправительMNSV10000Государственная информационная система жилищно-коммунального хозяйства735111111ПолучательGISGMPGFNCREQUEST2018-02-28T16:30:39.877+05:0061234567890' # print(get_digest(text)[2]) -print(get_digest(text, 'GostR3411_94_CryptoProParamSet')[2]) # /zP/7KpLyXyg6FpKsAjNNSMe3pvPjvfx24X9RdI9AYg= +print( + get_digest(text, "GostR3411_94_CryptoProParamSet")[2] +) +# /zP/7KpLyXyg6FpKsAjNNSMe3pvPjvfx24X9RdI9AYg= # # text = """\ # # diff --git a/cryptography__examples/hello_world.py b/cryptography__examples/hello_world.py index 7b34fe964..2237ceb53 100644 --- a/cryptography__examples/hello_world.py +++ b/cryptography__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/pyca/cryptography diff --git a/css_to_xpath__gui/examples.py b/css_to_xpath__gui/examples.py index 985a62e51..221af53b9 100644 --- a/css_to_xpath__gui/examples.py +++ b/css_to_xpath__gui/examples.py @@ -1,27 +1,39 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install cssselect from cssselect import HTMLTranslator + + css_to_xpath = HTMLTranslator(xhtml=True).css_to_xpath -if __name__ == '__main__': - xpath_expr = css_to_xpath('div#main > a[href]') +if __name__ == "__main__": + xpath_expr = css_to_xpath("div#main > a[href]") print(xpath_expr) # descendant-or-self::div[@id = 'main']/a[@href] - xpath_expr = css_to_xpath('div') + xpath_expr = css_to_xpath("div") print(xpath_expr) # descendant-or-self::div - xpath_expr = css_to_xpath('table:nth-last-child(1)') + xpath_expr = css_to_xpath("table:nth-last-child(1)") print(xpath_expr) # descendant-or-self::table[count(following-sibling::*) = 0] print() - for item in ('#title', '#head', '#heading', '.pageTitle', '.news_title', '.title', '.head', - '.heading', '.contentheading', '.small_header_red'): + for item in ( + "#title", + "#head", + "#heading", + ".pageTitle", + ".news_title", + ".title", + ".head", + ".heading", + ".contentheading", + ".small_header_red", + ): xpath_expr = css_to_xpath(item) print(xpath_expr) diff --git a/css_to_xpath__gui/main.py b/css_to_xpath__gui/main.py index 366519e86..7a78a57ce 100644 --- a/css_to_xpath__gui/main.py +++ b/css_to_xpath__gui/main.py @@ -1,36 +1,38 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import traceback +import sys from PyQt5 import Qt # pip install cssselect from cssselect import HTMLTranslator + + css_to_xpath = HTMLTranslator(xhtml=True).css_to_xpath -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - Qt.QMessageBox.critical(None, 'Error', text) + Qt.QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions class MainWindow(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('css_to_xpath__gui') + self.setWindowTitle("css_to_xpath__gui") self.text_edit_input = Qt.QPlainTextEdit() self.text_edit_output = Qt.QPlainTextEdit() @@ -38,11 +40,13 @@ def __init__(self): self.label_error = Qt.QLabel() self.label_error.setStyleSheet("QLabel { color : red; }") self.label_error.setTextInteractionFlags(Qt.Qt.TextSelectableByMouse) - self.label_error.setSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Preferred) + self.label_error.setSizePolicy( + Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Preferred + ) - self.button_detail_error = Qt.QPushButton('...') + self.button_detail_error = Qt.QPushButton("...") self.button_detail_error.setFixedSize(20, 20) - self.button_detail_error.setToolTip('Detail error') + self.button_detail_error.setToolTip("Detail error") self.button_detail_error.hide() self.last_error_message = None @@ -67,7 +71,7 @@ def __init__(self): self.setLayout(layout) - def on_process(self): + def on_process(self) -> None: self.text_edit_output.clear() self.label_error.clear() self.button_detail_error.hide() @@ -94,13 +98,13 @@ def on_process(self): self.last_detail_error_message = str(tb) self.button_detail_error.show() - self.label_error.setText('Error: ' + self.last_error_message) + self.label_error.setText("Error: " + self.last_error_message) - def show_detail_error_message(self): - message = self.last_error_message + '\n\n' + self.last_detail_error_message + def show_detail_error_message(self) -> None: + message = self.last_error_message + "\n\n" + self.last_detail_error_message mb = Qt.QErrorMessage() - mb.setWindowTitle('Error') + mb.setWindowTitle("Error") # Сообщение ошибки содержит отступы, символы-переходы на следующую строку, # которые поломаются при вставке через QErrorMessage.showMessage, и нет возможности # выбрать тип текста, то делаем такой хак. @@ -109,7 +113,7 @@ def show_detail_error_message(self): mb.exec_() -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = MainWindow() @@ -117,6 +121,6 @@ def show_detail_error_message(self): mw.show() # For example - mw.text_edit_input.setPlainText('div#main > a[href]') + mw.text_edit_input.setPlainText("div#main > a[href]") app.exec_() diff --git a/csv__examples/reader_csv.py b/csv__examples/reader_csv.py index 23437bf69..297307cb6 100644 --- a/csv__examples/reader_csv.py +++ b/csv__examples/reader_csv.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import csv -with open('input.csv') as f: + +with open("input.csv") as f: csv_reader = csv.reader(f, delimiter=";") for row in csv_reader: print(row) diff --git a/csv__examples/reader_csv__as_dict__DictReader.py b/csv__examples/reader_csv__as_dict__DictReader.py index 3710c8ef0..2dc9f6edc 100644 --- a/csv__examples/reader_csv__as_dict__DictReader.py +++ b/csv__examples/reader_csv__as_dict__DictReader.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import csv -with open('input.csv') as f: + +with open("input.csv") as f: csv_reader = csv.DictReader(f, delimiter=";") for row in csv_reader: - print(row['name'], row['address']) + print(row["name"], row["address"]) diff --git a/csv__examples/reader_csv__auto_dialect__using_csv_Sniffer.py b/csv__examples/reader_csv__auto_dialect__using_csv_Sniffer.py index cf0946a8b..88b9d3898 100644 --- a/csv__examples/reader_csv__auto_dialect__using_csv_Sniffer.py +++ b/csv__examples/reader_csv__auto_dialect__using_csv_Sniffer.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import csv -with open('input.csv') as f: + +with open("input.csv") as f: dialect = csv.Sniffer().sniff(f.readline()) # # With delimiters: - # dialect = csv.Sniffer().sniff(f.readline(), delimiters=[',', ';']) + # dialect = csv.Sniffer().sniff(f.readline(), delimiters=[",", ";"]) f.seek(0) csv_reader = csv.reader(f, dialect=dialect) diff --git a/csv__examples/reader_csv__from_text.py b/csv__examples/reader_csv__from_text.py index 16058dee3..5e12d0830 100644 --- a/csv__examples/reader_csv__from_text.py +++ b/csv__examples/reader_csv__from_text.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import csv +from io import StringIO + text = """\ id;name;address;zip @@ -20,11 +22,8 @@ print() # Variant 2 -from io import StringIO - csv_reader = csv.reader(StringIO(text), delimiter=";") for row in csv_reader: print(row) print() - diff --git a/csv__examples/writer_csv.py b/csv__examples/writer_csv.py index b188102d4..1899a0e51 100644 --- a/csv__examples/writer_csv.py +++ b/csv__examples/writer_csv.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://docs.python.org/3/library/csv.html#csv.writer import csv -with open('eggs.csv', 'w', newline='') as csvfile: + + +with open("eggs.csv", "w", newline="") as csvfile: writer = csv.writer(csvfile) - writer.writerow(['Spam'] * 5 + ['Baked Beans']) - writer.writerow(['Spam', 'Lovely Spam', 'Wonderful Spam']) + writer.writerow(["Spam"] * 5 + ["Baked Beans"]) + writer.writerow(["Spam", "Lovely Spam", "Wonderful Spam"]) diff --git a/csv__examples/writer_csv__DictWriter.py b/csv__examples/writer_csv__DictWriter.py index 22992e0ee..498540469 100644 --- a/csv__examples/writer_csv__DictWriter.py +++ b/csv__examples/writer_csv__DictWriter.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://docs.python.org/3/library/csv.html#csv.DictWriter @@ -9,12 +9,13 @@ import csv -with open('names.csv', 'w', encoding='utf-8', newline='') as csvfile: - fieldnames = ['first_name', 'last_name'] + +with open("names.csv", "w", encoding="utf-8", newline="") as csvfile: + fieldnames = ["first_name", "last_name"] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() - writer.writerow({'first_name': 'Baked', 'last_name': 'Beans'}) - writer.writerow({'first_name': 'Lovely', 'last_name': 'Spam'}) - writer.writerow({'first_name': 'Wonderful', 'last_name': 'Spam'}) + writer.writerow({"first_name": "Baked", "last_name": "Beans"}) + writer.writerow({"first_name": "Lovely", "last_name": "Spam"}) + writer.writerow({"first_name": "Wonderful", "last_name": "Spam"}) diff --git a/csv__examples/writer_list_of_dict.py b/csv__examples/writer_list_of_dict.py index a91961e58..eaaebbaf5 100644 --- a/csv__examples/writer_list_of_dict.py +++ b/csv__examples/writer_list_of_dict.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://docs.python.org/3/library/csv.html#csv.DictWriter @@ -10,13 +10,14 @@ import csv + items = [ - {'name': 'bob', 'age': 25, 'weight': 200}, - {'name': 'jim', 'age': 31, 'weight': 180} + {"name": "bob", "age": 25, "weight": 200}, + {"name": "jim", "age": 31, "weight": 180}, ] keys = items[0].keys() -with open('people.csv', 'w', encoding='utf-8', newline='') as f: +with open("people.csv", "w", encoding="utf-8", newline="") as f: dict_writer = csv.DictWriter(f, keys) dict_writer.writeheader() dict_writer.writerows(items) diff --git a/current_time.py b/current_time.py index 0ea0add71..1e13046dc 100644 --- a/current_time.py +++ b/current_time.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +# https://docs.python.org/3.4/library/datetime.html +# https://docs.python.org/3.4/library/time.html import datetime as dt import time -# https://docs.python.org/3.4/library/datetime.html -# https://docs.python.org/3.4/library/time.html -if __name__ == '__main__': - while True: - cur_time = dt.datetime.now().time() - print("Current time is: %s" % cur_time.strftime("%H:%M:%S"), end='\r') +while True: + cur_time = dt.datetime.now().time() + print("Current time is: %s" % cur_time.strftime("%H:%M:%S"), end="\r") - # every 0.5 second (500 millisecond) - time.sleep(0.5) + # every 0.5 second (500 millisecond) + time.sleep(0.5) diff --git a/currying__examples/hello_world.py b/currying__examples/hello_world.py index 03b5f78d8..32969378e 100644 --- a/currying__examples/hello_world.py +++ b/currying__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.wikipedia.org/wiki/Каррирование @@ -15,9 +15,11 @@ def foo(x): def a(y): def b(z): return my_sum(x, y, z) + return b + return a -print(foo(1)(2)(3)) # 6 +print(foo(1)(2)(3)) # 6 print(foo("1")("2")("3")) # 123 diff --git a/curtain for sleeping.py b/curtain for sleeping.py index eaffa8924..e9744f935 100644 --- a/curtain for sleeping.py +++ b/curtain for sleeping.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Программа при клике на кнопку разворачивается на весь экран, закрашивается черным цветом. При @@ -10,25 +10,40 @@ try: - from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QSizePolicy, QApplication + from PyQt5.QtWidgets import ( + QWidget, + QVBoxLayout, + QPushButton, + QSizePolicy, + QApplication, + ) from PyQt5.QtGui import QPainter from PyQt5.QtCore import Qt, QTimer except ImportError: - from PyQt4.QtGui import QWidget, QPainter, QVBoxLayout, QPushButton, QSizePolicy, QApplication + from PyQt4.QtGui import ( + QWidget, + QPainter, + QVBoxLayout, + QPushButton, + QSizePolicy, + QApplication, + ) from PyQt4.QtCore import Qt, QTimer class CurtainWidget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('Curtain for sleeping') + self.setWindowTitle("Curtain for sleeping") self._flags = self.windowFlags() - self._activate_button = QPushButton('Activate curtain for sleeping') - self._activate_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self._activate_button = QPushButton("Activate curtain for sleeping") + self._activate_button.setSizePolicy( + QSizePolicy.Expanding, QSizePolicy.Expanding + ) self._activate_button.clicked.connect(self._activate) # Таймер не дает, в течении 2 секунд, движением мышки вернуть окно в нормальный вид @@ -45,10 +60,10 @@ def __init__(self): self.setMouseTracking(True) - def _activate(self, _): + def _activate(self, _) -> None: self.showFullScreen() - def showNormal(self): + def showNormal(self) -> None: self._activate_button.show() self.unsetCursor() @@ -58,7 +73,7 @@ def showNormal(self): self.setWindowFlags(self._flags) self.show() - def showFullScreen(self): + def showFullScreen(self) -> None: self._activate_button.hide() self.setCursor(Qt.BlankCursor) self._timer_block_normal.start() @@ -68,13 +83,13 @@ def showFullScreen(self): super().showFullScreen() - def mouseMoveEvent(self, event): + def mouseMoveEvent(self, event) -> None: if not self._timer_block_normal.isActive() and self.isFullScreen(): self.showNormal() super().mouseMoveEvent(event) - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setBrush(Qt.black) painter.setPen(Qt.black) @@ -88,4 +103,3 @@ def paintEvent(self, event): widget.show() app.exec() - diff --git a/custom_with__context_manager/sqlite_execute.py b/custom_with__context_manager/sqlite_execute.py index 3af17175a..c27bb8f52 100644 --- a/custom_with__context_manager/sqlite_execute.py +++ b/custom_with__context_manager/sqlite_execute.py @@ -1,17 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def old_old_variant(): - import sqlite3 +import sqlite3 + +from types import TracebackType +from typing import Optional, Type + + +def old_old_variant() -> None: connect = sqlite3.connect(":memory:") try: - print(connect.execute('SELECT sqlite_version();').fetchone()) + print(connect.execute("SELECT sqlite_version();").fetchone()) - connect.executescript('''\ + connect.executescript("""\ CREATE TABLE Test ( id INTEGER PRIMARY KEY, name TEXT @@ -20,9 +25,9 @@ def old_old_variant(): INSERT INTO Test (name) VALUES ('One'); INSERT INTO Test (name) VALUES ('Two'); INSERT INTO Test (name) VALUES ('Three'); - ''') + """) - print(connect.execute('select * from Test;').fetchall()) + print(connect.execute("select * from Test;").fetchall()) print() connect.commit() @@ -31,13 +36,11 @@ def old_old_variant(): connect.close() -def old_variant(): - import sqlite3 - +def old_variant() -> None: with sqlite3.connect(":memory:") as connect: - print(connect.execute('SELECT sqlite_version();').fetchone()) + print(connect.execute("SELECT sqlite_version();").fetchone()) - connect.executescript('''\ + connect.executescript("""\ CREATE TABLE Test ( id INTEGER PRIMARY KEY, name TEXT @@ -46,31 +49,30 @@ def old_variant(): INSERT INTO Test (name) VALUES ('One'); INSERT INTO Test (name) VALUES ('Two'); INSERT INTO Test (name) VALUES ('Three'); - ''') + """) - print(connect.execute('select * from Test;').fetchall()) + print(connect.execute("select * from Test;").fetchall()) print() connect.commit() class SQLite3Connect(object): - def __init__(self, database): - import sqlite3 + def __init__(self, database) -> None: self._connect = sqlite3.connect(database) def __enter__(self): return self._connect - def __exit__(self, exc_type, exc_value, exc_traceback): + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], exc_traceback: Optional[TracebackType]) -> None: self._connect.close() -def new_variant(): +def new_variant() -> None: with SQLite3Connect(":memory:") as connect: - print(connect.execute('SELECT sqlite_version();').fetchone()) + print(connect.execute("SELECT sqlite_version();").fetchone()) - connect.executescript('''\ + connect.executescript("""\ CREATE TABLE Test ( id INTEGER PRIMARY KEY, name TEXT @@ -79,15 +81,15 @@ def new_variant(): INSERT INTO Test (name) VALUES ('One'); INSERT INTO Test (name) VALUES ('Two'); INSERT INTO Test (name) VALUES ('Three'); - ''') + """) - print(connect.execute('select * from Test;').fetchall()) + print(connect.execute("select * from Test;").fetchall()) print() connect.commit() -if __name__ == '__main__': +if __name__ == "__main__": old_old_variant() old_variant() new_variant() diff --git a/custom_with__context_manager/time_this_using_with.py b/custom_with__context_manager/time_this_using_with.py deleted file mode 100644 index 41d0a8623..000000000 --- a/custom_with__context_manager/time_this_using_with.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import time - - -class TimeThis(object): - def __init__(self, title="TimeThis"): - self.title = title - self.start_time = None - - def __enter__(self): - self.start_time = time.clock() - return self - - def __exit__(self, exc_type, exc_value, exc_traceback): - print('[{}] total time: {:.3f} sec'.format(self.title, time.clock() - self.start_time)) - - -if __name__ == '__main__': - with TimeThis(): - time.sleep(1) - - with TimeThis("Test"): - text = '' - for i in range(10 ** 6): - text += str(i) - - with TimeThis("Test"): - items = [] - for i in range(10 ** 6): - items.append(str(i)) - - text = ''.join(items) diff --git a/custom_with__context_manager/time_this_using_with__class.py b/custom_with__context_manager/time_this_using_with__class.py new file mode 100644 index 000000000..c907ff1df --- /dev/null +++ b/custom_with__context_manager/time_this_using_with__class.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from timeit import default_timer +from types import TracebackType +from typing import Optional, Type + + +class TimeThis: + def __init__(self, title: str = "TimeThis") -> None: + self.title: str = title + self.start_time: float = 0.0 + + def __enter__(self) -> "TimeThis": + self.start_time = default_timer() + return self + + def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], exc_traceback: Optional[TracebackType]) -> None: + print(f"[{self.title}] total time: {default_timer() - self.start_time:.3f} sec") + + +if __name__ == "__main__": + import time + + with TimeThis(): + time.sleep(1) + + with TimeThis("Test"): + text = "" + for i in range(10**5): + text += str(i) + + with TimeThis("Test"): + items = [] + for i in range(10**5): + items.append(str(i)) + + text = "".join(items) diff --git a/custom_with__context_manager/time_this_using_with__function.py b/custom_with__context_manager/time_this_using_with__function.py new file mode 100644 index 000000000..634d3e8ae --- /dev/null +++ b/custom_with__context_manager/time_this_using_with__function.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from contextlib import contextmanager +from timeit import default_timer + + +@contextmanager +def time_this(title: str = "TimeThis"): + start_time: float = default_timer() + try: + yield + finally: + print(f"[{title}] total time: {default_timer() - start_time:.3f} sec") + + +if __name__ == "__main__": + import time + + with time_this(): + time.sleep(1) + + with time_this("Test"): + text = "" + for i in range(10**5): + text += str(i) + + with time_this("Test"): + items = [] + for i in range(10**5): + items.append(str(i)) + + text = "".join(items) diff --git a/cydoomgeneric__examples/.gitignore b/cydoomgeneric__examples/.gitignore new file mode 100644 index 000000000..e414a02c6 --- /dev/null +++ b/cydoomgeneric__examples/.gitignore @@ -0,0 +1,2 @@ +*.wad +.savegame/ \ No newline at end of file diff --git a/cydoomgeneric__examples/README.md b/cydoomgeneric__examples/README.md new file mode 100644 index 000000000..b507075b9 --- /dev/null +++ b/cydoomgeneric__examples/README.md @@ -0,0 +1 @@ +https://github.com/wojciech-graj/cydoomgeneric \ No newline at end of file diff --git a/cydoomgeneric__examples/demopygame.py b/cydoomgeneric__examples/demopygame.py new file mode 100644 index 000000000..1b4b5b791 --- /dev/null +++ b/cydoomgeneric__examples/demopygame.py @@ -0,0 +1,79 @@ +""" + Copyright(C) 2024 Wojciech Graj + Copyright(C) 2024 Miika Lönnqvist + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +""" + +import sys +from typing import Optional + +import numpy as np +import pygame + +import cydoomgeneric as cdg + +keymap = { + pygame.K_LEFT: cdg.Keys.LEFTARROW, + pygame.K_RIGHT: cdg.Keys.RIGHTARROW, + pygame.K_UP: cdg.Keys.UPARROW, + pygame.K_DOWN: cdg.Keys.DOWNARROW, + pygame.K_COMMA: cdg.Keys.STRAFE_L, + pygame.K_PERIOD: cdg.Keys.STRAFE_R, + pygame.K_LCTRL: cdg.Keys.FIRE, + pygame.K_SPACE: cdg.Keys.USE, + pygame.K_RSHIFT: cdg.Keys.RSHIFT, + pygame.K_RETURN: cdg.Keys.ENTER, + pygame.K_ESCAPE: cdg.Keys.ESCAPE, +} + + +class PygameDoom: + + def __init__(self) -> None: + self._resx = 640 + self._resy = 400 + pygame.init() + self._screen = pygame.display.set_mode((self._resx, self._resy)) + + def draw_frame(self, pixels: np.ndarray) -> None: + pixels = np.rot90(pixels) + pixels = np.flipud(pixels) + pygame.surfarray.blit_array(self._screen, pixels[:, :, [2, 1, 0]]) + pygame.display.flip() + + def get_key(self) -> Optional[tuple[int, int]]: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + sys.exit() + + if event.type == pygame.KEYDOWN: + if event.key in keymap: + return 1, keymap[event.key] + + if event.type == pygame.KEYUP: + if event.key in keymap: + return 0, keymap[event.key] + + return None + + def set_window_title(self, t: str) -> None: + pygame.display.set_caption(t) + + +if __name__ == "__main__": + g = PygameDoom() + cdg.init(g._resx, + g._resy, + g.draw_frame, + g.get_key, + set_window_title=g.set_window_title) + cdg.main() diff --git a/cydoomgeneric__examples/download_DOOM1_WAD.py b/cydoomgeneric__examples/download_DOOM1_WAD.py new file mode 100644 index 000000000..7c8816c38 --- /dev/null +++ b/cydoomgeneric__examples/download_DOOM1_WAD.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/nneonneo/universal-doom/blob/main/DOOM1.WAD + + +from pathlib import Path +from urllib.request import urlretrieve + + +url = "https://github.com/nneonneo/universal-doom/raw/refs/heads/main/DOOM1.WAD" +path = Path(__file__).parent.resolve() / Path(url).name +print(f"Download to {path}") + +urlretrieve(url, path) diff --git a/cydoomgeneric__examples/pyqt5.py b/cydoomgeneric__examples/pyqt5.py new file mode 100644 index 000000000..153a54342 --- /dev/null +++ b/cydoomgeneric__examples/pyqt5.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +import traceback + +import cydoomgeneric as cdg +import numpy as np + +from PyQt5.QtCore import Qt, QThread, pyqtSignal +from PyQt5.QtGui import QImage, QPainter, QKeyEvent, QPaintEvent +from PyQt5.QtWidgets import QApplication, QWidget, QMessageBox + + +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}\n" + text += "".join(traceback.format_tb(tb)) + print(text) + + if isinstance(ex, KeyboardInterrupt): + QApplication.instance().quit() + return + + if QApplication.instance(): + msg_box = QMessageBox( + QMessageBox.Critical, + "Ошибка", + f"Ошибка: {ex}", + ) + msg_box.setDetailedText(text) + msg_box.setStandardButtons(QMessageBox.Ok) + msg_box.exec() + + +sys.excepthook = log_uncaught_exceptions + + +def numpy_array_to_QImage(numpy_array: np.ndarray) -> QImage: + height, width = numpy_array.shape[:2] + data: bytes = numpy_array.data.tobytes() + + return QImage( + data, + width, + height, + QImage.Format_RGB32, + ) + + +def get_key(event: QKeyEvent) -> int | None: + if event.modifiers() & Qt.ControlModifier: + return cdg.Keys.RCTRL + + if event.modifiers() & Qt.ShiftModifier: + return cdg.Keys.RSHIFT + + if event.modifiers() & Qt.AltModifier: + return cdg.Keys.LALT + + match event.key(): + case Qt.Key_W | Qt.Key_Up: + return cdg.Keys.UPARROW + case Qt.Key_A: + return cdg.Keys.STRAFE_L + case Qt.Key_D: + return cdg.Keys.STRAFE_R + case Qt.Key_S | Qt.Key_Down: + return cdg.Keys.DOWNARROW + case Qt.Key_Left: + return cdg.Keys.LEFTARROW + case Qt.Key_Right: + return cdg.Keys.RIGHTARROW + case Qt.Key_E: + return cdg.Keys.USE + case Qt.Key_Space: + return cdg.Keys.FIRE + case Qt.Key_Return: + return cdg.Keys.ENTER + case Qt.Key_Escape: + return cdg.Keys.ESCAPE + + +KEY_PRESSED: dict[int, bool] = dict() + + +class CyDoomGenericThread(QThread): + about_draw_frame = pyqtSignal(np.ndarray) + about_set_window_title = pyqtSignal(str) + + def __init__( + self, + path_wad: str, + width: int, + height: int, + ) -> None: + super().__init__() + + self.path_wad = path_wad + self.width = width + self.height = height + + def get_key(self) -> tuple[int, int] | None: + if not KEY_PRESSED: + return + + key, is_pressed = KEY_PRESSED.popitem() + return int(is_pressed), key + + def run(self) -> None: + cdg.init( + self.width, + self.height, + draw_frame=self.about_draw_frame.emit, + get_key=self.get_key, + set_window_title=self.about_set_window_title.emit, + ) + cdg.main(argv=["cydoomgeneric", "-iwad", self.path_wad]) + + +class WidgetDoom(QWidget): + def __init__(self, path_wad: str) -> None: + super().__init__() + + self._resx = 640 + self._resy = 400 + + self.thread_engine = CyDoomGenericThread( + path_wad=path_wad, + width=self._resx, + height=self._resy, + ) + self.thread_engine.about_draw_frame.connect(self.draw_frame) + self.thread_engine.about_set_window_title.connect(self.setWindowTitle) + # TODO: + # self.thread_engine.finished.connect(self.close) + self.thread_engine.start() + + self.img: QImage | None = None + + self.setFixedSize(self._resx, self._resy) + + def draw_frame(self, pixels: np.ndarray) -> None: + self.img = numpy_array_to_QImage(pixels) + self.update() + + def keyPressEvent(self, event: QKeyEvent) -> None: + key = get_key(event) + if key is not None: + KEY_PRESSED[key] = True + + def keyReleaseEvent(self, event: QKeyEvent) -> None: + key = get_key(event) + if key is not None: + KEY_PRESSED[key] = False + + def paintEvent(self, event: QPaintEvent) -> None: + if not self.img: + return + + p = QPainter(self) + p.drawImage(0, 0, self.img) + + +if __name__ == "__main__": + from pathlib import Path + + path_wad = str(Path(__file__).parent.resolve() / "DOOM1.WAD") + + app = QApplication([]) + + g = WidgetDoom(path_wad=path_wad) + g.show() + + app.exec() diff --git a/cydoomgeneric__examples/run_demopygame.py b/cydoomgeneric__examples/run_demopygame.py new file mode 100644 index 000000000..fcaa406cc --- /dev/null +++ b/cydoomgeneric__examples/run_demopygame.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import cydoomgeneric as cdg +from demopygame import PygameDoom + + +g = PygameDoom() +cdg.init( + g._resx, + g._resy, + g.draw_frame, + g.get_key, + set_window_title=g.set_window_title, +) +cdg.main(argv=["cydoomgeneric", "-iwad", "DOOM1.WAD"]) diff --git a/darknet-deeplearning-useful-scripts/change-quality.py b/darknet-deeplearning-useful-scripts/change-quality.py index 70dd47f54..8f382c7fc 100644 --- a/darknet-deeplearning-useful-scripts/change-quality.py +++ b/darknet-deeplearning-useful-scripts/change-quality.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import glob diff --git a/darknet-deeplearning-useful-scripts/create-train-list.py b/darknet-deeplearning-useful-scripts/create-train-list.py index 5712eb604..b4ef77612 100644 --- a/darknet-deeplearning-useful-scripts/create-train-list.py +++ b/darknet-deeplearning-useful-scripts/create-train-list.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import glob @@ -12,6 +12,6 @@ cur_dir = dirname(sys.argv[0]) -with open('train.txt', 'w') as f: +with open("train.txt", "w") as f: for file_name in glob.glob(f"{cur_dir}/*.png"): f.write(f"{file_name}\n") diff --git a/darknet-deeplearning-useful-scripts/remove-bad-images.py b/darknet-deeplearning-useful-scripts/remove-bad-images.py index b8f4f935d..28965c49e 100644 --- a/darknet-deeplearning-useful-scripts/remove-bad-images.py +++ b/darknet-deeplearning-useful-scripts/remove-bad-images.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os @@ -15,7 +15,7 @@ try: image = Image.open(file_name) if not image.verify(): - raise ValueError(f'bad img {file_name}') + raise ValueError(f"bad img {file_name}") except: print(f"{file_name} looks bad") try: diff --git a/darknet-deeplearning-useful-scripts/remove-empty-png.py b/darknet-deeplearning-useful-scripts/remove-empty-png.py index 6a53330c7..9c026b1f3 100644 --- a/darknet-deeplearning-useful-scripts/remove-empty-png.py +++ b/darknet-deeplearning-useful-scripts/remove-empty-png.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os @@ -9,7 +9,7 @@ for file_name in glob.glob("*.png"): - img = file_name.split('.')[0] + ".txt" + img = file_name.split(".")[0] + ".txt" if os.path.isfile(img): continue diff --git a/darknet-deeplearning-useful-scripts/remove-empty-txt.py b/darknet-deeplearning-useful-scripts/remove-empty-txt.py index 64e8a97be..afb9dc6a5 100644 --- a/darknet-deeplearning-useful-scripts/remove-empty-txt.py +++ b/darknet-deeplearning-useful-scripts/remove-empty-txt.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os @@ -9,7 +9,7 @@ for file_name in glob.glob("*.txt"): - img = file_name.split('.')[0] + ".png" + img = file_name.split(".")[0] + ".png" if os.path.isfile(img): continue diff --git a/darknet-deeplearning-useful-scripts/scour-templates.py b/darknet-deeplearning-useful-scripts/scour-templates.py index 969080cef..4da1f08e3 100644 --- a/darknet-deeplearning-useful-scripts/scour-templates.py +++ b/darknet-deeplearning-useful-scripts/scour-templates.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os @@ -11,7 +11,7 @@ print("reading") templates = [ - int(basename(f).split('.')[0]) + int(basename(f).split(".")[0]) for f in glob.glob("*.png") ] @@ -24,10 +24,10 @@ for i in templates: try: if i != index: - os.rename(f'{i}.txt', f'{index}.txt') - os.rename(f'{i}.png', f'{index}.png') + os.rename(f"{i}.txt", f"{index}.txt") + os.rename(f"{i}.png", f"{index}.png") except OSError as err: - print(f'bad index {i}') + print(f"bad index {i}") raise err index += 1 diff --git a/datetime__current_week_number.py b/datetime__current_week_number.py index e3da8e03c..b011871c8 100644 --- a/datetime__current_week_number.py +++ b/datetime__current_week_number.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +from datetime import datetime -week_number = DT.datetime.now().isocalendar()[1] -print('week_number:', week_number) + +week_number = datetime.now().isocalendar()[1] +print("week_number:", week_number) diff --git a/datetime__get_prev_month.py b/datetime__get_prev_month.py index 00d8ce655..f05c01e88 100644 --- a/datetime__get_prev_month.py +++ b/datetime__get_prev_month.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/7153449/5909792 -import datetime as DT +from datetime import date, timedelta -prev_month = DT.date.today().replace(day=1) - DT.timedelta(days=1) +prev_month = date.today().replace(day=1) - timedelta(days=1) print(prev_month) diff --git a/datetime_example/correct_datetime.py b/datetime_example/correct_datetime.py new file mode 100644 index 000000000..a70224d26 --- /dev/null +++ b/datetime_example/correct_datetime.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime, date, timedelta + + +# Текущий день может отсутствовать, поэтому ищем ближайший день +# Например, если день 31, то ищем ближайший день 30 +def correct_datetime( + dt: datetime | date, + target_year: int, + target_month: int, + target_day: int | None = None, +) -> datetime | date: + # TODO: Можно ли упростить проверку, начиная ее для дня больше 28? + # target_day больше нужен для случаев, если день больше 28 + # Можно явно перебирать дни вместе с месяцем и годом + while True: + try: + dt = dt.replace(year=target_year, month=target_month) + break + except ValueError: + dt -= timedelta(days=1) + + if target_day is not None: + # Попробуем сразу указать target_day + try: + return dt.replace(day=target_day) + except ValueError: + pass + + # Стараемся найти день, ближайший к target_day + start_day = min(dt.day, target_day) + end_day = max(dt.day, target_day) + + for day in reversed(range(start_day, end_day + 1)): + try: + return dt.replace(day=day) + except ValueError: + pass + + return dt + + +if __name__ == "__main__": + dt = datetime(year=2023, month=1, day=1, hour=12, minute=34, second=56) + + assert isinstance( + correct_datetime(dt, target_year=2023, target_month=1), + datetime, + ) + assert isinstance( + correct_datetime(dt.date(), target_year=2023, target_month=1), + date, + ) + + assert correct_datetime(dt, dt.year, dt.month, dt.day) == dt + assert correct_datetime(dt, dt.year, dt.month, dt.day + 1) == dt.replace( + day=dt.day + 1 + ) + + assert correct_datetime(dt.date(), dt.year, dt.month, dt.day) == dt.date() + assert correct_datetime( + dt.date(), dt.year, dt.month, dt.day + 1 + ) == dt.date().replace(day=dt.day + 1) + + assert correct_datetime( + datetime(year=2023, month=1, day=1, hour=12, minute=34, second=56), + target_year=2025, + target_month=4, + ) == datetime(year=2025, month=4, day=1, hour=12, minute=34, second=56) + assert correct_datetime( + datetime(year=2023, month=1, day=1, hour=12, minute=34, second=56), + target_year=2025, + target_month=4, + target_day=30, + ) == datetime(year=2025, month=4, day=30, hour=12, minute=34, second=56) + assert correct_datetime( + datetime(year=2023, month=1, day=1, hour=12, minute=34, second=56), + target_year=2025, + target_month=4, + target_day=99, + ) == datetime(year=2025, month=4, day=30, hour=12, minute=34, second=56) + + # Leap year + assert correct_datetime( + datetime(year=2024, month=2, day=29, hour=12, minute=34, second=56), + target_year=2025, + target_month=2, + ) == datetime(year=2025, month=2, day=28, hour=12, minute=34, second=56) + + assert correct_datetime( + datetime(year=2024, month=1, day=31, hour=12, minute=34, second=56), + target_year=2024, + target_month=2, + ) == datetime(year=2024, month=2, day=29, hour=12, minute=34, second=56) + assert correct_datetime( + datetime(year=2024, month=1, day=31, hour=12, minute=34, second=56), + target_year=2024, + target_month=2, + target_day=28, + ) == datetime(year=2024, month=2, day=28, hour=12, minute=34, second=56) + assert correct_datetime( + datetime(year=2024, month=2, day=28, hour=12, minute=34, second=56), + target_year=2024, + target_month=1, + target_day=31, + ) == datetime(year=2024, month=1, day=31, hour=12, minute=34, second=56) diff --git a/datetime_example/datetime_example.py b/datetime_example/datetime_example.py index 8e68b2e19..42eef045d 100644 --- a/datetime_example/datetime_example.py +++ b/datetime_example/datetime_example.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # TODO: больше примеров @@ -6,16 +6,26 @@ # http://pythonworld.ru/moduli/modul-datetime.html -from datetime import date +from datetime import datetime, date, timedelta -if __name__ == '__main__': - # Dates are easily constructed and formatted - now = date.today() - print(now) - print(now.strftime("%m-%d-%y. %d %b %Y is a %A on the %d day of %B.")) +# Dates are easily constructed and formatted +now = datetime.today() +print(now) +print(now.strftime("%m-%d-%y. %d %b %Y is a %A on the %d day of %B.")) - # Dates support calendar arithmetic - birthday = date(1964, 7, 31) - age = now - birthday - print(age.days) +# Dates support calendar arithmetic +birthday: date = datetime(year=1964, month=7, day=31) +age: timedelta = now - birthday +print(age.days) + +print() + +now = date.today() +print(now) +print(now.strftime("%m-%d-%y. %d %b %Y is a %A on the %d day of %B.")) + +# Dates support calendar arithmetic +birthday: date = date(year=1964, month=7, day=31) +age: timedelta = now - birthday +print(age.days) diff --git a/datetime_example/get_human_delta.py b/datetime_example/get_human_delta.py new file mode 100644 index 000000000..d7c13c4d3 --- /dev/null +++ b/datetime_example/get_human_delta.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import timedelta + + +def get_human_delta(delta: timedelta) -> str: + days = delta.days + + seconds_remainder = delta.seconds + hours, seconds_remainder = divmod(seconds_remainder, 3600) + minutes, seconds = divmod(seconds_remainder, 60) + + years, days = divmod(days, 365) + lines: list[str] = [] + if years > 0: + lines.append(f"{years} year{'s' if years > 1 else ''}") + + if days > 0: + lines.append(f"{days} day{'s' if days > 1 else ''}") + + lines.append(f"{hours:02d}:{minutes:02d}:{seconds:02d}") + return ", ".join(lines) + + +if __name__ == "__main__": + for expected, delta in [ + ("00:00:05", timedelta(seconds=5)), + ("02:30:00", timedelta(hours=2, minutes=30, seconds=0)), + ("02:30:00", timedelta(hours=2, minutes=30, seconds=0, microseconds=777)), + ("1 day, 02:30:00", timedelta(days=1, hours=2, minutes=30, seconds=0)), + ("2 days, 02:30:00", timedelta(days=2, hours=2, minutes=30, seconds=0)), + ("1 year, 35 days, 02:01:00", timedelta(days=400, hours=2, minutes=1)), + ("2 years, 170 days, 02:01:00", timedelta(days=900, hours=2, minutes=1)), + ]: + human_delta: str = get_human_delta(delta) + print(human_delta) + assert expected == human_delta diff --git a/datetime_example/parse_text.py b/datetime_example/parse_text.py index 19f811368..402dccbc9 100644 --- a/datetime_example/parse_text.py +++ b/datetime_example/parse_text.py @@ -1,25 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from datetime import datetime -def get_age(text): +def get_age(text: str) -> int: date_text = text.split()[0] - dt = datetime.strptime(date_text, '%d/%m/%Y') + dt = datetime.strptime(date_text, "%d/%m/%Y") years_delta = datetime.today() - dt - return int(years_delta.days / 365) + return years_delta.days // 365 -if __name__ == '__main__': - text = '31/07/1972 (45 years old)' +if __name__ == "__main__": + text = "31/07/1972 (45 years old)" age = get_age(text) print(age) # 45 - text = '18/08/1992' + text = "18/08/1992" age = get_age(text) print(age) # 25 diff --git a/datetime_strptime__and__setlocale.py b/datetime_strptime__and__setlocale.py index 2d4db7baf..1e69345ba 100644 --- a/datetime_strptime__and__setlocale.py +++ b/datetime_strptime__and__setlocale.py @@ -1,27 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT import locale +from datetime import datetime -def check(): +def check() -> None: try: - print(DT.datetime.strptime("Fri, 06-Nov-2020 21:36:45 GMT", "%a, %d-%b-%Y %H:%M:%S %Z")) + print( + datetime.strptime( + "Fri, 06-Nov-2020 21:36:45 GMT", "%a, %d-%b-%Y %H:%M:%S %Z" + ) + ) except Exception as e: - print(f'[-] {e}') + print(f"[-] {e}") check() # 2020-11-06 21:36:45 -locale.setlocale(locale.LC_TIME, 'ru') +locale.setlocale(locale.LC_TIME, "ru") check() # [-] time data 'Fri, 06-Nov-2020 21:36:45 GMT' does not match format '%a, %d-%b-%Y %H:%M:%S %Z' -locale.setlocale(locale.LC_TIME, 'C') +locale.setlocale(locale.LC_TIME, "C") check() # 2020-11-06 21:36:45 diff --git a/decode_base64_with_Incorrect_padding.py b/decode_base64_with_Incorrect_padding.py index e6bf81646..8fea6ebef 100644 --- a/decode_base64_with_Incorrect_padding.py +++ b/decode_base64_with_Incorrect_padding.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/32517907/5909792 @@ -11,21 +11,25 @@ def base64_decode(text: str) -> bytes: - text += "=" * (- len(text) % 4) + text += "=" * (-len(text) % 4) return base64.b64decode(text) -if __name__ == '__main__': - bad_b64_string = '7P1pbxvJtjXqfr+/Qnd92huoKrFVk8ABXhUl1+td7iDJ68L3+IBISbTMpYa+pMQq1cH57zeSndjMmZEyqYwYkwN7G7UsU1SMZIQ4Hmb3f9Yq1cNfK41fK7Wd6l5SqSTNyv' +if __name__ == "__main__": + bad_b64_string = "7P1pbxvJtjXqfr+/Qnd92huoKrFVk8ABXhUl1+td7iDJ68L3+IBISbTMpYa+pMQq1cH57zeSndjMmZEyqYwYkwN7G7UsU1SMZIQ4Hmb3f9Yq1cNfK41fK7Wd6l5SqSTNyv" try: print(base64.b64decode(bad_b64_string)) except Exception as e: print(e) # Incorrect padding - print(base64_decode(bad_b64_string)) # b'\xec\xfdio\x1b\xc9\xb65\xea~\xbf\xbfBw}\xda\x1b\xa8*\xb1U\x93\xc0\x01^\x15%\xd7\xeb]\xee \xc9\xeb\xc2\xf7\xf8\x80HI\xb4\xcc\xa5\x86\xbe\xa4\xc4*\xd5\xc1\xf9\xef7\x92\x9d\xd8\xcc\x99\x912\xa9\x8c\x18\x93\x03{\x1b\xb5,ST\x8cd\x848\x1ef\xf7\x7f\xd6*\xd5\xc3_+\x8d_+\xb5\x9d\xea^R\xa9$\xcd\xca' + print(base64_decode(bad_b64_string)) + # b'\xec\xfdio\x1b\xc9\xb65\xea~\xbf\xbfBw}\xda\x1b\xa8*\xb1U\x93\xc0\x01^\x15%\xd7\xeb]\xee \xc9\xeb\xc2\xf7\xf8\x80HI\xb4\xcc\xa5\x86\xbe\xa4\xc4*\xd5\xc1\xf9\xef7\x92\x9d\xd8\xcc\x99\x912\xa9\x8c\x18\x93\x03{\x1b\xb5,ST\x8cd\x848\x1ef\xf7\x7f\xd6*\xd5\xc3_+\x8d_+\xb5\x9d\xea^R\xa9$\xcd\xca' print() - good_b64_string = '7P1pbxvJtjXqfr+/Qnd92huoKrFVk8ABXhUl1+td7iDJ68L3+IBISbTMpYa+pMQq1cH57zeSndjMmZEyqYwYkwN7G7UsU1SMZIQ4Hmb3f9Yq1cNfK41fK7Wd6l5SqSTNyv==' - print(base64.b64decode(good_b64_string)) # b'\xec\xfdio\x1b\xc9\xb65\xea~\xbf\xbfBw}\xda\x1b\xa8*\xb1U\x93\xc0\x01^\x15%\xd7\xeb]\xee \xc9\xeb\xc2\xf7\xf8\x80HI\xb4\xcc\xa5\x86\xbe\xa4\xc4*\xd5\xc1\xf9\xef7\x92\x9d\xd8\xcc\x99\x912\xa9\x8c\x18\x93\x03{\x1b\xb5,ST\x8cd\x848\x1ef\xf7\x7f\xd6*\xd5\xc3_+\x8d_+\xb5\x9d\xea^R\xa9$\xcd\xca' - print(base64_decode(good_b64_string)) # b'\xec\xfdio\x1b\xc9\xb65\xea~\xbf\xbfBw}\xda\x1b\xa8*\xb1U\x93\xc0\x01^\x15%\xd7\xeb]\xee \xc9\xeb\xc2\xf7\xf8\x80HI\xb4\xcc\xa5\x86\xbe\xa4\xc4*\xd5\xc1\xf9\xef7\x92\x9d\xd8\xcc\x99\x912\xa9\x8c\x18\x93\x03{\x1b\xb5,ST\x8cd\x848\x1ef\xf7\x7f\xd6*\xd5\xc3_+\x8d_+\xb5\x9d\xea^R\xa9$\xcd\xca' + good_b64_string = "7P1pbxvJtjXqfr+/Qnd92huoKrFVk8ABXhUl1+td7iDJ68L3+IBISbTMpYa+pMQq1cH57zeSndjMmZEyqYwYkwN7G7UsU1SMZIQ4Hmb3f9Yq1cNfK41fK7Wd6l5SqSTNyv==" + print(base64.b64decode(good_b64_string)) + # b'\xec\xfdio\x1b\xc9\xb65\xea~\xbf\xbfBw}\xda\x1b\xa8*\xb1U\x93\xc0\x01^\x15%\xd7\xeb]\xee \xc9\xeb\xc2\xf7\xf8\x80HI\xb4\xcc\xa5\x86\xbe\xa4\xc4*\xd5\xc1\xf9\xef7\x92\x9d\xd8\xcc\x99\x912\xa9\x8c\x18\x93\x03{\x1b\xb5,ST\x8cd\x848\x1ef\xf7\x7f\xd6*\xd5\xc3_+\x8d_+\xb5\x9d\xea^R\xa9$\xcd\xca' + + print(base64_decode(good_b64_string)) + # b'\xec\xfdio\x1b\xc9\xb65\xea~\xbf\xbfBw}\xda\x1b\xa8*\xb1U\x93\xc0\x01^\x15%\xd7\xeb]\xee \xc9\xeb\xc2\xf7\xf8\x80HI\xb4\xcc\xa5\x86\xbe\xa4\xc4*\xd5\xc1\xf9\xef7\x92\x9d\xd8\xcc\x99\x912\xa9\x8c\x18\x93\x03{\x1b\xb5,ST\x8cd\x848\x1ef\xf7\x7f\xd6*\xd5\xc3_+\x8d_+\xb5\x9d\xea^R\xa9$\xcd\xca' diff --git a/delete_sublist.py b/delete_sublist.py index 8eda227f4..a8bdc3e50 100644 --- a/delete_sublist.py +++ b/delete_sublist.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # http://ru.stackoverflow.com/questions/495034 @@ -18,18 +18,18 @@ def find_sublist(l, m): for i in range(len(l)): try: - if tuple(l[i: i + len(m)]) == tuple(m): + if tuple(l[i : i + len(m)]) == tuple(m): yield i, i + len(m) except IndexError: pass -def delete_sublist(l, m): +def delete_sublist(l, m) -> None: for i, j in find_sublist(l, m): - del l[i: j] + del l[i:j] -print('Before: {}.'.format(l)) +print(f"Before: {l}.") delete_sublist(l, m) -print('After: {}.'.format(l)) +print(f"After: {l}.") diff --git a/deputies_by_factions.py b/deputies_by_factions.py index 60b03922c..6446371b9 100644 --- a/deputies_by_factions.py +++ b/deputies_by_factions.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -12,11 +12,11 @@ browser = RoboBrowser( - user_agent='Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0', - parser='lxml' + user_agent="Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0", + parser="lxml", ) -url = 'http://www.duma.gov.ru/structure/deputies/?letter=%D0%92%D1%81%D0%B5&by=name&order=asc' +url = "http://www.duma.gov.ru/structure/deputies/?letter=%D0%92%D1%81%D0%B5&by=name&order=asc" browser.open(url) if not browser.response.ok: print(browser.response.status_code, browser.response.reason) @@ -27,21 +27,23 @@ total = 0 # Парсинг таблицы депутатов -for i, tr in enumerate(browser.select('#lists_list_elements_35 tr')[1:], 1): - td_list = tr.select('td') +for i, tr in enumerate(browser.select("#lists_list_elements_35 tr")[1:], 1): + td_list = tr.select("td") user = td_list[1].text faction = td_list[2].text user_by_factions[faction].append(user) total += 1 -print(f'Total: {total}') +print(f"Total: {total}") # Сортировка словаря по количеству человек в партии -for faction, users in sorted(user_by_factions.items(), key=lambda x: len(x[1]), reverse=True): - print(f'{faction} ({len(users)}):') +for faction, users in sorted( + user_by_factions.items(), key=lambda x: len(x[1]), reverse=True +): + print(f"{faction} ({len(users)}):") for i, user in enumerate(users, 1): - print(f' {i}. {user}') + print(f" {i}. {user}") print() diff --git a/design_patterns__examples/Abstract Factory/example.py b/design_patterns__examples/Abstract Factory/example.py index 2b889fe84..5566162ff 100644 --- a/design_patterns__examples/Abstract Factory/example.py +++ b/design_patterns__examples/Abstract Factory/example.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Abstract Factory - Абстрактная фабрика @@ -40,22 +40,22 @@ def create_button(self) -> IButton: class WinLabel(ILabel): - def paint(self): + def paint(self) -> None: print("WinLabel") class OSXLabel(ILabel): - def paint(self): + def paint(self) -> None: print("OSXLabel") class WinButton(IButton): - def paint(self): + def paint(self) -> None: print("WinButton") class OSXButton(IButton): - def paint(self): + def paint(self) -> None: print("OSXButton") @@ -75,7 +75,7 @@ def create_button(self) -> IButton: return OSXButton() -if __name__ == '__main__': +if __name__ == "__main__": def random_appearance() -> str: import random return random.choice(["OSX", "Windows", "error"]) diff --git a/design_patterns__examples/Adapter/example__using_composition.py b/design_patterns__examples/Adapter/example__using_composition.py index de3f735a3..008874430 100644 --- a/design_patterns__examples/Adapter/example__using_composition.py +++ b/design_patterns__examples/Adapter/example__using_composition.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Adapter - Адаптер @@ -18,17 +18,17 @@ def get_picture(self): class GameConsole: - def create_game_picture(self): - return 'picture from console' + def create_game_picture(self) -> str: + return "picture from console" class Antenna: - def create_wave_picture(self): - return 'picture from wave' + def create_wave_picture(self) -> str: + return "picture from wave" class SourceGameConsoleAdapter(SourceAdapter): - def __init__(self, game_console: GameConsole): + def __init__(self, game_console: GameConsole) -> None: self.game_console = game_console def get_picture(self): @@ -36,7 +36,7 @@ def get_picture(self): class SourceAntennaAdapter(SourceAdapter): - def __init__(self, antenna: Antenna): + def __init__(self, antenna: Antenna) -> None: self.antenna = antenna def get_picture(self): @@ -44,14 +44,14 @@ def get_picture(self): class TV: - def __init__(self, source: SourceAdapter): + def __init__(self, source: SourceAdapter) -> None: self.source = source def show_picture(self): return self.source.get_picture() -if __name__ == '__main__': +if __name__ == "__main__": g = SourceGameConsoleAdapter(GameConsole()) game_tv = TV(g) print(game_tv.show_picture()) diff --git a/design_patterns__examples/Adapter/example__using_inheritance.py b/design_patterns__examples/Adapter/example__using_inheritance.py index f4a7eafa2..6fff68655 100644 --- a/design_patterns__examples/Adapter/example__using_inheritance.py +++ b/design_patterns__examples/Adapter/example__using_inheritance.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Adapter - Адаптер @@ -18,13 +18,13 @@ def get_picture(self): class GameConsole: - def create_game_picture(self): - return 'picture from console' + def create_game_picture(self) -> str: + return "picture from console" class Antenna: - def create_wave_picture(self): - return 'picture from wave' + def create_wave_picture(self) -> str: + return "picture from wave" class SourceGameConsoleAdapter(SourceAdapter, GameConsole): @@ -38,14 +38,14 @@ def get_picture(self): class TV: - def __init__(self, source: SourceAdapter): + def __init__(self, source: SourceAdapter) -> None: self.source = source def show_picture(self): return self.source.get_picture() -if __name__ == '__main__': +if __name__ == "__main__": g = SourceGameConsoleAdapter() game_tv = TV(g) print(game_tv.show_picture()) diff --git a/design_patterns__examples/Bridge/example_1/devices/__init__.py b/design_patterns__examples/Bridge/example_1/devices/__init__.py index 06334aa61..f25732790 100644 --- a/design_patterns__examples/Bridge/example_1/devices/__init__.py +++ b/design_patterns__examples/Bridge/example_1/devices/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' - +__author__ = "ipetrash" diff --git a/design_patterns__examples/Bridge/example_1/devices/device.py b/design_patterns__examples/Bridge/example_1/devices/device.py index d78717460..9a7a82e9c 100644 --- a/design_patterns__examples/Bridge/example_1/devices/device.py +++ b/design_patterns__examples/Bridge/example_1/devices/device.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from abc import ABC, abstractmethod diff --git a/design_patterns__examples/Bridge/example_1/devices/radio.py b/design_patterns__examples/Bridge/example_1/devices/radio.py index d3b88434f..d639c3eba 100644 --- a/design_patterns__examples/Bridge/example_1/devices/radio.py +++ b/design_patterns__examples/Bridge/example_1/devices/radio.py @@ -1,45 +1,45 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from devices.device import Device class Radio(Device): - def __init__(self): + def __init__(self) -> None: self._on = False self._volume = 30 self._channel = 1 - + def is_enabled(self) -> bool: return self._on - - def enable(self): + + def enable(self) -> None: self._on = True - def disable(self): + def disable(self) -> None: self._on = False - + def get_volume(self) -> int: return self._volume - - def set_volume(self, volume: int): + + def set_volume(self, volume: int) -> None: if volume > 100: self._volume = 100 elif volume < 0: self._volume = 0 else: self._volume = volume - + def get_channel(self) -> int: return self._channel - - def set_channel(self, channel: int): + + def set_channel(self, channel: int) -> None: self._channel = channel - - def print_status(self): + + def print_status(self) -> None: print("------------------------------------") print("| I'm radio.") print("| I'm " + ("enabled" if self._on else "disabled")) diff --git a/design_patterns__examples/Bridge/example_1/devices/tv.py b/design_patterns__examples/Bridge/example_1/devices/tv.py index c11592340..5adfc0437 100644 --- a/design_patterns__examples/Bridge/example_1/devices/tv.py +++ b/design_patterns__examples/Bridge/example_1/devices/tv.py @@ -1,45 +1,45 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from .device import Device class Tv(Device): - def __init__(self): + def __init__(self) -> None: self._on = False self._volume = 30 self._channel = 1 - + def is_enabled(self) -> bool: return self._on - - def enable(self): + + def enable(self) -> None: self._on = True - def disable(self): + def disable(self) -> None: self._on = False - + def get_volume(self) -> int: return self._volume - - def set_volume(self, volume: int): + + def set_volume(self, volume: int) -> None: if volume > 100: self._volume = 100 elif volume < 0: self._volume = 0 else: self._volume = volume - + def get_channel(self) -> int: return self._channel - - def set_channel(self, channel: int): + + def set_channel(self, channel: int) -> None: self._channel = channel - - def print_status(self): + + def print_status(self) -> None: print("------------------------------------") print("| I'm TV set.") print("| I'm " + ("enabled" if self._on else "disabled")) diff --git a/design_patterns__examples/Bridge/example_1/main.py b/design_patterns__examples/Bridge/example_1/main.py index bef6de6f3..745a2a1f5 100644 --- a/design_patterns__examples/Bridge/example_1/main.py +++ b/design_patterns__examples/Bridge/example_1/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Bridge — Мост @@ -17,7 +17,7 @@ from remotes.advanced_remote import AdvancedRemote -def test_device(device): +def test_device(device) -> None: print("Tests with basic remote.") basic_remote = BasicRemote(device) basic_remote.power() @@ -30,6 +30,6 @@ def test_device(device): device.print_status() -if __name__ == '__main__': +if __name__ == "__main__": test_device(Tv()) test_device(Radio()) diff --git a/design_patterns__examples/Bridge/example_1/remotes/__init__.py b/design_patterns__examples/Bridge/example_1/remotes/__init__.py index 06334aa61..f25732790 100644 --- a/design_patterns__examples/Bridge/example_1/remotes/__init__.py +++ b/design_patterns__examples/Bridge/example_1/remotes/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' - +__author__ = "ipetrash" diff --git a/design_patterns__examples/Bridge/example_1/remotes/advanced_remote.py b/design_patterns__examples/Bridge/example_1/remotes/advanced_remote.py index 59cd32213..c0367b280 100644 --- a/design_patterns__examples/Bridge/example_1/remotes/advanced_remote.py +++ b/design_patterns__examples/Bridge/example_1/remotes/advanced_remote.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from remotes.basic_remote import BasicRemote @@ -10,6 +10,6 @@ class AdvancedRemote(BasicRemote): """Улучшенный пульт""" - def mute(self): + def mute(self) -> None: print("Remote: mute") self._device.set_volume(0) diff --git a/design_patterns__examples/Bridge/example_1/remotes/basic_remote.py b/design_patterns__examples/Bridge/example_1/remotes/basic_remote.py index 2f7f821f2..80b681269 100644 --- a/design_patterns__examples/Bridge/example_1/remotes/basic_remote.py +++ b/design_patterns__examples/Bridge/example_1/remotes/basic_remote.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from remotes.remote import Remote @@ -11,28 +11,28 @@ class BasicRemote(Remote): """Стандартный пульт""" - def __init__(self, device: Device): + def __init__(self, device: Device) -> None: self._device = device - def power(self): + def power(self) -> None: print("Remote: power toggle") if self._device.is_enabled(): self._device.disable() else: self._device.enable() - - def volume_down(self): + + def volume_down(self) -> None: print("Remote: volume down") self._device.set_volume(self._device.get_volume() - 10) - def volume_up(self): + def volume_up(self) -> None: print("Remote: volume up") self._device.set_volume(self._device.get_volume() + 10) - def channel_down(self): + def channel_down(self) -> None: print("Remote: channel down") self._device.set_channel(self._device.get_channel() - 1) - def channel_up(self): + def channel_up(self) -> None: print("Remote: channel up") self._device.set_channel(self._device.get_channel() + 1) diff --git a/design_patterns__examples/Bridge/example_1/remotes/remote.py b/design_patterns__examples/Bridge/example_1/remotes/remote.py index 3c7b49a51..5baeddbed 100644 --- a/design_patterns__examples/Bridge/example_1/remotes/remote.py +++ b/design_patterns__examples/Bridge/example_1/remotes/remote.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from abc import ABC, abstractmethod diff --git a/design_patterns__examples/Bridge/example_2.py b/design_patterns__examples/Bridge/example_2.py index 1679b0c77..3e2248dd7 100644 --- a/design_patterns__examples/Bridge/example_2.py +++ b/design_patterns__examples/Bridge/example_2.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Bridge — Мост @@ -15,47 +15,51 @@ class Drawer(ABC): @abstractmethod def draw_circle(self, x: int, y: int, radius: int): pass - + class SmallCircleDrawer(Drawer): RADIUS_MULTIPLIER = 0.25 - - def draw_circle(self, x: int, y: int, radius: int): - print(f"Small circle center = {x},{y} radius = {radius * self.RADIUS_MULTIPLIER}") + + def draw_circle(self, x: int, y: int, radius: int) -> None: + print( + f"Small circle center = {x},{y} radius = {radius * self.RADIUS_MULTIPLIER}" + ) class LargeCircleDrawer(Drawer): RADIUS_MULTIPLIER = 10 - - def draw_circle(self, x: int, y: int, radius: int): - print(f"Large circle center = {x},{y} radius = {radius * self.RADIUS_MULTIPLIER}") - + + def draw_circle(self, x: int, y: int, radius: int) -> None: + print( + f"Large circle center = {x},{y} radius = {radius * self.RADIUS_MULTIPLIER}" + ) + class Shape(ABC): - def __init__(self, drawer: Drawer): + def __init__(self, drawer: Drawer) -> None: self._drawer = drawer - + @abstractmethod - def draw(self): + def draw(self) -> None: pass - + @abstractmethod - def enlarge_radius(self, multiplier: int): + def enlarge_radius(self, multiplier: int) -> None: pass - + class Circle(Shape): - def __init__(self, x: int, y: int, radius: int, drawer: Drawer): + def __init__(self, x: int, y: int, radius: int, drawer: Drawer) -> None: super().__init__(drawer) self._x = x self._y = y self._radius = radius - def draw(self): + def draw(self) -> None: self._drawer.draw_circle(self._x, self._y, self._radius) - def enlarge_radius(self, multiplier: int): + def enlarge_radius(self, multiplier: int) -> None: self._radius *= multiplier def get_x(self) -> int: @@ -67,17 +71,17 @@ def get_y(self) -> int: def get_radius(self) -> int: return self._radius - def set_x(self, x: int): + def set_x(self, x: int) -> None: self._x = x - def set_y(self, y: int): + def set_y(self, y: int) -> None: self._y = y - def set_radius(self, radius: int): + def set_radius(self, radius: int) -> None: self._radius = radius -if __name__ == '__main__': +if __name__ == "__main__": shapes = [ Circle(5, 10, 10, LargeCircleDrawer()), Circle(20, 30, 100, SmallCircleDrawer()), diff --git a/design_patterns__examples/Builder/example.py b/design_patterns__examples/Builder/example.py index 556645e3f..ea97765c8 100644 --- a/design_patterns__examples/Builder/example.py +++ b/design_patterns__examples/Builder/example.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Builder — Строитель @@ -13,91 +13,91 @@ # "Product" class Pizza: - def __init__(self): + def __init__(self) -> None: self._dough = "" self._sauce = "" self._topping = "" - def set_dough(self, dough: str): + def set_dough(self, dough: str) -> None: self._dough = dough - - def set_sauce(self, sauce: str): + + def set_sauce(self, sauce: str) -> None: self._sauce = sauce - def set_topping(self, topping: str): + def set_topping(self, topping: str) -> None: self._topping = topping - def __str__(self): + def __str__(self) -> str: return f'Pizza(dough="{self._dough}, sauce="{self._sauce}, topping="{self._topping}")' # "Abstract Builder" class PizzaBuilder(ABC): - def __init__(self): + def __init__(self) -> None: self._pizza = None def get_pizza(self) -> Pizza: return self._pizza - def create_new_pizza_product(self): + def create_new_pizza_product(self) -> None: self._pizza = Pizza() @abstractmethod - def build_dough(self): + def build_dough(self) -> None: pass @abstractmethod - def build_sauce(self): + def build_sauce(self) -> None: pass @abstractmethod - def build_topping(self): + def build_topping(self) -> None: pass # "ConcreteBuilder" class HawaiianPizzaBuilder(PizzaBuilder): - def build_dough(self): + def build_dough(self) -> None: self._pizza.set_dough("cross") - def build_sauce(self): + def build_sauce(self) -> None: self._pizza.set_sauce("mild") - def build_topping(self): + def build_topping(self) -> None: self._pizza.set_topping("ham+pineapple") # "ConcreteBuilder" class SpicyPizzaBuilder(PizzaBuilder): - def build_dough(self): + def build_dough(self) -> None: self._pizza.set_dough("pan baked") - def build_sauce(self): + def build_sauce(self) -> None: self._pizza.set_sauce("hot") - def build_topping(self): + def build_topping(self) -> None: self._pizza.set_topping("pepperoni+salami") # "Director" class Waiter: - def __init__(self): + def __init__(self) -> None: self.pizza_builder = None - def set_pizza_builder(self, pb: PizzaBuilder): + def set_pizza_builder(self, pb: PizzaBuilder) -> None: self.pizza_builder = pb def get_pizza(self) -> Pizza: return self.pizza_builder.get_pizza() - def construct_pizza(self): + def construct_pizza(self) -> None: self.pizza_builder.create_new_pizza_product() self.pizza_builder.build_dough() self.pizza_builder.build_sauce() self.pizza_builder.build_topping() -if __name__ == '__main__': +if __name__ == "__main__": # A customer ordering a pizza. hawaiianPizzaBuilder = HawaiianPizzaBuilder() diff --git a/design_patterns__examples/Builder/example2.py b/design_patterns__examples/Builder/example2.py new file mode 100644 index 000000000..9acb0c03d --- /dev/null +++ b/design_patterns__examples/Builder/example2.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from typing import Any, Self + + +class Foo: + def __init__(self) -> None: + self.items = [] + self.key_by_value = dict() + + def add_item(self, value) -> Self: + self.items.append(value) + return self + + def add_items(self, values) -> Self: + self.items += values + return self + + def set_value(self, key, value) -> Self: + self.key_by_value[key] = value + return self + + def get_value(self, key) -> Any: + return self.key_by_value[key] + + def __repr__(self) -> str: + return f"Foo" + + +foo = Foo().add_item(1).add_items("abc").set_value("x", 1).set_value("x[]", [1, 2, 3]) + +print(foo) +print(foo.items) +print(foo.key_by_value) +print(foo.get_value("x")) +print(foo.get_value("x[]")) diff --git a/design_patterns__examples/Builder/example_wok.py b/design_patterns__examples/Builder/example_wok.py index e961d901c..45e91d4f3 100644 --- a/design_patterns__examples/Builder/example_wok.py +++ b/design_patterns__examples/Builder/example_wok.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Builder — Строитель @@ -54,56 +54,92 @@ class WokTopping(NamedTuple): weight: int = 5 -WokUdon = WokBase.Factory.create(name='Удон', description='Пшеничная лапша + свежие овощи') -WokUdonSmall = WokBase.Factory.create_small(name='Удон (эконом)', description='Пшеничная лапша + свежие овощи') - -WokSoba = WokBase.Factory.create(name='Соба', description='Гречневая лапша + свежие овощи') -WokSobaSmall = WokBase.Factory.create_small(name='Соба (эконом)', description='Гречневая лапша + свежие овощи') - -WokRamen = WokBase.Factory.create_small(name='Рамен', description='Яичная лапша + свежие овощи') -WokRamenSmall = WokBase.Factory.create_small(name='Рамен (эконом)', description='Яичная лапша + свежие овощи') - -WokHarusame = WokBase.Factory.create(name='Харусаме', description='Рисовая лапша + свежие овощи') -WokHarusameSmall = WokBase.Factory.create_small(name='Харусаме (эконом)', description='Рисовая лапша + свежие овощи') - -WokRice = WokBase.Factory.create(name='Рис', description='Цельнозерный рис + свежие овощи') -WokRiceSmall = WokBase.Factory.create_small(name='Рис (эконом)', description='Цельнозерный рис + свежие овощи') - - -WokAdditiveChicken = WokAdditive.Factory.create(name='Курица', price=58) -WokAdditiveChickenSmall = WokAdditive.Factory.create_small(name='Курица (эконом)', price=38) - -WokAdditiveBeef = WokAdditive.Factory.create(name='Говядина', price=84) -WokAdditiveBeefSmall = WokAdditive.Factory.create_small(name='Говядина (эконом)', price=64) - -WokAdditiveShrimp = WokAdditive.Factory.create(name='Креветка', price=109) -WokAdditiveShrimpSmall = WokAdditive.Factory.create_small(name='Креветка (эконом)', price=89) - -WokAdditiveSquid = WokAdditive.Factory.create(name='Кальмар', price=95) -WokAdditiveSquidSmall = WokAdditive.Factory.create_small(name='Кальмар (эконом)', price=75) - -WokAdditiveSeafood = WokAdditive.Factory.create(name='Морепродукты', price=94) -WokAdditiveSeafoodSmall = WokAdditive.Factory.create_small(name='Морепродукты (эконом)', price=74) - -WokAdditiveSalmon = WokAdditive.Factory.create(name='Лосось', price=102) -WokAdditiveSalmonSmall = WokAdditive.Factory.create_small(name='Лосось (эконом)', price=82) - -WokAdditiveShiitakeMushrooms = WokAdditive.Factory.create(name='Грибы Шиитаке', price=44) -WokAdditiveShiitakeMushroomsSmall = WokAdditive.Factory.create_small(name='Грибы Шиитаке (эконом)', price=24) - - -WokSauceCreamy = WokSauce(name='Сливочный соус') -WokSauceOyster = WokSauce(name='Устричный соус') -WokSauceCheese = WokSauce(name='Сырный соус') -WokSauceTeriyaki = WokSauce(name='Терияки соус') -WokSauceAcute = WokSauce(name='Острый соус') - - -WokToppingFriedOnions = WokTopping(name='Лук обжаренный') -WokToppingGreenOnion = WokTopping(name='Лук зеленый') -WokToppingRoastedSesame = WokTopping(name='Кунжут обжаренный') -WokToppingChilli = WokTopping(name='Перец чили') -WokToppingParmesanCheese = WokTopping(name='Сыр пармезан') +WokUdon = WokBase.Factory.create( + name="Удон", description="Пшеничная лапша + свежие овощи" +) +WokUdonSmall = WokBase.Factory.create_small( + name="Удон (эконом)", description="Пшеничная лапша + свежие овощи" +) + +WokSoba = WokBase.Factory.create( + name="Соба", description="Гречневая лапша + свежие овощи" +) +WokSobaSmall = WokBase.Factory.create_small( + name="Соба (эконом)", description="Гречневая лапша + свежие овощи" +) + +WokRamen = WokBase.Factory.create_small( + name="Рамен", description="Яичная лапша + свежие овощи" +) +WokRamenSmall = WokBase.Factory.create_small( + name="Рамен (эконом)", description="Яичная лапша + свежие овощи" +) + +WokHarusame = WokBase.Factory.create( + name="Харусаме", description="Рисовая лапша + свежие овощи" +) +WokHarusameSmall = WokBase.Factory.create_small( + name="Харусаме (эконом)", description="Рисовая лапша + свежие овощи" +) + +WokRice = WokBase.Factory.create( + name="Рис", description="Цельнозерный рис + свежие овощи" +) +WokRiceSmall = WokBase.Factory.create_small( + name="Рис (эконом)", description="Цельнозерный рис + свежие овощи" +) + + +WokAdditiveChicken = WokAdditive.Factory.create(name="Курица", price=58) +WokAdditiveChickenSmall = WokAdditive.Factory.create_small( + name="Курица (эконом)", price=38 +) + +WokAdditiveBeef = WokAdditive.Factory.create(name="Говядина", price=84) +WokAdditiveBeefSmall = WokAdditive.Factory.create_small( + name="Говядина (эконом)", price=64 +) + +WokAdditiveShrimp = WokAdditive.Factory.create(name="Креветка", price=109) +WokAdditiveShrimpSmall = WokAdditive.Factory.create_small( + name="Креветка (эконом)", price=89 +) + +WokAdditiveSquid = WokAdditive.Factory.create(name="Кальмар", price=95) +WokAdditiveSquidSmall = WokAdditive.Factory.create_small( + name="Кальмар (эконом)", price=75 +) + +WokAdditiveSeafood = WokAdditive.Factory.create(name="Морепродукты", price=94) +WokAdditiveSeafoodSmall = WokAdditive.Factory.create_small( + name="Морепродукты (эконом)", price=74 +) + +WokAdditiveSalmon = WokAdditive.Factory.create(name="Лосось", price=102) +WokAdditiveSalmonSmall = WokAdditive.Factory.create_small( + name="Лосось (эконом)", price=82 +) + +WokAdditiveShiitakeMushrooms = WokAdditive.Factory.create( + name="Грибы Шиитаке", price=44 +) +WokAdditiveShiitakeMushroomsSmall = WokAdditive.Factory.create_small( + name="Грибы Шиитаке (эконом)", price=24 +) + + +WokSauceCreamy = WokSauce(name="Сливочный соус") +WokSauceOyster = WokSauce(name="Устричный соус") +WokSauceCheese = WokSauce(name="Сырный соус") +WokSauceTeriyaki = WokSauce(name="Терияки соус") +WokSauceAcute = WokSauce(name="Острый соус") + + +WokToppingFriedOnions = WokTopping(name="Лук обжаренный") +WokToppingGreenOnion = WokTopping(name="Лук зеленый") +WokToppingRoastedSesame = WokTopping(name="Кунжут обжаренный") +WokToppingChilli = WokTopping(name="Перец чили") +WokToppingParmesanCheese = WokTopping(name="Сыр пармезан") class Wok: @@ -113,29 +149,25 @@ class Wok: sauce_additional: WokSauce = None topping: List[WokTopping] = None - def __init__(self): + def __init__(self) -> None: self.topping = [] def get_order_text(self) -> str: - """ Текст заказа """ + """Текст заказа""" - text = 'Заказ:\n' + text = "Заказ:\n" for item in self.get_order_items(): - text += f' {item.name:25} : {item.price} рублей\n' + text += f" {item.name:25} : {item.price} рублей\n" - text += ' {:25} : {} рублей'.format('', self.get_order_price()) + text += f" {'':25} : {self.get_order_price()} рублей" return text def get_order_items(self) -> list: - """ Элементы заказа """ + """Элементы заказа""" - items = [ - self.base, - self.additive, - self.sauce - ] + items = [self.base, self.additive, self.sauce] if self.sauce_additional: items.append(self.sauce_additional) @@ -145,87 +177,95 @@ def get_order_items(self) -> list: return items def get_order_price(self) -> int: - """ Стоимость заказа """ + """Стоимость заказа""" return sum(x.price for x in self.get_order_items()) def get_order_weight(self) -> int: - """ Примерный вес заказа """ + """Примерный вес заказа""" return sum(x.weight for x in self.get_order_items()) class Builder: - def __init__(self): + def __init__(self) -> None: self.wok = Wok() - def set_base(self, base: WokBase) -> 'Builder': - """ Основа """ + def set_base(self, base: WokBase) -> "Builder": + """Основа""" self.wok.base = base return self - def set_additive(self, additive: WokAdditive) -> 'Builder': - """ Добавка """ + def set_additive(self, additive: WokAdditive) -> "Builder": + """Добавка""" self.wok.additive = additive return self - def set_sauce(self, sauce: WokSauce) -> 'Builder': - """ Соус """ + def set_sauce(self, sauce: WokSauce) -> "Builder": + """Соус""" self.wok.sauce = sauce._replace(price=0) return self - def set_sauce_additional(self, sauce: WokSauce) -> 'Builder': - """ Дополнительный соус """ + def set_sauce_additional(self, sauce: WokSauce) -> "Builder": + """Дополнительный соус""" self.wok.sauce_additional = sauce return self - def add_topping(self, topping: WokTopping) -> 'Builder': - """ Топпинг """ + def add_topping(self, topping: WokTopping) -> "Builder": + """Топпинг""" self.wok.topping.append(topping) return self - def build(self) -> 'Wok': + def build(self) -> "Wok": return self.wok class WokDirector: @staticmethod def make_economy() -> Wok: - return Wok.Builder()\ - .set_base(WokSoba) \ - .set_additive(WokAdditiveChickenSmall) \ - .set_sauce(WokSauceCheese) \ + return ( + Wok.Builder() + .set_base(WokSoba) + .set_additive(WokAdditiveChickenSmall) + .set_sauce(WokSauceCheese) .build() + ) @staticmethod def make_business_lunch() -> Wok: - return Wok.Builder()\ - .set_base(WokUdon) \ - .set_additive(WokAdditiveChicken) \ - .set_sauce(WokSauceCreamy) \ + return ( + Wok.Builder() + .set_base(WokUdon) + .set_additive(WokAdditiveChicken) + .set_sauce(WokSauceCreamy) .build() + ) @staticmethod def make_vegetarian() -> Wok: - return Wok.Builder()\ - .set_base(WokUdon) \ - .set_additive(WokAdditiveShiitakeMushrooms) \ - .set_sauce(WokSauceTeriyaki) \ - .add_topping(WokToppingGreenOnion) \ + return ( + Wok.Builder() + .set_base(WokUdon) + .set_additive(WokAdditiveShiitakeMushrooms) + .set_sauce(WokSauceTeriyaki) + .add_topping(WokToppingGreenOnion) .build() + ) -if __name__ == '__main__': - wok = Wok.Builder()\ - .set_base(WokUdon)\ - .set_additive(WokAdditiveBeef)\ - .set_sauce(WokSauceCheese)\ +if __name__ == "__main__": + wok = ( + Wok.Builder() + .set_base(WokUdon) + .set_additive(WokAdditiveBeef) + .set_sauce(WokSauceCheese) .build() - print(wok.get_order_price()) # 213 + ) + print(wok.get_order_price()) # 213 print(wok.get_order_weight()) # 350 print(wok.get_order_text()) # Заказ: @@ -235,15 +275,17 @@ def make_vegetarian() -> Wok: # : 213 рублей print() - wok = Wok.Builder()\ - .set_base(WokSoba)\ - .set_additive(WokAdditiveBeef)\ - .set_sauce(WokSauceCheese)\ - .add_topping(WokToppingFriedOnions)\ - .add_topping(WokToppingGreenOnion)\ - .add_topping(WokToppingChilli)\ + wok = ( + Wok.Builder() + .set_base(WokSoba) + .set_additive(WokAdditiveBeef) + .set_sauce(WokSauceCheese) + .add_topping(WokToppingFriedOnions) + .add_topping(WokToppingGreenOnion) + .add_topping(WokToppingChilli) .build() - print(wok.get_order_price()) # 243 + ) + print(wok.get_order_price()) # 243 print(wok.get_order_weight()) # 365 print(wok.get_order_text()) # Заказ: @@ -255,7 +297,7 @@ def make_vegetarian() -> Wok: # Перец чили : 10 рублей # : 243 рублей - print('\n') + print("\n") print(WokDirector.make_economy().get_order_text()) print(WokDirector.make_business_lunch().get_order_text()) diff --git a/design_patterns__examples/Chain of responsibility/example_1.py b/design_patterns__examples/Chain of responsibility/example_1.py index 9f27c3a26..388756434 100644 --- a/design_patterns__examples/Chain of responsibility/example_1.py +++ b/design_patterns__examples/Chain of responsibility/example_1.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Chain of responsibility — Цепочка обязанностей @@ -18,17 +18,17 @@ class Handler(ABC): Он также объявляет метод для обработки. """ - _next_handler: 'Handler' = None + _next_handler: "Handler" = None - def set_next(self, handler: 'Handler') -> 'Handler': + def set_next(self, handler: "Handler") -> "Handler": self._next_handler = handler return self._next_handler @abstractmethod - def handle(self, obj): + def handle(self, obj) -> None: pass - def next_handle(self, obj): + def next_handle(self, obj) -> None: # Вызываем следующий обработки if self._next_handler: self._next_handler.handle(obj) @@ -43,7 +43,7 @@ def handle(self, obj): class IsNotStringHandler(Handler): - def handle(self, obj): + def handle(self, obj) -> None: if type(obj) != str: raise Exception(f"Object {repr(obj)} is not string!") @@ -51,35 +51,36 @@ def handle(self, obj): class IsNotMatchReHandler(Handler): - def __init__(self, re_pattern: str): + def __init__(self, re_pattern: str) -> None: self._re_pattern = re_pattern - def handle(self, obj): + def handle(self, obj) -> None: if not re.search(self._re_pattern, obj): - raise Exception(f'String {repr(obj)} is not matching by regexp: "{self._re_pattern}"!') + raise Exception( + f'String {repr(obj)} is not matching by regexp: "{self._re_pattern}"!' + ) self.next_handle(obj) -if __name__ == '__main__': - def client_code(handler: Handler): - for obj in [None, '123', '456', '111', 456]: - print(f'Object {repr(obj)} is ', end='') +if __name__ == "__main__": + def client_code(handler: Handler) -> None: + for obj in [None, "123", "456", "111", 456]: + print(f"Object {repr(obj)} is ", end="") try: handler.handle(obj) - print('ok!') + print("ok!") except Exception as e: print(f'fail -> "{e}"') - is_not_none = IsNotNoneHandler() is_not_string = IsNotStringHandler() - is_not_match_re_1 = IsNotMatchReHandler("\d") + is_not_match_re_1 = IsNotMatchReHandler(r"\d") is_not_match_re_2 = IsNotMatchReHandler("1..") - is_not_match_re_3 = IsNotMatchReHandler("\d{3}") + is_not_match_re_3 = IsNotMatchReHandler(r"\d{3}") is_not_match_re_1.set_next(is_not_match_re_2).set_next(is_not_match_re_3) # Check only None diff --git a/design_patterns__examples/Chain of responsibility/example_2.py b/design_patterns__examples/Chain of responsibility/example_2.py index 3a440a4cf..b1b310fe3 100644 --- a/design_patterns__examples/Chain of responsibility/example_2.py +++ b/design_patterns__examples/Chain of responsibility/example_2.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Chain of responsibility — Цепочка обязанностей @@ -10,44 +10,44 @@ class Car: - def __init__(self): + def __init__(self) -> None: self.name = None self.km = 11100 self.fuel = 5 self.oil = 5 -def handle_fuel(car): +def handle_fuel(car) -> None: if car.fuel < 10: print("Added fuel") car.fuel = 100 -def handle_km(car): +def handle_km(car) -> None: if car.km > 10000: print("Made a car test.") car.km = 0 -def handle_oil(car): +def handle_oil(car) -> None: if car.oil < 10: print("Added oil") car.oil = 100 class Garage: - def __init__(self): + def __init__(self) -> None: self.handlers = [] - def add_handler(self, handler): + def add_handler(self, handler) -> None: self.handlers.append(handler) - def handle_car(self, car): + def handle_car(self, car) -> None: for handler in self.handlers: handler(car) -if __name__ == '__main__': +if __name__ == "__main__": handlers = [handle_fuel, handle_km, handle_oil] garage = Garage() diff --git a/design_patterns__examples/Chain of responsibility/example_3.py b/design_patterns__examples/Chain of responsibility/example_3.py index f1877f420..7b6461f84 100644 --- a/design_patterns__examples/Chain of responsibility/example_3.py +++ b/design_patterns__examples/Chain of responsibility/example_3.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Chain of responsibility — Цепочка обязанностей @@ -22,6 +22,7 @@ class LogLevel(IntEnum): """ Log Levels Enum. """ + NONE = auto() INFO = auto() DEBUG = auto() @@ -37,7 +38,7 @@ class Logger(ABC): Abstract handler in chain of responsibility pattern. """ - def __init__(self, levels: List[LogLevel]): + def __init__(self, levels: List[LogLevel]) -> None: """ Initialize new logger @@ -48,7 +49,7 @@ def __init__(self, levels: List[LogLevel]): self._next = None self._log_levels = list(levels) - def set_next(self, next_logger: 'Logger') -> 'Logger': + def set_next(self, next_logger: "Logger") -> "Logger": """ Set next responsible logger in the chain. @@ -61,7 +62,7 @@ def set_next(self, next_logger: 'Logger') -> 'Logger': self._next = next_logger return self._next - def message(self, msg: str, severity: LogLevel): + def message(self, msg: str, severity: LogLevel) -> None: """ Message writer handler. @@ -90,7 +91,7 @@ def write_message(self, msg: str): class ConsoleLogger(Logger): - def write_message(self, msg: str): + def write_message(self, msg: str) -> None: """ Overrides parent's abstract method to write to console. @@ -107,7 +108,8 @@ class EmailLogger(Logger): Args: msg (str): Message string. """ - def write_message(self, msg: str): + + def write_message(self, msg: str) -> None: print("Sending via email:", msg) @@ -118,11 +120,12 @@ class FileLogger(Logger): Args: msg (str): Message string. """ - def write_message(self, msg: str): + + def write_message(self, msg: str) -> None: print("Writing to log file:", msg) -def main(): +def main() -> None: """ Building the chain of responsibility. """ diff --git a/design_patterns__examples/Chain of responsibility/example_4.py b/design_patterns__examples/Chain of responsibility/example_4.py index 0bd1e40e7..4e1f86e83 100644 --- a/design_patterns__examples/Chain of responsibility/example_4.py +++ b/design_patterns__examples/Chain of responsibility/example_4.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Chain of responsibility — Цепочка обязанностей @@ -14,7 +14,7 @@ # Вспомогательный класс, описывающий некоторое преступление class CriminalAction: - def __init__(self, complexity: int, description: str): + def __init__(self, complexity: int, description: str) -> None: # Сложность дела self.complexity = complexity @@ -24,10 +24,10 @@ def __init__(self, complexity: int, description: str): # Абстрактный полицейский, который может заниматься расследованием преступлений class Policeman(ABC): - def __init__(self, deduction: int): + def __init__(self, deduction: int) -> None: # Дедукция (умение распутывать сложные дела) у данного полицейского self.deduction = deduction - + # Более умелый полицейский, который получит дело, если для текущего оно слишком сложное self.next: Policeman = None @@ -38,45 +38,55 @@ def _investigate_сoncrete(self, description: str): # Добавляет в цепочку ответственности более опытного полицейского, который сможет принять на себя # расследование, если текущий не справится - def set_next(self, policeman: 'Policeman') -> 'Policeman': + def set_next(self, policeman: "Policeman") -> "Policeman": self.next = policeman return self.next # Полицейский начинает расследование или, если дело слишком сложное, передает ее более опытному коллеге - def investigate(self, criminal_action: CriminalAction): + def investigate(self, criminal_action: CriminalAction) -> None: if self.deduction < criminal_action.complexity: if self.next: self.next.investigate(criminal_action) else: print("Это дело не раскрыть никому.") - + else: self._investigate_сoncrete(criminal_action.description) class MartinRiggs(Policeman): - def _investigate_сoncrete(self, description: str): - print("Расследование по делу \"" + description + "\" ведет сержант Мартин Риггс") + def _investigate_сoncrete(self, description: str) -> None: + print('Расследование по делу "' + description + '" ведет сержант Мартин Риггс') class JohnMcClane(Policeman): - def _investigate_сoncrete(self, description: str): - print("Расследование по делу \"" + description + "\" ведет детектив Джон Макклейн") + def _investigate_сoncrete(self, description: str) -> None: + print( + 'Расследование по делу "' + description + '" ведет детектив Джон Макклейн' + ) class VincentHanna(Policeman): - def _investigate_сoncrete(self, description: str): - print("Расследование по делу \"" + description + "\" ведет лейтенант Винсент Ханна") + def _investigate_сoncrete(self, description: str) -> None: + print( + 'Расследование по делу "' + description + '" ведет лейтенант Винсент Ханна' + ) -if __name__ == '__main__': +if __name__ == "__main__": print("OUTPUT:") policeman = MartinRiggs(3) # Полицейский с наименьшим навыком ведения расследований # Добавляем ему двух опытных коллег policeman.set_next(JohnMcClane(5)).set_next(VincentHanna(8)) - policeman.investigate(CriminalAction(2, "Торговля наркотиками из Вьетнама")) - policeman.investigate(CriminalAction(7, "Дерзкое ограбление банка в центре Лос-Анджелеса")) - policeman.investigate(CriminalAction(5, "Серия взрывов в центре Нью-Йорка")) + policeman.investigate( + CriminalAction(2, "Торговля наркотиками из Вьетнама") + ) + policeman.investigate( + CriminalAction(7, "Дерзкое ограбление банка в центре Лос-Анджелеса") + ) + policeman.investigate( + CriminalAction(5, "Серия взрывов в центре Нью-Йорка") + ) # OUTPUT: # Расследование по делу "Торговля наркотиками из Вьетнама" ведет сержант Мартин Риггс diff --git a/design_patterns__examples/Chain of responsibility/example_5.py b/design_patterns__examples/Chain of responsibility/example_5.py index e80e0f892..bec7ed0cc 100644 --- a/design_patterns__examples/Chain of responsibility/example_5.py +++ b/design_patterns__examples/Chain of responsibility/example_5.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Chain of responsibility — Цепочка обязанностей @@ -20,7 +20,7 @@ class Handler(ABC): """ @abstractmethod - def set_next(self, handler: 'Handler') -> 'Handler': + def set_next(self, handler: "Handler") -> "Handler": pass @abstractmethod @@ -89,7 +89,7 @@ def client_code(handler: Handler) -> None: """ for food in ["Nut", "Banana", "Cup of coffee"]: - print(f'\nClient: Who wants a {food}?') + print(f"\nClient: Who wants a {food}?") result = handler.handle(food) if result: print(f" {result}", end="") @@ -103,11 +103,11 @@ def print_chain(handler: Handler) -> None: chain = [] while handler: - name = handler.__class__.__name__.replace('Handler', '') + name = handler.__class__.__name__.replace("Handler", "") chain.append(name) handler = handler._next_handler - print('Chain: ' + ' > '.join(chain)) + print("Chain: " + " > ".join(chain)) if __name__ == "__main__": @@ -127,7 +127,7 @@ def print_chain(handler: Handler) -> None: # Клиент должен иметь возможность отправлять запрос любому обработчику, а не # только первому в цепочке. - print('OUTPUT:') + print("OUTPUT:") print("Chain: Monkey > Squirrel > Dog") client_code(monkey) diff --git a/design_patterns__examples/Chain of responsibility/example_6.py b/design_patterns__examples/Chain of responsibility/example_6.py index b8e2c7458..0b5be3e07 100644 --- a/design_patterns__examples/Chain of responsibility/example_6.py +++ b/design_patterns__examples/Chain of responsibility/example_6.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Chain of responsibility — Цепочка обязанностей @@ -20,33 +20,33 @@ def get_timestamp() -> int: # Базовый класс цепочки. class Middleware(ABC): - def __init__(self): - self._next: 'Middleware' = None - + def __init__(self) -> None: + self._next: "Middleware" = None + # Помогает строить цепь из объектов-проверок. - def link_with(self, next: 'Middleware') -> 'Middleware': + def link_with(self, next: "Middleware") -> "Middleware": self._next = next return next - + # Подклассы реализуют в этом методе конкретные проверки. @abstractmethod def check(self, email: str, password: str) -> bool: pass - + # Запускает проверку в следующем объекте или завершает проверку, если мы в # последнем элементе цепи. def _check_next(self, email: str, password: str) -> bool: if not self._next: return True - + return self._next.check(email, password) # Конкретный элемент цепи обрабатывает запрос по-своему. class ThrottlingMiddleware(Middleware): - def __init__(self, request_per_minute: int): + def __init__(self, request_per_minute: int) -> None: super().__init__() - + self._request: int = 0 self._request_per_minute: int = request_per_minute self._current_time: int = get_timestamp() @@ -62,30 +62,30 @@ def check(self, email: str, password: str) -> bool: self._current_time = get_timestamp() self._request += 1 - + if self._request > self._request_per_minute: print("Request limit exceeded!") return False - + return self._check_next(email, password) # Конкретный элемент цепи обрабатывает запрос по-своему. class UserExistsMiddleware(Middleware): - def __init__(self, server: 'Server'): + def __init__(self, server: "Server") -> None: super().__init__() - + self._server: Server = server def check(self, email: str, password: str) -> bool: if not self._server.has_email(email): print("This email is not registered!") return False - + if not self._server.is_valid_password(email, password): print("Wrong password!") return False - + return self._check_next(email, password) @@ -95,22 +95,22 @@ def check(self, email: str, password: str) -> bool: if email == "admin@example.com": print("Hello, admin!") return True - + print("Hello, user!") return self._check_next(email, password) # Класс сервера. class Server: - def __init__(self): + def __init__(self) -> None: self._users: Dict[str, str] = dict() self._middleware: Middleware = None - + # Клиент подаёт готовую цепочку в сервер. Это увеличивает гибкость и # упрощает тестирование класса сервера. - def set_middleware(self, middleware: Middleware): + def set_middleware(self, middleware: Middleware) -> None: self._middleware = middleware - + # Сервер получает email и пароль от клиента и запускает проверку # авторизации у цепочки. def log_in(self, email: str, password: str) -> bool: @@ -120,10 +120,10 @@ def log_in(self, email: str, password: str) -> bool: # Здесь должен быть какой-то полезный код, работающий для # авторизированных пользователей. return True - + return False - def register(self, email: str, password: str): + def register(self, email: str, password: str) -> None: self._users[email] = password def has_email(self, email: str) -> bool: @@ -131,9 +131,9 @@ def has_email(self, email: str) -> bool: def is_valid_password(self, email: str, password: str) -> bool: return self._users.get(email) == password - -if __name__ == '__main__': + +if __name__ == "__main__": server = Server() server.register("admin@example.com", "admin_pass") server.register("user@example.com", "user_pass") @@ -142,14 +142,12 @@ def is_valid_password(self, email: str, password: str) -> bool: # Проверки связаны в одну цепь. Клиент может строить различные цепи, # используя одни и те же компоненты. middleware = ThrottlingMiddleware(request_per_minute=2) - middleware\ - .link_with(UserExistsMiddleware(server))\ - .link_with(RoleCheckMiddleware()) + middleware.link_with(UserExistsMiddleware(server)).link_with(RoleCheckMiddleware()) # Сервер получает цепочку от клиентского кода. server.set_middleware(middleware) - - print('OUTPUT:') + + print("OUTPUT:") while True: email = input("Enter email: ") diff --git a/design_patterns__examples/Command/example.py b/design_patterns__examples/Command/example.py new file mode 100644 index 000000000..e21d52dd6 --- /dev/null +++ b/design_patterns__examples/Command/example.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: Design Patterns: Command — Команда +# SOURCE: https://ru.wikipedia.org/wiki/Команда_(шаблон_проектирования) +# SOURCE: https://javarush.ru/groups/posts/584-patternih-proektirovanija + + +from abc import ABC, abstractmethod + + +class Command(ABC): + @abstractmethod + def execute(self): + pass + + +class Car: + def start_engine(self) -> None: + print("Запустить двигатель") + + def stop_engine(self) -> None: + print("Остановить двигатель") + + +class StartCar(Command): + def __init__(self, car: Car) -> None: + self.car: Car = car + + def execute(self) -> None: + self.car.start_engine() + + +class StopCar(Command): + def __init__(self, car: Car) -> None: + self.car: Car = car + + def execute(self) -> None: + self.car.stop_engine() + + +class CarInvoker: + def __init__(self, command: Command) -> None: + self.command: Command = command + + def execute(self) -> None: + self.command.execute() + + +if __name__ == "__main__": + car = Car() + + startCar = StartCar(car) + stopCar = StopCar(car) + + carInvoker = CarInvoker(startCar) + carInvoker.execute() + + stopCar.execute() diff --git a/design_patterns__examples/Composite/example.py b/design_patterns__examples/Composite/example.py index 6c10f9f88..0c3b6fc2c 100644 --- a/design_patterns__examples/Composite/example.py +++ b/design_patterns__examples/Composite/example.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Composite - Компоновщик @@ -19,69 +19,69 @@ def draw(self, *args, **kwargs): class CompositeGraphic(Graphic): - def __init__(self): + def __init__(self) -> None: self._child_graphics: List[Graphic] = [] - def draw(self, *args, **kwargs): + def draw(self, *args, **kwargs) -> None: for graphic in self._child_graphics: graphic.draw(*args, **kwargs) # Adds the graphic to the composition - def add(self, graphic: Graphic): + def add(self, graphic: Graphic) -> None: if graphic in self._child_graphics: return self._child_graphics.append(graphic) # Removes the graphic from the composition - def remove(self, graphic: Graphic): + def remove(self, graphic: Graphic) -> None: self._child_graphics.remove(graphic) class Ellipse(Graphic): - def __init__(self, x, y, rx, ry): + def __init__(self, x, y, rx, ry) -> None: self.x = x self.y = y self.rx = rx self.ry = ry - def draw(self, *args, **kwargs): - print(f'Ellipse: x={self.x}, y={self.y}, rx={self.rx}, ry={self.ry}') + def draw(self, *args, **kwargs) -> None: + print(f"Ellipse: x={self.x}, y={self.y}, rx={self.rx}, ry={self.ry}") class Point(Graphic): - def __init__(self, x, y): + def __init__(self, x, y) -> None: self.x = x self.y = y - def draw(self, *args, **kwargs): - print(f'Point: x={self.x}, y={self.y}') + def draw(self, *args, **kwargs) -> None: + print(f"Point: x={self.x}, y={self.y}") class Rect(Graphic): - def __init__(self, x, y, w, h): + def __init__(self, x, y, w, h) -> None: self.x = x self.y = y self.w = w self.h = h - def draw(self, *args, **kwargs): - print(f'Rect: x={self.x}, y={self.y}, w={self.w}, h={self.h}') + def draw(self, *args, **kwargs) -> None: + print(f"Rect: x={self.x}, y={self.y}, w={self.w}, h={self.h}") class Line(Graphic): - def __init__(self, x1, y1, x2, y2): + def __init__(self, x1, y1, x2, y2) -> None: self.x1 = x1 self.y1 = y1 self.x2 = x2 self.y2 = y2 - def draw(self, *args, **kwargs): - print(f'Line: x1={self.x1}, y1={self.y1}, x2={self.x2}, y2={self.y2}') + def draw(self, *args, **kwargs) -> None: + print(f"Line: x1={self.x1}, y1={self.y1}, x2={self.x2}, y2={self.y2}") -if __name__ == '__main__': +if __name__ == "__main__": ellipse_1 = Ellipse(x=50, y=50, rx=10, ry=15) ellipse_2 = Ellipse(x=10, y=5, rx=40, ry=50) diff --git a/design_patterns__examples/Composite/example__pyqt_draw.py b/design_patterns__examples/Composite/example__pyqt_draw.py index a672a6d23..a9e97de75 100644 --- a/design_patterns__examples/Composite/example__pyqt_draw.py +++ b/design_patterns__examples/Composite/example__pyqt_draw.py @@ -1,118 +1,119 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Composite - Компоновщик # SOURCE: https://ru.wikipedia.org/wiki/Компоновщик_(шаблон_проектирования) +import traceback +import sys + from abc import ABC, abstractmethod from typing import List from PyQt5.Qt import * -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions class Graphic(ABC): @abstractmethod - def draw(self, painter: QPainter): + def draw(self, painter: QPainter) -> None: pass class CompositeGraphic(Graphic): - def __init__(self): + def __init__(self) -> None: self._child_graphics: List[Graphic] = [] - def draw(self, painter: QPainter): + def draw(self, painter: QPainter) -> None: for graphic in self._child_graphics: graphic.draw(painter) # Adds the graphic to the composition - def add(self, graphic: Graphic): + def add(self, graphic: Graphic) -> None: if graphic in self._child_graphics: return self._child_graphics.append(graphic) # Removes the graphic from the composition - def remove(self, graphic: Graphic): + def remove(self, graphic: Graphic) -> None: self._child_graphics.remove(graphic) class Ellipse(Graphic): - def __init__(self, x, y, rx, ry): + def __init__(self, x, y, rx, ry) -> None: self.x = x self.y = y self.rx = rx self.ry = ry - def draw(self, painter: QPainter): + def draw(self, painter: QPainter) -> None: painter.drawEllipse(self.x, self.y, self.rx, self.ry) class Point(Graphic): - def __init__(self, x, y): + def __init__(self, x, y) -> None: self.x = x self.y = y - def draw(self, painter: QPainter): + def draw(self, painter: QPainter) -> None: painter.drawPoint(self.x, self.y) class Rect(Graphic): - def __init__(self, x, y, w, h): + def __init__(self, x, y, w, h) -> None: self.x = x self.y = y self.w = w self.h = h - def draw(self, painter: QPainter): + def draw(self, painter: QPainter) -> None: painter.drawRect(self.x, self.y, self.w, self.h) class Line(Graphic): - def __init__(self, x1, y1, x2, y2): + def __init__(self, x1, y1, x2, y2) -> None: self.x1 = x1 self.y1 = y1 self.x2 = x2 self.y2 = y2 - def draw(self, painter: QPainter): + def draw(self, painter: QPainter) -> None: painter.drawLine(self.x1, self.y1, self.x2, self.y2) class CanvasWidget(QWidget): - def __init__(self, graphic: Graphic): + def __init__(self, graphic: Graphic) -> None: super().__init__() - self.setWindowTitle('Canvas') + self.setWindowTitle("Canvas") self.graphic = graphic - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setBrush(Qt.green) self.graphic.draw(painter) -if __name__ == '__main__': +if __name__ == "__main__": ellipse_1 = Ellipse(x=50, y=50, rx=10, ry=15) ellipse_2 = Ellipse(x=10, y=5, rx=40, ry=50) diff --git a/design_patterns__examples/Decorator/example_1.py b/design_patterns__examples/Decorator/example_1.py index d64a5ee20..12edf7a6f 100644 --- a/design_patterns__examples/Decorator/example_1.py +++ b/design_patterns__examples/Decorator/example_1.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Decorator — Декоратор @@ -23,7 +23,7 @@ def run(self, text: str) -> str: class BaseDecorator(IOperation): - def __init__(self, operation: IOperation): + def __init__(self, operation: IOperation) -> None: self._wrapped = operation @@ -42,8 +42,8 @@ def run(self, text: str) -> str: return self._wrapped.run(text).upper() -if __name__ == '__main__': - text = 'Hello World!' +if __name__ == "__main__": + text = "Hello World!" operation = SimpleOperation() print(operation.run(text)) # Hello World! diff --git a/design_patterns__examples/Decorator/example_2.py b/design_patterns__examples/Decorator/example_2.py index bb01161b8..87a965c17 100644 --- a/design_patterns__examples/Decorator/example_2.py +++ b/design_patterns__examples/Decorator/example_2.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Decorator — Декоратор @@ -10,49 +10,49 @@ class Notifier: - def send(self, message: str): + def send(self, message: str) -> None: print(f'[Email] Send "{message}".') class BaseDecorator(Notifier): - def __init__(self, notifier: Notifier): + def __init__(self, notifier: Notifier) -> None: self._wrapped = notifier class SMSDecorator(BaseDecorator): - def send(self, message: str): + def send(self, message: str) -> None: self._wrapped.send(message) print(f'[SMS] send "{message}".') class FacebookDecorator(BaseDecorator): - def send(self, message: str): + def send(self, message: str) -> None: self._wrapped.send(message) print(f'[Facebook] send "{message}".') class SlackDecorator(BaseDecorator): - def send(self, message: str): + def send(self, message: str) -> None: self._wrapped.send(message) print(f'[Slack] send "{message}".') class Application: - def __init__(self): + def __init__(self) -> None: self._notifier: Notifier = None - def set_notifier(self, notifier: Notifier): + def set_notifier(self, notifier: Notifier) -> None: self._notifier = notifier - def about_alert(self): + def about_alert(self) -> None: if self._notifier: - self._notifier.send('Alert!') + self._notifier.send("Alert!") -if __name__ == '__main__': +if __name__ == "__main__": sms_enabled = True facebook_enabled = False slack_enabled = True @@ -78,7 +78,7 @@ def about_alert(self): print() notifier = Notifier() - notifier.send('Test!') + notifier.send("Test!") # [Email] Send "Test!". print() @@ -86,7 +86,7 @@ def about_alert(self): notifier = SMSDecorator(notifier) notifier = FacebookDecorator(notifier) notifier = SlackDecorator(notifier) - notifier.send('Test!') + notifier.send("Test!") # [Email] Send "Test!". # [SMS] send "Test!". # [Facebook] send "Test!". diff --git a/design_patterns__examples/Facade/example_1.py b/design_patterns__examples/Facade/example_1.py index 0ce24b4be..9142444d4 100644 --- a/design_patterns__examples/Facade/example_1.py +++ b/design_patterns__examples/Facade/example_1.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Facade — Фасад @@ -15,13 +15,13 @@ # Class CPU, отвечает за работу процессора class CPU: - def freeze(self): + def freeze(self) -> None: pass - def jump(self, position: int): + def jump(self, position: int) -> None: pass - def execute(self): + def execute(self) -> None: pass @@ -29,7 +29,7 @@ def execute(self): class Memory: BOOT_ADDRESS = 0x0005 - def load(self, position: int, data: bytes): + def load(self, position: int, data: bytes) -> None: pass @@ -46,23 +46,25 @@ def read(self, lba: int, size: int) -> bytes: # В качестве унифицированного объекта выступает Компьютер. # За этим объектом будут скрыты, все детали работы его внутренних частей. class Computer: - def __init__(self): + def __init__(self) -> None: self._cpu = CPU() self._memory = Memory() self._hard_drive = HardDrive() # Упрощённая обработка поведения "запуск компьютера" - def start_computer(self): + def start_computer(self) -> None: self._cpu.freeze() self._memory.load( self._memory.BOOT_ADDRESS, - self._hard_drive.read(self._hard_drive.BOOT_SECTOR, self._hard_drive.SECTOR_SIZE) + self._hard_drive.read( + self._hard_drive.BOOT_SECTOR, self._hard_drive.SECTOR_SIZE + ), ) self._cpu.jump(self._memory.BOOT_ADDRESS) self._cpu.execute() -if __name__ == '__main__': +if __name__ == "__main__": # Пользователям компьютера предоставляется Фасад (компьютер), # который скрывает все сложность работы с отдельными компонентами. computer = Computer() diff --git a/design_patterns__examples/Facade/example_2.py b/design_patterns__examples/Facade/example_2.py index 22299ef6b..33a3c32dd 100644 --- a/design_patterns__examples/Facade/example_2.py +++ b/design_patterns__examples/Facade/example_2.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Facade — Фасад @@ -76,7 +76,7 @@ def operation(self) -> str: self._subsystem2.operation_ready(), "Facade orders subsystems to perform the action:", self._subsystem1.operation_go(), - self._subsystem2.operation_fire() + self._subsystem2.operation_fire(), ] return "\n".join(results) diff --git a/design_patterns__examples/Facade/example_3.py b/design_patterns__examples/Facade/example_3.py index 9621c2cfb..468c49830 100644 --- a/design_patterns__examples/Facade/example_3.py +++ b/design_patterns__examples/Facade/example_3.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Facade — Фасад @@ -14,67 +14,67 @@ # Абстрактный музыкант - не является обязательной составляющей паттерна, введен для упрощения кода class Musician(ABC): - def __init__(self, name: str): + def __init__(self, name: str) -> None: self.name = name - - def output(self, text: str): + + def output(self, text: str) -> None: print(self.name + " " + text + ".") - + # Конкретные музыканты class Vocalist(Musician): - def sing_couplet(self, couplet_number: int): + def sing_couplet(self, couplet_number: int) -> None: self.output("спел куплет №" + str(couplet_number)) - - def sing_chorus(self): + + def sing_chorus(self) -> None: self.output("спел припев") class Guitarist(Musician): - def play_cool_opening(self): + def play_cool_opening(self) -> None: self.output("начинает с крутого вступления") - - def play_cool_riffs(self): + + def play_cool_riffs(self) -> None: self.output("играет крутые риффы") - def play_another_cool_riffs(self): + def play_another_cool_riffs(self) -> None: self.output("играет другие крутые риффы") - def play_incredibly_cool_solo(self): + def play_incredibly_cool_solo(self) -> None: self.output("выдает невероятно крутое соло") - def play_final_accord(self): + def play_final_accord(self) -> None: self.output("заканчивает песню мощным аккордом") - + class Bassist(Musician): - def follow_the_drums(self): + def follow_the_drums(self) -> None: self.output("следует за барабанами") - - def change_rhythm(self, type_rhythm: str): + + def change_rhythm(self, type_rhythm: str) -> None: self.output("перешел на ритм " + type_rhythm + "a") - def stop_playing(self): + def stop_playing(self) -> None: self.output("заканчивает играть") class Drummer(Musician): - def start_playing(self): + def start_playing(self) -> None: self.output("начинает играть") - - def stop_playing(self): + + def stop_playing(self) -> None: self.output("заканчивает играть") - + # Фасад, в данном случае - знаменитая рок-группа class BlackSabbath: - def __init__(self): + def __init__(self) -> None: self.vocalist = Vocalist("Оззи Осборн") self.guitarist = Guitarist("Тони Айомми") self.bassist = Bassist("Гизер Батлер") self.drummer = Drummer("Билл Уорд") - - def play_cool_song(self): + + def play_cool_song(self) -> None: self.guitarist.play_cool_opening() self.drummer.start_playing() self.bassist.follow_the_drums() @@ -103,11 +103,11 @@ def play_cool_song(self): self.guitarist.play_final_accord() -if __name__ == '__main__': +if __name__ == "__main__": print("OUTPUT:") band = BlackSabbath() band.play_cool_song() - + # OUTPUT: # Тони Айомми начинает с крутого вступления. # Билл Уорд начинает играть. diff --git a/design_patterns__examples/Factory/example_1.py b/design_patterns__examples/Factory/example_1.py index 6f30a0f54..ab5e6a9e1 100644 --- a/design_patterns__examples/Factory/example_1.py +++ b/design_patterns__examples/Factory/example_1.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.wikipedia.org/wiki/Фабричный_метод_(шаблон_проектирования) @@ -19,13 +19,13 @@ def parse(self, data: str) -> object: class ParserFactory: @staticmethod def get_parser(name_parser: str) -> Parser: - if name_parser == 'XML': + if name_parser == "XML": return ParserXML() - elif name_parser == 'JSON': + elif name_parser == "JSON": return ParserJSON() - elif name_parser == 'CSV': + elif name_parser == "CSV": return ParserCSV() raise Exception(f'Unsupported parser "{name_parser}"') @@ -34,34 +34,38 @@ def get_parser(name_parser: str) -> Parser: class ParserXML(Parser): def parse(self, data: str) -> object: from bs4 import BeautifulSoup - root = BeautifulSoup(data, 'xml') + + root = BeautifulSoup(data, "xml") return root class ParserJSON(Parser): def parse(self, data: str) -> object: import json + return json.loads(data) class ParserCSV(Parser): def parse(self, data: str) -> object: import csv + csv_reader = csv.reader(data.splitlines(), delimiter=";") return list(csv_reader) -if __name__ == '__main__': - result = ParserFactory.get_parser('XML').parse('123') +if __name__ == "__main__": + result = ParserFactory.get_parser("XML").parse("123") print(result) # '\n123' - json_parser = ParserFactory.get_parser('JSON') - result = json_parser.parse('[null, null, null]') + json_parser = ParserFactory.get_parser("JSON") + result = json_parser.parse("[null, null, null]") print(result) # [None, None, None] result = json_parser.parse('["a", ["b", "c"], null]') print(result) # ['a', ['b', 'c'], None] - csv_parser = ParserFactory.get_parser('CSV') - result = csv_parser.parse('1;vasya;moscow;11111\n2;oleg;sochi;22222') - print(result) # [['1', 'vasya', 'moscow', '11111'], ['2', 'oleg', 'sochi', '22222']] + csv_parser = ParserFactory.get_parser("CSV") + result = csv_parser.parse("1;vasya;moscow;11111\n2;oleg;sochi;22222") + print(result) + # [['1', 'vasya', 'moscow', '11111'], ['2', 'oleg', 'sochi', '22222']] diff --git a/design_patterns__examples/Factory/example_2.py b/design_patterns__examples/Factory/example_2.py index daf786a1d..ae56828b1 100644 --- a/design_patterns__examples/Factory/example_2.py +++ b/design_patterns__examples/Factory/example_2.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.wikipedia.org/wiki/Фабричный_метод_(шаблон_проектирования) @@ -12,32 +12,32 @@ class Animal(ABC): @staticmethod - def initial(animal: str) -> 'Animal': - if animal == 'Lion': + def initial(animal: str) -> "Animal": + if animal == "Lion": return Lion() - elif animal == 'Cat': + elif animal == "Cat": return Cat() raise Exception(f'Unsupported animal "{animal}"') @abstractmethod - def voice(self): + def voice(self) -> None: pass class Lion(Animal): - def voice(self): + def voice(self) -> None: print("Rrrrrrrr i'm the lion") class Cat(Animal): - def voice(self): + def voice(self) -> None: print("Meow, meow i'm the kitty") -if __name__ == '__main__': - animal_1 = Animal.initial('Lion') - animal_2 = Animal.initial('Cat') +if __name__ == "__main__": + animal_1 = Animal.initial("Lion") + animal_2 = Animal.initial("Cat") animal_1.voice() animal_2.voice() diff --git a/design_patterns__examples/Flyweight/example_1.py b/design_patterns__examples/Flyweight/example_1.py index 24695c80a..5f91166f1 100644 --- a/design_patterns__examples/Flyweight/example_1.py +++ b/design_patterns__examples/Flyweight/example_1.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Flyweight — Приспособленец @@ -11,34 +11,31 @@ # SOURCE: https://javarush.ru/groups/posts/584-patternih-proektirovanija -from typing import List - - class Flyweight: - def __init__(self, row: int): + def __init__(self, row: int) -> None: self.row = row print("ctor:", self.row) - - def report(self, col: int): - print(" {}{}".format(self.row, col), end='') + + def report(self, col: int) -> None: + print(f" {self.row}{col}", end="") class Factory: - def __init__(self, max_rows: int): - self._pool: List[Flyweight] = [None] * max_rows - + def __init__(self, max_rows: int) -> None: + self._pool: list[Flyweight | None] = [None] * max_rows + def get_flyweight(self, row: int) -> Flyweight: if self._pool[row] is None: self._pool[row] = Flyweight(row) - + return self._pool[row] - -if __name__ == '__main__': + +if __name__ == "__main__": rows = 5 the_factory = Factory(rows) for i in range(rows): for j in range(rows): the_factory.get_flyweight(i).report(j) - + print() diff --git a/design_patterns__examples/Flyweight/example_2.py b/design_patterns__examples/Flyweight/example_2.py index eaeca5a1d..f945f6de2 100644 --- a/design_patterns__examples/Flyweight/example_2.py +++ b/design_patterns__examples/Flyweight/example_2.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Flyweight — Приспособленец @@ -9,7 +9,6 @@ from abc import ABC -from typing import Dict # "Flyweight" @@ -21,40 +20,40 @@ class Character(ABC): descent: int point_size: int - def display(self, point_size: int): + def display(self, point_size: int) -> None: self.point_size = point_size - print('{} (point_size {})'.format(self.symbol, self.point_size)) + print(f"{self.symbol} (point_size {self.point_size})") # "FlyweightFactory" class CharacterFactory: - def __init__(self): - self._characters: Dict[str, Character] = dict() + def __init__(self) -> None: + self._characters: dict[str, Character] = dict() def get_character(self, key: str) -> Character: # Uses "lazy initialization" character = self._characters.get(key) if character is None: - if key == 'A': + if key == "A": character = CharacterA() - elif key == 'B': + elif key == "B": character = CharacterB() # ... - elif key == 'Z': + elif key == "Z": character = CharacterZ() self._characters[key] = character return character - - + + # "ConcreteFlyweight" class CharacterA(Character): - def __init__(self): - self.symbol = 'A' + def __init__(self) -> None: + self.symbol = "A" self.height = 100 self.width = 120 self.ascent = 70 @@ -63,8 +62,8 @@ def __init__(self): # "ConcreteFlyweight" class CharacterB(Character): - def __init__(self): - self.symbol = 'B' + def __init__(self) -> None: + self.symbol = "B" self.height = 100 self.width = 140 self.ascent = 72 @@ -76,15 +75,15 @@ def __init__(self): # "ConcreteFlyweight" class CharacterZ(Character): - def __init__(self): - self.symbol = 'Z' + def __init__(self) -> None: + self.symbol = "Z" self.height = 100 self.width = 100 self.ascent = 68 self.descent = 0 -if __name__ == '__main__': +if __name__ == "__main__": # Build a document with text chars = "AAZZBBZB" diff --git a/design_patterns__examples/Mediator/example_1.py b/design_patterns__examples/Mediator/example_1.py index 4f0c936b9..2309230b6 100644 --- a/design_patterns__examples/Mediator/example_1.py +++ b/design_patterns__examples/Mediator/example_1.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Mediator — Посредник @@ -28,13 +28,13 @@ def notify(self, sender: object, event: str): # Конкретные Посредники реализуют совместное поведение, координируя отдельные компоненты. class ConcreteMediator(IMediator): - def __init__(self, component1, component2): + def __init__(self, component1, component2) -> None: self._component1 = component1 self._component1.set_mediator(self) self._component2 = component2 self._component2.set_mediator(self) - def notify(self, sender: object, event: str): + def notify(self, sender: object, event: str) -> None: print(f'[+] Mediator notify(sender={sender}, event="{event}")') if event == "A": @@ -43,7 +43,7 @@ def notify(self, sender: object, event: str): elif event == "B": print("[+] Mediator reacts on B and triggers following operations:") - print('[-] Nothing!') + print("[-] Nothing!") elif event == "D": print("[+] Mediator reacts on D and triggers following operations:") @@ -57,36 +57,36 @@ class BaseComponent(ABC): посредника внутри объектов компонентов. """ - def __init__(self, mediator: IMediator = None): + def __init__(self, mediator: IMediator = None) -> None: self._mediator = mediator - def set_mediator(self, mediator: IMediator): + def set_mediator(self, mediator: IMediator) -> None: self._mediator = mediator # Конкретные Компоненты реализуют различную функциональность. Они не зависят от других # компонентов. Они также не зависят от каких-либо конкретных классов посредников. class Component1(BaseComponent): - def do_a(self): + def do_a(self) -> None: print("Component 1 does A.") self._mediator.notify(self, "A") - def do_b(self): + def do_b(self) -> None: print("Component 1 does B.") self._mediator.notify(self, "B") class Component2(BaseComponent): - def do_c(self): + def do_c(self) -> None: print("Component 2 does C.") self._mediator.notify(self, "C") - def do_d(self): + def do_d(self) -> None: print("Component 2 does D.") self._mediator.notify(self, "D") -if __name__ == '__main__': +if __name__ == "__main__": # Клиентский код component1 = Component1() component2 = Component2() diff --git a/design_patterns__examples/Mediator/example_2.py b/design_patterns__examples/Mediator/example_2.py index 2a10d1b8f..f0ccd146b 100644 --- a/design_patterns__examples/Mediator/example_2.py +++ b/design_patterns__examples/Mediator/example_2.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Mediator — Посредник @@ -11,19 +11,19 @@ class Mediator: @staticmethod - def send_message(user: 'User', msg: str): - print(user.name + ": " + msg) - + def send_message(user: "User", msg: str) -> None: + print(f"{user.name}: {msg}") + class User: - def __init__(self, name: str): + def __init__(self, name: str) -> None: self.name = name - - def send_message(self, msg: str): + + def send_message(self, msg: str) -> None: Mediator.send_message(self, msg) -if __name__ == '__main__': +if __name__ == "__main__": user1 = User("user1") user2 = User("user2") user1.send_message("message1") diff --git a/design_patterns__examples/Mediator/example_notes__pyqt.py b/design_patterns__examples/Mediator/example_notes__pyqt.py index 5d005218b..e0facea05 100644 --- a/design_patterns__examples/Mediator/example_notes__pyqt.py +++ b/design_patterns__examples/Mediator/example_notes__pyqt.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Mediator — Посредник @@ -9,9 +9,11 @@ # SOURCE: https://refactoring.guru/ru/design-patterns/mediator/java/example -from abc import abstractmethod -from typing import List, Any import traceback +import sys + +from abc import abstractmethod +from typing import Any try: @@ -28,29 +30,28 @@ from PySide.QtCore import * -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions # Класс заметок class Note: - def __init__(self): + def __init__(self) -> None: self._name = "note" - self._text = '' + self._text = "" - def setName(self, name: str): + def setName(self, name: str) -> None: self._name = name - def setText(self, text: str): + def setText(self, text: str) -> None: self._text = text def getName(self) -> str: @@ -61,12 +62,12 @@ def getText(self) -> str: class DefaultListModel(QAbstractListModel): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.items: List[Note] = [] + self.items: list[Note] = [] - def rowCount(self, parent: QModelIndex=None): + def rowCount(self, parent: QModelIndex = None) -> int: return len(self.items) def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any: @@ -74,16 +75,16 @@ def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any: return QVariant() element = self.items[index.row()] - + if role == Qt.DisplayRole: return element.getName() - + return QVariant() def get_element(self, index: int) -> Note: return self.items[index] - def addElement(self, element: Note): + def addElement(self, element: Note) -> None: length = self.rowCount() self.beginInsertRows(QModelIndex(), length, length) @@ -93,7 +94,7 @@ def addElement(self, element: Note): # Говорим view, что данные изменились self.dataChanged.emit(self.createIndex(length, 0), self.createIndex(length, 0)) - def removeElement(self, index: int): + def removeElement(self, index: int) -> None: self.beginRemoveRows(QModelIndex(), index, index) self.items.pop(index) self.endRemoveRows() @@ -101,64 +102,64 @@ def removeElement(self, index: int): # Говорим view, что данные изменились self.dataChanged.emit(self.createIndex(index, 0), self.createIndex(index, 0)) - def get_items(self) -> List[Note]: + def get_items(self) -> list[Note]: return self.items # Общий интерфейс посредников. class Mediator: @abstractmethod - def addNewNote(self, note: Note): + def addNewNote(self, note: Note) -> None: pass @abstractmethod - def deleteNote(self): + def deleteNote(self) -> None: pass @abstractmethod - def getInfoFromList(self, note: Note): + def getInfoFromList(self, note: Note) -> None: pass @abstractmethod - def saveChanges(self): + def saveChanges(self) -> None: pass @abstractmethod - def markNote(self): + def markNote(self) -> None: pass @abstractmethod - def clear(self): + def clear(self) -> None: pass @abstractmethod - def sendToFilter(self, listModel: DefaultListModel): + def sendToFilter(self, listModel: DefaultListModel) -> None: pass @abstractmethod - def setElementsList(self, listModel: DefaultListModel): + def setElementsList(self, listModel: DefaultListModel) -> None: pass @abstractmethod - def registerComponent(self, component: 'Component'): + def registerComponent(self, component: "Component") -> None: pass @abstractmethod - def hideElements(self, flag: bool): + def hideElements(self, flag: bool) -> None: pass @abstractmethod - def createGUI(self): + def createGUI(self) -> None: pass class Component: """Общий класс компонентов.""" - def __init__(self): + def __init__(self) -> None: self._mediator: Mediator = None - def setMediator(self, mediator: Mediator): + def setMediator(self, mediator: Mediator) -> None: self._mediator = mediator @abstractmethod @@ -169,7 +170,7 @@ def getName(self) -> str: # Конкретные компоненты никак не связаны между собой. У них есть только один # канал общения – через отправку уведомлений посреднику. class AddButton(QPushButton, Component): - def __init__(self): + def __init__(self) -> None: super().__init__("Add") # При клике на кнопку вызываем метод посредника @@ -182,7 +183,7 @@ def getName(self) -> str: # Конкретные компоненты никак не связаны между собой. У них есть только один # канал общения – через отправку уведомлений посреднику. class DeleteButton(QPushButton, Component): - def __init__(self): + def __init__(self) -> None: super().__init__("Del") # При клике на кнопку вызываем метод посредника @@ -195,21 +196,21 @@ def getName(self) -> str: # Конкретные компоненты никак не связаны между собой. У них есть только один # канал общения – через отправку уведомлений посреднику. class Filter(QLineEdit, Component): - def __init__(self): + def __init__(self) -> None: super().__init__() self._listModel: DefaultListModel = None - def setList(self, listModel: DefaultListModel): + def setList(self, listModel: DefaultListModel) -> None: self._listModel = listModel - def keyPressEvent(self, event: 'QKeyEvent'): + def keyPressEvent(self, event: "QKeyEvent") -> None: super().keyPressEvent(event) start = self.text() self._searchElements(start) - def _searchElements(self, text: str): + def _searchElements(self, text: str) -> None: if self._listModel is None: return @@ -234,13 +235,13 @@ def getName(self) -> str: # Конкретные компоненты никак не связаны между собой. У них есть только один # канал общения – через отправку уведомлений посреднику. class ListNote(QListView, Component): - def __init__(self, listModel: DefaultListModel): + def __init__(self, listModel: DefaultListModel) -> None: super().__init__() self._listModel = listModel self.setModel(self._listModel) - def addElement(self, note: Note): + def addElement(self, note: Note) -> None: self._listModel.addElement(note) index = self._listModel.rowCount() - 1 index = self._listModel.index(index, 0) @@ -249,7 +250,7 @@ def addElement(self, note: Note): self._mediator.sendToFilter(self._listModel) - def deleteElement(self): + def deleteElement(self) -> None: if not self.currentIndex().isValid(): return @@ -267,12 +268,12 @@ def getCurrentElement(self) -> Note: def getName(self) -> str: return "List" - + # Конкретные компоненты никак не связаны между собой. У них есть только один # канал общения – через отправку уведомлений посреднику. class SaveButton(QPushButton, Component): - def __init__(self): + def __init__(self) -> None: super().__init__("Save") # При клике на кнопку вызываем метод посредника @@ -285,7 +286,7 @@ def getName(self) -> str: # Конкретные компоненты никак не связаны между собой. У них есть только один # канал общения – через отправку уведомлений посреднику. class TextBox(QTextEdit, Component): - def keyPressEvent(self, event: 'QKeyEvent'): + def keyPressEvent(self, event: "QKeyEvent") -> None: super().keyPressEvent(event) self._mediator.markNote() @@ -297,7 +298,7 @@ def getName(self) -> str: # Конкретные компоненты никак не связаны между собой. У них есть только один # канал общения – через отправку уведомлений посреднику. class Title(QLineEdit, Component): - def keyPressEvent(self, event: 'QKeyEvent'): + def keyPressEvent(self, event: "QKeyEvent") -> None: super().keyPressEvent(event) self._mediator.markNote() @@ -310,7 +311,7 @@ def getName(self) -> str: # код посредника. Он получает извещения от своих компонентов и знает как на них # реагировать. class Editor(Mediator): - def __init__(self): + def __init__(self) -> None: self._title: Title = None self._textBox: TextBox = None self._add: AddButton = None @@ -326,22 +327,22 @@ def __init__(self): self._mainWindow = None # Здесь происходит регистрация компонентов посредником. - def registerComponent(self, component: Component): + def registerComponent(self, component: Component) -> None: component.setMediator(self) - if component.getName() == 'AddButton': + if component.getName() == "AddButton": self._add = component - elif component.getName() == 'DelButton': + elif component.getName() == "DelButton": self._del = component - elif component.getName() == 'Filter': + elif component.getName() == "Filter": self._filter = component - elif component.getName() == 'List': + elif component.getName() == "List": self._list: ListNote = component - def foo(*args): + def foo(*args) -> None: empty = len(self._list.selectedIndexes()) == 0 self.hideElements(empty) @@ -352,20 +353,20 @@ def foo(*args): # Вызываем нашу функцию при изменении выделения элементов в списке self._list.clicked.connect(foo) - elif component.getName() == 'SaveButton': + elif component.getName() == "SaveButton": self._save = component - elif component.getName() == 'TextBox': + elif component.getName() == "TextBox": self._textBox = component - elif component.getName() == 'Title': + elif component.getName() == "Title": self._title = component # # Разнообразные методы общения с компонентами. # - def addNewNote(self, note: Note): + def addNewNote(self, note: Note) -> None: # Инкрементальные названия text = note.getName() + str(self._list.model().rowCount() + 1) note.setName(text) @@ -374,14 +375,14 @@ def addNewNote(self, note: Note): self._textBox.setText("") self._list.addElement(note) - def deleteNote(self): + def deleteNote(self) -> None: self._list.deleteElement() - def getInfoFromList(self, note: Note): - self._title.setText(note.getName().replace('*', ' ')) + def getInfoFromList(self, note: Note) -> None: + self._title.setText(note.getName().replace("*", " ")) self._textBox.setText(note.getText()) - def saveChanges(self): + def saveChanges(self) -> None: try: note = self._list.getCurrentElement() note.setName(self._title.text()) @@ -392,7 +393,7 @@ def saveChanges(self): except Exception: traceback.print_exc() - def markNote(self): + def markNote(self) -> None: try: note = self._list.getCurrentElement() name = note.getName() @@ -407,18 +408,18 @@ def markNote(self): except Exception: traceback.print_exc() - def clear(self): + def clear(self) -> None: self._title.setText("") self._textBox.setText("") - def sendToFilter(self, listModel: DefaultListModel): + def sendToFilter(self, listModel: DefaultListModel) -> None: self._filter.setList(listModel) - def setElementsList(self, listModel: DefaultListModel): + def setElementsList(self, listModel: DefaultListModel) -> None: self._list.setModel(listModel) self._list.update() - def hideElements(self, flag: bool): + def hideElements(self, flag: bool) -> None: self._titleLabel.setVisible(not flag) self._textLabel.setVisible(not flag) self._title.setVisible(not flag) @@ -426,7 +427,7 @@ def hideElements(self, flag: bool): self._save.setVisible(not flag) self._label.setVisible(flag) - def createGUI(self): + def createGUI(self) -> None: # notes_side notes_side_filter_layout = QHBoxLayout() notes_side_filter_layout.addWidget(QLabel("Filter:")) @@ -466,7 +467,7 @@ def createGUI(self): splitter.addWidget(notes_editor) self._mainWindow = QMainWindow() - self._mainWindow.setWindowTitle('Notes') + self._mainWindow.setWindowTitle("Notes") self._mainWindow.setCentralWidget(splitter) # По умолчанию прячем элементы в редакторе заметки @@ -475,7 +476,7 @@ def createGUI(self): self._mainWindow.show() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mediator = Editor() diff --git a/design_patterns__examples/Observer/example_1.py b/design_patterns__examples/Observer/example_1.py index 09dea5e14..2585c4c06 100644 --- a/design_patterns__examples/Observer/example_1.py +++ b/design_patterns__examples/Observer/example_1.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Observer — Наблюдатель @@ -12,13 +12,13 @@ from abc import ABC, abstractmethod -# В примере описывается получение данных от метеорологической станции (класс weather_data, рассылатель событий) и -# использование их для вывода на экран (класс CurrentConditionsDisplay, слушатель событий). +# В примере описывается получение данных от метеорологической станции (класс weather_data, рассылатель событий) и +# использование их для вывода на экран (класс CurrentConditionsDisplay, слушатель событий). # Слушатель регистрируется у наблюдателя с помощью метода register_observer (при этом слушатель заносится в # список observers). Регистрация происходит в момент создания объекта currentDisplay, т.к. метод register_observer # применяется в конструкторе. -# При изменении погодных данных вызывается метод notify_observers, который в свою очередь вызывает метод update -# у всех слушателей, передавая им обновлённые данные. +# При изменении погодных данных вызывается метод notify_observers, который в свою очередь вызывает метод update +# у всех слушателей, передавая им обновлённые данные. class Observer(ABC): @@ -42,23 +42,23 @@ def notify_observers(self): class WeatherData(Observable): - def __init__(self): + def __init__(self) -> None: self.observers = [] self.temperature: float = None self.humidity: float = None self.pressure: int = None - def register_observer(self, o: Observer): + def register_observer(self, o: Observer) -> None: self.observers.append(o) - def remove_observer(self, o: Observer): + def remove_observer(self, o: Observer) -> None: self.observers.remove(o) - def notify_observers(self): + def notify_observers(self) -> None: for observer in self.observers: observer.update(self.temperature, self.humidity, self.pressure) - def set_measurements(self, temperature: float, humidity: float, pressure: int): + def set_measurements(self, temperature: float, humidity: float, pressure: int) -> None: self.temperature = temperature self.humidity = humidity self.pressure = pressure @@ -66,7 +66,7 @@ def set_measurements(self, temperature: float, humidity: float, pressure: int): class CurrentConditionsDisplay(Observer): - def __init__(self, weather_data: WeatherData): + def __init__(self, weather_data: WeatherData) -> None: self.weather_data = weather_data self.weather_data.register_observer(self) @@ -74,23 +74,24 @@ def __init__(self, weather_data: WeatherData): self.humidity: float = None self.pressure: int = None - def update(self, temperature: float, humidity: float, pressure: int): + def update(self, temperature: float, humidity: float, pressure: int) -> None: self.temperature = temperature self.humidity = humidity self.pressure = pressure self.display() - def display(self): - print("Сейчас значения: {:.1f} градусов цельсия и {:.1f}% влажности. Давление {} мм рт. ст.".format( - self.temperature, self.humidity, self.pressure - )) + def display(self) -> None: + print( + f"Сейчас значения: {self.temperature:.1f} градусов цельсия и {self.humidity:.1f}% влажности. " + f"Давление {self.pressure} мм рт. ст." + ) -if __name__ == '__main__': +if __name__ == "__main__": weather_data = WeatherData() current_display = CurrentConditionsDisplay(weather_data) - + weather_data.set_measurements(temperature=29, humidity=65, pressure=745) weather_data.set_measurements(temperature=39, humidity=70, pressure=760) weather_data.set_measurements(temperature=42, humidity=72, pressure=763) diff --git a/design_patterns__examples/Observer/example_2.py b/design_patterns__examples/Observer/example_2.py index fff55ab4a..ca034cda3 100644 --- a/design_patterns__examples/Observer/example_2.py +++ b/design_patterns__examples/Observer/example_2.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Observer — Наблюдатель @@ -34,7 +34,7 @@ def __init__(self) -> None: """ Constructor. """ - self.observers = [] # инициализация списка наблюдателей + self.observers = [] # инициализация списка наблюдателей def register(self, observer: Observer) -> None: """ @@ -80,15 +80,15 @@ def update(self, message: str) -> None: """ Получение очередной новости """ - print('{} узнал следующее: {}'.format(self.name, message)) + print(f"{self.name} узнал следующее: {message}") -if __name__ == '__main__': - newspaper = Newspaper() # Создаем небольшую газету - newspaper.register(Citizen('Иван')) # Добавляем двух человек, которые - newspaper.register(Citizen('Василий')) # ... ее регулярно выписывают +if __name__ == "__main__": + newspaper = Newspaper() # Создаем небольшую газету + newspaper.register(Citizen("Иван")) # Добавляем двух человек, которые + newspaper.register(Citizen("Василий")) # ... ее регулярно выписывают # ... и вбрасываем очередную газетную утку - newspaper.add_news('Наблюдатель - поведенческий шаблон проектирования') + newspaper.add_news("Наблюдатель - поведенческий шаблон проектирования") # Иван узнал следующее: Наблюдатель - поведенческий шаблон проектирования # Василий узнал следующее: Наблюдатель - поведенческий шаблон проектирования diff --git a/design_patterns__examples/Observer/example_3.py b/design_patterns__examples/Observer/example_3.py index 515c339b9..dacd8521d 100644 --- a/design_patterns__examples/Observer/example_3.py +++ b/design_patterns__examples/Observer/example_3.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Observer — Наблюдатель @@ -11,7 +11,6 @@ from abc import ABC, abstractmethod from random import randrange -from typing import List class Observer(ABC): @@ -21,7 +20,7 @@ class Observer(ABC): """ @abstractmethod - def update(self, subject: 'Subject') -> None: + def update(self, subject: "Subject") -> None: """ Получить обновление от субъекта. """ @@ -67,7 +66,7 @@ class ConcreteSubject(Subject): подписчикам. """ - _observers: List[Observer] = [] + _observers: list[Observer] = [] """ Список подписчиков. В реальной жизни список подписчиков может храниться в более подробном виде (классифицируется по типу события и т.д.) diff --git a/design_patterns__examples/Observer/example_4.py b/design_patterns__examples/Observer/example_4.py index 276b1a149..c797b1f59 100644 --- a/design_patterns__examples/Observer/example_4.py +++ b/design_patterns__examples/Observer/example_4.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Observer — Наблюдатель @@ -21,7 +21,7 @@ from abc import ABC, abstractmethod -from typing import IO, List, Dict +from typing import IO class EventListener(ABC): @@ -31,34 +31,34 @@ def update(self, event_type: str, file: IO): class EventManager: - def __init__(self, *operations): - self.listeners: Dict[str, List[EventListener]] = dict() + def __init__(self, *operations) -> None: + self.listeners: dict[str, list[EventListener]] = dict() for operation in operations: self.listeners[operation] = [] - def subscribe(self, event_type: str, listener: EventListener): + def subscribe(self, event_type: str, listener: EventListener) -> None: items = self.listeners[event_type] items.append(listener) - def unsubscribe(self, event_type: str, listener: EventListener): + def unsubscribe(self, event_type: str, listener: EventListener) -> None: items = self.listeners[event_type] if listener in items: items.remove(listener) - def notify(self, event_type: str, file: IO): + def notify(self, event_type: str, file: IO) -> None: items = self.listeners[event_type] for listener in items: listener.update(event_type, file) class Editor: - def __init__(self): + def __init__(self) -> None: self.file: IO = None self.events = EventManager("open", "save") - - def open_file(self, file_path: str): - self.file = open(file_path, 'w', encoding='utf-8') + + def open_file(self, file_path: str) -> None: + self.file = open(file_path, "w", encoding="utf-8") self.events.notify("open", self.file) def save_file(self): @@ -69,24 +69,28 @@ def save_file(self): class EmailNotificationListener(EventListener): - def __init__(self, email: str): + def __init__(self, email: str) -> None: self.email = email - def update(self, event_type: str, file: IO): - print(f"Email to {self.email}: Someone has performed {event_type} " - f"operation with the following file: {file.name}") - + def update(self, event_type: str, file: IO) -> None: + print( + f"Email to {self.email}: Someone has performed {event_type} " + f"operation with the following file: {file.name}" + ) + class LogOpenListener(EventListener): - def __init__(self, file_name: str): + def __init__(self, file_name: str) -> None: # self.log: IO = open(file_name, encoding='utf-8') self.file_name = file_name - def update(self, event_type: str, file: IO): + def update(self, event_type: str, file: IO) -> None: # print(f"Save to log {self.log}: Someone has performed {event_type} " # f"operation with the following file: {file.name}") - print(f"Save to log {self.file_name}: Someone has performed {event_type} " - f"operation with the following file: {file.name}") + print( + f"Save to log {self.file_name}: Someone has performed {event_type} " + f"operation with the following file: {file.name}" + ) if __name__ == "__main__": diff --git a/design_patterns__examples/Prototype/example.py b/design_patterns__examples/Prototype/example.py index b9096fd88..d9793cacf 100644 --- a/design_patterns__examples/Prototype/example.py +++ b/design_patterns__examples/Prototype/example.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Prototype - Прототип @@ -9,46 +9,47 @@ import copy +from typing import Any class Prototype: - def __init__(self): + def __init__(self) -> None: self._objects = dict() - def register_object(self, name, obj): + def register_object(self, name, obj) -> None: """Register an object""" self._objects[name] = obj - def unregister_object(self, name): + def unregister_object(self, name) -> None: """Unregister an object""" self._objects.pop(name) - def clone(self, name, **attr): + def clone(self, name, **attr) -> Any: """Clone a registered object and update inner attributes dictionary""" obj = copy.deepcopy(self._objects.get(name)) obj.__dict__.update(attr) return obj -if __name__ == '__main__': +if __name__ == "__main__": + class A: - def __init__(self): + def __init__(self) -> None: self.x = 3 self.y = 8 self.z = 15 self.garbage = [38, 11, 19] - def __str__(self): - return 'A({}, {}, {}, {})'.format(self.x, self.y, self.z, self.garbage) - + def __str__(self) -> str: + return f"A({self.x}, {self.y}, {self.z}, {self.garbage})" a = A() prototype = Prototype() - prototype.register_object('object_a', a) + prototype.register_object("object_a", a) - b = prototype.clone('object_a') - c = prototype.clone('object_a', x=1, y=2, garbage=[88, 1]) + b = prototype.clone("object_a") + c = prototype.clone("object_a", x=1, y=2, garbage=[88, 1]) for x in (a, b, c): print(x) diff --git a/design_patterns__examples/Proxy/example.py b/design_patterns__examples/Proxy/example.py index 08fd3f72a..2c520c2b7 100644 --- a/design_patterns__examples/Proxy/example.py +++ b/design_patterns__examples/Proxy/example.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Proxy - Заместитель @@ -43,7 +43,7 @@ def div(self, x, y): class MathProxy(IMath): """Прокси""" - def __init__(self): + def __init__(self) -> None: self.math = None # Быстрые операции - не требуют реального субъекта @@ -62,7 +62,7 @@ def mul(self, x, y): def div(self, x, y): if y == 0: - return float('inf') # Вернуть positive infinity + return float("inf") # Вернуть positive infinity if not self.math: self.math = Math() @@ -70,10 +70,10 @@ def div(self, x, y): return self.math.div(x, y) -if __name__ == '__main__': +if __name__ == "__main__": p = MathProxy() x, y = 4, 2 - print('4 + 2 =', p.add(x, y)) - print('4 - 2 =', p.sub(x, y)) - print('4 * 2 =', p.mul(x, y)) - print('4 / 2 =', p.div(x, y)) + print("4 + 2 =", p.add(x, y)) + print("4 - 2 =", p.sub(x, y)) + print("4 * 2 =", p.mul(x, y)) + print("4 / 2 =", p.div(x, y)) diff --git a/design_patterns__examples/Proxy/example__cached.py b/design_patterns__examples/Proxy/example__cached.py index d110913a8..e3c7ee6d7 100644 --- a/design_patterns__examples/Proxy/example__cached.py +++ b/design_patterns__examples/Proxy/example__cached.py @@ -1,10 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from abc import ABC, abstractmethod +from types import TracebackType +from typing import Optional, Type + import requests @@ -33,7 +36,7 @@ def get_status_code(self, url: str) -> int: class GoUrlCachedProxy(IGoUrl): """Прокси""" - def __init__(self): + def __init__(self) -> None: self._url = GoUrl() self._cache = dict() self._cache_status_code = dict() @@ -57,18 +60,23 @@ def get_status_code(self, url: str) -> int: return code -if __name__ == '__main__': - import time +if __name__ == "__main__": + from timeit import default_timer class TimeThis: def __enter__(self): - self.start_time = time.clock() + self.start_time = default_timer() return self - def __exit__(self, exc_type, exc_value, exc_traceback): - print('Elapsed time: {:.6f} sec'.format(time.clock() - self.start_time)) + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + exc_traceback: Optional[TracebackType], + ) -> None: + print(f"Elapsed time: {default_timer() - self.start_time:.6f} sec") - url = 'https://github.com/gil9red' + url = "https://github.com/gil9red" go_url = GoUrl() @@ -88,7 +96,7 @@ def __exit__(self, exc_type, exc_value, exc_traceback): print(rs, rs.status_code, code, rs.content) print() - print('Cached proxy:') + print("Cached proxy:") go_url = GoUrlCachedProxy() diff --git a/design_patterns__examples/Proxy/example__logged.py b/design_patterns__examples/Proxy/example__logged.py index 0d8017d73..10e59266e 100644 --- a/design_patterns__examples/Proxy/example__logged.py +++ b/design_patterns__examples/Proxy/example__logged.py @@ -1,28 +1,36 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging +import sys + +from logging.handlers import RotatingFileHandler +from types import TracebackType +from typing import Optional, Type + from example__cached import IGoUrl, GoUrl, GoUrlCachedProxy, requests -def get_logger(name, file='log.txt', encoding='utf-8'): - import logging +def get_logger(name, file="log.txt", encoding="utf-8"): log = logging.getLogger(name) log.setLevel(logging.DEBUG) - formatter = logging.Formatter('[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s') + formatter = logging.Formatter( + "[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s" + ) # Simple file handler # fh = logging.FileHandler(file, encoding=encoding) # or: - from logging.handlers import RotatingFileHandler - fh = RotatingFileHandler(file, maxBytes=10000000, backupCount=5, encoding=encoding) + fh = RotatingFileHandler( + file, maxBytes=10_000_000, backupCount=5, encoding=encoding + ) fh.setFormatter(formatter) log.addHandler(fh) - import sys sh = logging.StreamHandler(stream=sys.stdout) sh.setFormatter(formatter) log.addHandler(sh) @@ -33,9 +41,9 @@ def get_logger(name, file='log.txt', encoding='utf-8'): class GoUrlLoggedProxy(IGoUrl): """Прокси""" - _LOGGER = get_logger('GoUrlLoggedProxy') + _LOGGER = get_logger("GoUrlLoggedProxy") - def __init__(self, go_url: IGoUrl = None): + def __init__(self, go_url: IGoUrl = None) -> None: if go_url is None: go_url = GoUrl() @@ -57,22 +65,27 @@ def get_status_code(self, url: str) -> int: return code - def _log(self, text: str): + def _log(self, text: str) -> None: GoUrlLoggedProxy._LOGGER.debug(text) -if __name__ == '__main__': - import time +if __name__ == "__main__": + from timeit import default_timer class TimeThis: def __enter__(self): - self.start_time = time.clock() + self.start_time = default_timer() return self - def __exit__(self, exc_type, exc_value, exc_traceback): - print('Elapsed time: {:.6f} sec'.format(time.clock() - self.start_time)) + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + exc_traceback: Optional[TracebackType], + ) -> None: + print(f"Elapsed time: {default_timer() - self.start_time:.6f} sec") - url = 'https://github.com/gil9red' + url = "https://github.com/gil9red" go_url = GoUrl() @@ -82,7 +95,7 @@ def __exit__(self, exc_type, exc_value, exc_traceback): print(rs, rs.status_code, code, rs.content) print() - print('Logged proxy:') + print("Logged proxy:") go_url = GoUrlLoggedProxy() @@ -92,7 +105,7 @@ def __exit__(self, exc_type, exc_value, exc_traceback): print(rs, rs.status_code, code, rs.content) print() - print('Cached proxy with Logged proxy:') + print("Cached proxy with Logged proxy:") go_url = GoUrlLoggedProxy(go_url=GoUrlCachedProxy()) diff --git a/design_patterns__examples/Proxy/example__with_abc.py b/design_patterns__examples/Proxy/example__with_abc.py index 85dcb3baa..30ff7f6b4 100644 --- a/design_patterns__examples/Proxy/example__with_abc.py +++ b/design_patterns__examples/Proxy/example__with_abc.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.wikipedia.org/wiki/Заместитель_(шаблон_проектирования) @@ -49,7 +49,7 @@ def div(self, x, y): class MathProxy(IMath): """Прокси""" - def __init__(self): + def __init__(self) -> None: self.math = None # Быстрые операции - не требуют реального субъекта @@ -68,7 +68,7 @@ def mul(self, x, y): def div(self, x, y): if y == 0: - return float('inf') # Вернуть positive infinity + return float("inf") # Вернуть positive infinity if not self.math: self.math = Math() @@ -76,10 +76,10 @@ def div(self, x, y): return self.math.div(x, y) -if __name__ == '__main__': +if __name__ == "__main__": p = MathProxy() x, y = 4, 2 - print('4 + 2 =', p.add(x, y)) - print('4 - 2 =', p.sub(x, y)) - print('4 * 2 =', p.mul(x, y)) - print('4 / 2 =', p.div(x, y)) + print("4 + 2 =", p.add(x, y)) + print("4 - 2 =", p.sub(x, y)) + print("4 * 2 =", p.mul(x, y)) + print("4 / 2 =", p.div(x, y)) diff --git a/design_patterns__examples/Singleton/example.py b/design_patterns__examples/Singleton/example.py index ec0c137cf..92a30f0e4 100644 --- a/design_patterns__examples/Singleton/example.py +++ b/design_patterns__examples/Singleton/example.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://www.python.org/dev/peps/pep-0318/#examples @@ -14,10 +14,12 @@ def get_instance(): if cls not in instances: instances[cls] = cls() return instances[cls] + return get_instance -if __name__ == '__main__': +if __name__ == "__main__": + @singleton class MyClass: pass diff --git a/design_patterns__examples/Strategy/example.py b/design_patterns__examples/Strategy/example.py index 30ed9216b..dddbba159 100644 --- a/design_patterns__examples/Strategy/example.py +++ b/design_patterns__examples/Strategy/example.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Strategy — Стратегия @@ -19,27 +19,27 @@ def download(self, file: str): class DownloadWindowsStrategy(Strategy): - def download(self, file: str): + def download(self, file: str) -> None: print("Windows download: " + file) class DownloadLinuxStrategy(Strategy): - def download(self, file: str): + def download(self, file: str) -> None: print("Linux download: " + file) class Context: - def __init__(self, strategy: Strategy): + def __init__(self, strategy: Strategy) -> None: self._strategy = strategy - def set_strategy(self, strategy: Strategy): + def set_strategy(self, strategy: Strategy) -> None: self._strategy = strategy - def download(self, file: str): + def download(self, file: str) -> None: self._strategy.download(file) -if __name__ == '__main__': +if __name__ == "__main__": context = Context(DownloadWindowsStrategy()) context.download("file.txt") # Windows download: file.txt print() diff --git a/design_patterns__examples/Strategy/example_1.py b/design_patterns__examples/Strategy/example_1.py index 9ac1136ec..5519920bb 100644 --- a/design_patterns__examples/Strategy/example_1.py +++ b/design_patterns__examples/Strategy/example_1.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Strategy — Стратегия @@ -28,17 +28,17 @@ def action(self, a, b): class Context: - def __init__(self, strategy: Strategy): + def __init__(self, strategy: Strategy) -> None: self._strategy = strategy - def set_strategy(self, strategy: Strategy): + def set_strategy(self, strategy: Strategy) -> None: self._strategy = strategy def action(self, a, b): return self._strategy.action(a, b) -if __name__ == '__main__': +if __name__ == "__main__": context = Context(AdditionStrategy()) print(context.action(10, 5)) # 15 print() diff --git a/design_patterns__examples/Template method/example_1.py b/design_patterns__examples/Template method/example_1.py index f31c7c2c5..8ae667269 100644 --- a/design_patterns__examples/Template method/example_1.py +++ b/design_patterns__examples/Template method/example_1.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Template method — Шаблонный метод @@ -29,10 +29,10 @@ def hit_and_run(self) -> None: """ Шаблонный метод """ - self._move('вперед') + self._move("вперед") self._stop() self._attack() - self._move('назад') + self._move("назад") @abstractmethod def _attack(self) -> None: @@ -48,7 +48,7 @@ def _move(self, direction: str) -> None: :param direction: направление движения """ - self._output('движется {} со скоростью {}'.format(direction, self._speed)) + self._output(f"движется {direction} со скоростью {self._speed}") def _output(self, message: str) -> None: """ @@ -56,7 +56,7 @@ def _output(self, message: str) -> None: :param message: выводимое сообщение """ - print('Отряд типа {} {}'.format(self.__class__.__name__, message)) + print(f"Отряд типа {self.__class__.__name__} {message}") class Archers(Unit): @@ -65,10 +65,10 @@ class Archers(Unit): """ def _attack(self) -> None: - self._output('обстреливает врага') + self._output("обстреливает врага") def _stop(self) -> None: - self._output('останавливается в 100 шагах от врага') + self._output("останавливается в 100 шагах от врага") class Cavarlymen(Unit): @@ -77,14 +77,14 @@ class Cavarlymen(Unit): """ def _attack(self) -> None: - self._output('на полном скаку врезается во вражеский строй') + self._output("на полном скаку врезается во вражеский строй") def _stop(self) -> None: - self._output('летит вперед, не останавливаясь') + self._output("летит вперед, не останавливаясь") -if __name__ == '__main__': - print('OUTPUT:') +if __name__ == "__main__": + print("OUTPUT:") archers = Archers(4) archers.hit_and_run() cavarlymen = Cavarlymen(8) diff --git a/design_patterns__examples/Template method/example_2.py b/design_patterns__examples/Template method/example_2.py index e3271ea52..6c6b81956 100644 --- a/design_patterns__examples/Template method/example_2.py +++ b/design_patterns__examples/Template method/example_2.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Template method — Шаблонный метод @@ -18,22 +18,22 @@ def end_of_game(self) -> bool: pass @abstractmethod - def initialize_game(self): + def initialize_game(self) -> None: pass @abstractmethod - def make_play(self, player: int): + def make_play(self, player: int) -> None: pass @abstractmethod - def print_winner(self): + def print_winner(self) -> None: pass # A template method : - def play_one_game(self, players_count: int): + def play_one_game(self, players_count: int) -> None: self.players_count = players_count self.initialize_game() - + j = 0 while not self.end_of_game(): @@ -46,18 +46,18 @@ def play_one_game(self, players_count: int): # Now we can extend this class in order to implement actual games: class Monopoly(GameObject): # Implementation of necessary concrete methods - def initialize_game(self): + def initialize_game(self) -> None: # Initialize money ... - def make_play(self, player: int): + def make_play(self, player: int) -> None: # Process one turn of player ... def end_of_game(self) -> bool: return True - def print_winner(self): + def print_winner(self) -> None: # Display who won ... @@ -67,11 +67,11 @@ def print_winner(self): class Chess(GameObject): # Implementation of necessary concrete methods - def initialize_game(self): + def initialize_game(self) -> None: # Put the pieces on the board ... - def make_play(self, player: int): + def make_play(self, player: int) -> None: # Process a turn for the player ... @@ -79,7 +79,7 @@ def end_of_game(self) -> bool: # Return true if in Checkmate or Stalemate has been reached return True - def print_winner(self): + def print_winner(self) -> None: # Display the winning player ... @@ -87,7 +87,7 @@ def print_winner(self): # ... -if __name__ == '__main__': +if __name__ == "__main__": game = Monopoly() game.play_one_game(2) diff --git a/design_patterns__examples/Template method/example_3.py b/design_patterns__examples/Template method/example_3.py index ff82da0ef..6ea680ff2 100644 --- a/design_patterns__examples/Template method/example_3.py +++ b/design_patterns__examples/Template method/example_3.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Template method — Шаблонный метод @@ -19,7 +19,7 @@ class Network(ABC): user_name: str password: str - + # Публикация данных в любой сети. def post(self, message: str) -> bool: # Проверка данных пользователя перед постом в соцсеть. Каждая сеть для @@ -29,7 +29,7 @@ def post(self, message: str) -> bool: result = self.send_data(message.encode()) self.log_out() return result - + return False @abstractmethod @@ -41,20 +41,20 @@ def send_data(self, data: bytes) -> bool: pass @abstractmethod - def log_out(self): + def log_out(self) -> None: pass # Класс социальной сети. class Facebook(Network): - def __init__(self, user_name: str, password: str): + def __init__(self, user_name: str, password: str) -> None: self.user_name = user_name self.password = password def log_in(self, user_name: str, password: str) -> bool: print("\nChecking user's parameters") print("Name: " + self.user_name) - print("Password: " + ('*' * len(self.password)), end='') + print("Password: " + ("*" * len(self.password)), end="") self._simulate_network_latency() print("\n\nlog_in success on Facebook") @@ -68,34 +68,34 @@ def send_data(self, data: bytes) -> bool: return False - def log_out(self): + def log_out(self) -> None: print("User: '" + self.user_name + "' was logged out from Facebook") - def _simulate_network_latency(self): + def _simulate_network_latency(self) -> None: print() try: i = 0 while i < 10: - print(".", end='') + print(".", end="") time.sleep(0.5) i += 1 - + except Exception as e: print(e) # Класс социальной сети. class Twitter(Network): - def __init__(self, user_name: str, password: str): + def __init__(self, user_name: str, password: str) -> None: self.user_name = user_name self.password = password def log_in(self, user_name: str, password: str) -> bool: print("\nChecking user's parameters") print("Name: " + self.user_name) - print("Password: " + ('*' * len(self.password)), end='') - + print("Password: " + ("*" * len(self.password)), end="") + self._simulate_network_latency() print("\n\nlog_in success on Twitter") return True @@ -108,31 +108,33 @@ def send_data(self, data: bytes) -> bool: return False - def log_out(self): + def log_out(self) -> None: print("User: '" + self.user_name + "' was logged out from Twitter") - def _simulate_network_latency(self): + def _simulate_network_latency(self) -> None: print() try: i = 0 while i < 10: - print(".", end='') + print(".", end="") time.sleep(0.5) i += 1 except Exception as e: print(e) - -if __name__ == '__main__': + +if __name__ == "__main__": user_name = input("Input user name: ") password = input("Input password: ") # Вводим сообщение. message = input("Input message: ") - choice = input("\nChoose social network for posting message.\n1 - Facebook\n2 - Twitter\n") + choice = input( + "\nChoose social network for posting message.\n1 - Facebook\n2 - Twitter\n" + ) network = None # Создаем сетевые объекты и публикуем пост. @@ -143,7 +145,7 @@ def _simulate_network_latency(self): network = Twitter(user_name, password) else: - print('Unknown network!') + print("Unknown network!") sys.exit() - + network.post(message) diff --git a/design_patterns__examples/Template method/example_4.py b/design_patterns__examples/Template method/example_4.py index dd48f5545..31c765e05 100644 --- a/design_patterns__examples/Template method/example_4.py +++ b/design_patterns__examples/Template method/example_4.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Design Patterns: Template def — Шаблонный метод @@ -10,7 +10,7 @@ from abc import ABC, abstractmethod -from typing import Optional, List +from typing import Optional class Map: @@ -19,7 +19,7 @@ def center(self) -> ...: class GameAI(ABC): - def __init__(self): + def __init__(self) -> None: self.scouts = [] self.warriors = [] self.map = Map() @@ -27,28 +27,28 @@ def __init__(self): # Шаблонный метод должен быть задан в базовом классе. Он # состоит из вызовов методов в определённом порядке. Чаще # всего эти методы являются шагами некоего алгоритма. - def turn(self): + def turn(self) -> None: self.collect_resources() self.build_structures() self.build_units() self.attack() # Некоторые из этих методов могут быть реализованы прямо в базовом классе. - def collect_resources(self): + def collect_resources(self) -> None: for s in self.build_structures(): s.collect() # А некоторые могут быть полностью абстрактными. @abstractmethod - def build_structures(self) -> List['Structure']: + def build_structures(self) -> list["Structure"]: pass @abstractmethod - def build_units(self): + def build_units(self) -> None: pass # Кстати, шаблонных методов в классе может быть несколько. - def attack(self): + def attack(self) -> None: enemy = self.closest_enemy() if enemy is None: @@ -56,22 +56,22 @@ def attack(self): else: self.send_warriors(enemy.position) - def closest_enemy(self) -> Optional['Enemy']: + def closest_enemy(self) -> Optional["Enemy"]: ... @abstractmethod - def send_scouts(self, position): + def send_scouts(self, position) -> None: pass @abstractmethod - def send_warriors(self, position): + def send_warriors(self, position) -> None: pass # Подклассы могут предоставлять свою реализацию шагов # алгоритма, не изменяя сам шаблонный метод. class OrcsAI(GameAI): - def build_structures(self) -> List['Structure']: + def build_structures(self) -> list["Structure"]: structures = [] there_are_some_resources: bool = ... @@ -82,7 +82,7 @@ def build_structures(self) -> List['Structure']: return structures - def build_units(self): + def build_units(self) -> None: there_are_plenty_of_resources: bool = ... there_are_no_scouts: bool = ... @@ -96,12 +96,12 @@ def build_units(self): # ... - def send_scouts(self, position): + def send_scouts(self, position) -> None: if self.scouts: # Отправить разведчиков на позицию. ... - def send_warriors(self, position): + def send_warriors(self, position) -> None: if len(self.warriors) > 5: # Отправить воинов на позицию. ... @@ -110,30 +110,30 @@ def send_warriors(self, position): # Подклассы могут не только реализовывать абстрактные шаги, но # и переопределять шаги, уже реализованные в базовом классе. class MonstersAI(GameAI): - def collect_resources(self): + def collect_resources(self) -> None: # Ничего не делать. pass - def build_structures(self): + def build_structures(self) -> None: # Ничего не делать. pass - def build_units(self): + def build_units(self) -> None: # Ничего не делать. pass - def send_scouts(self, position): + def send_scouts(self, position) -> None: if self.scouts: # Отправить разведчиков на позицию. ... - def send_warriors(self, position): + def send_warriors(self, position) -> None: if len(self.warriors) > 5: # Отправить воинов на позицию. ... -if __name__ == '__main__': +if __name__ == "__main__": ai = MonstersAI() ai.turn() diff --git a/destroy_chain_balls.py b/destroy_chain_balls.py index b966d5057..a38c2b0a2 100644 --- a/destroy_chain_balls.py +++ b/destroy_chain_balls.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def destroy_chain_balls(balls): @@ -41,27 +41,27 @@ def destroy_chain_balls(balls): return balls -if __name__ == '__main__': +if __name__ == "__main__": balls = [0, 1, 2, 3] balls_2 = destroy_chain_balls(balls) - print("{} -> {}, уничтожено: {}".format(balls, balls_2, len(balls) - len(balls_2))) + print(f"{balls} -> {balls_2}, уничтожено: {len(balls) - len(balls_2)}") balls = [0, 1, 2, 2, 1, 2, 3] balls_2 = destroy_chain_balls(balls) - print("{} -> {}, уничтожено: {}".format(balls, balls_2, len(balls) - len(balls_2))) + print(f"{balls} -> {balls_2}, уничтожено: {len(balls) - len(balls_2)}") balls = [0, 1, 2, 2, 1, 1, 1, 2, 3] balls_2 = destroy_chain_balls(balls) - print("{} -> {}, уничтожено: {}".format(balls, balls_2, len(balls) - len(balls_2))) + print(f"{balls} -> {balls_2}, уничтожено: {len(balls) - len(balls_2)}") balls = [1, 3, 3, 3, 2] balls_2 = destroy_chain_balls(balls) - print("{} -> {}, уничтожено: {}".format(balls, balls_2, len(balls) - len(balls_2))) + print(f"{balls} -> {balls_2}, уничтожено: {len(balls) - len(balls_2)}") balls = [1, 1, 1] balls_2 = destroy_chain_balls(balls) - print("{} -> {}, уничтожено: {}".format(balls, balls_2, len(balls) - len(balls_2))) + print(f"{balls} -> {balls_2}, уничтожено: {len(balls) - len(balls_2)}") balls = [1, 1] balls_2 = destroy_chain_balls(balls) - print("{} -> {}, уничтожено: {}".format(balls, balls_2, len(balls) - len(balls_2))) + print(f"{balls} -> {balls_2}, уничтожено: {len(balls) - len(balls_2)}") diff --git a/detection_of_site_changes_Unistream/gui.py b/detection_of_site_changes_Unistream/gui.py index 0d594e234..afaf178f9 100644 --- a/detection_of_site_changes_Unistream/gui.py +++ b/detection_of_site_changes_Unistream/gui.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт создает окно, позволяющее просматривать все ревизии и их различие.""" +import os import sys from PySide.QtGui import * @@ -21,10 +22,10 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('detection_of_site_changes_Unistream') + self.setWindowTitle("detection_of_site_changes_Unistream") self.revision_list_widget = QListWidget() self.revision_list_widget.itemDoubleClicked.connect(self._show_last_diff) @@ -47,28 +48,31 @@ def __init__(self): if len(items) > 1: for item in items: # Придаем элементу ревизии цвет, зависящий от первых 6-ти символов его хеша - color = QColor('#' + text_hash[:6]) + color = QColor("#" + text_hash[:6]) item.setBackground(color) self.setCentralWidget(self.revision_list_widget) - def _show_last_diff(self, item): + def _show_last_diff(self, item) -> None: row = self.revision_list_widget.row(item) - file_name_a = 'file_a' - file_name_b = 'file_b' + file_name_a = "file_a" + file_name_b = "file_b" - file_a_text = self.revision_list_widget.item(row - 1).data(Qt.UserRole).text if row > 0 else '' + file_a_text = ( + self.revision_list_widget.item(row - 1).data(Qt.UserRole).text + if row > 0 + else "" + ) file_b_text = self.revision_list_widget.item(row).data(Qt.UserRole).text - with open(file_name_a, mode='w', encoding='utf-8') as f: + with open(file_name_a, mode="w", encoding="utf-8") as f: f.write(file_a_text) - with open(file_name_b, mode='w', encoding='utf-8') as f: + with open(file_name_b, mode="w", encoding="utf-8") as f: f.write(file_b_text) - import os - os.system('kdiff3 {} {}'.format(file_name_a, file_name_b)) + os.system(f"kdiff3 {file_name_a} {file_name_b}") if os.path.exists(file_name_a): os.remove(file_name_a) @@ -78,7 +82,7 @@ def _show_last_diff(self, item): # TODO: выделять одинаковые элементы одним цветом -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) mw = MainWindow() diff --git a/detection_of_site_changes_Unistream/main.py b/detection_of_site_changes_Unistream/main.py index f190d6a82..bfb3676bb 100644 --- a/detection_of_site_changes_Unistream/main.py +++ b/detection_of_site_changes_Unistream/main.py @@ -1,50 +1,64 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import difflib +import hashlib +import logging +import os +import sys + +from PySide.QtGui import QApplication +from PySide.QtCore import QEventLoop +from PySide.QtWebKit import QWebSettings, QWebPage, QWebView +from PySide.QtNetwork import QNetworkProxyFactory + +from sqlalchemy import create_engine, Column, Integer, String, DateTime +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + # NOTE: костыль для винды, для исправления проблем с исключениями # при выводе юникодных символов в консоль винды -import sys -if sys.platform == 'win32': +if sys.platform == "win32": import codecs + # Для винды кодировкой консоли будет cp866 - sys.stdout = codecs.getwriter(sys.stdout.encoding)(sys.stdout.detach(), 'backslashreplace') - sys.stderr = codecs.getwriter(sys.stderr.encoding)(sys.stderr.detach(), 'backslashreplace') + sys.stdout = codecs.getwriter(sys.stdout.encoding)( + sys.stdout.detach(), "backslashreplace" + ) + sys.stderr = codecs.getwriter(sys.stderr.encoding)( + sys.stderr.detach(), "backslashreplace" + ) + -import os DIR = os.path.dirname(__file__) - -import logging + + logging.basicConfig( level=logging.DEBUG, - format='[%(asctime)s] %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s', + format="[%(asctime)s] %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s", handlers=[ - logging.FileHandler(os.path.join(DIR, 'log'), encoding='utf8'), + logging.FileHandler(os.path.join(DIR, "log"), encoding="utf8"), logging.StreamHandler(stream=sys.stdout), ], ) -def get_site_text(url='https://test.api.unistream.com/help/index.html'): +def get_site_text(url="https://test.api.unistream.com/help/index.html"): """Функция возвращает содержимое по указанному url.""" - import sys - - from PySide.QtGui import QApplication - from PySide.QtCore import QEventLoop - from PySide.QtWebKit import QWebSettings, QWebPage, QWebView - from PySide.QtNetwork import QNetworkProxyFactory - # Чтобы не было проблем запуска компов с прокси: QNetworkProxyFactory.setUseSystemConfiguration(True) - QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, True) + QWebSettings.globalSettings().setAttribute( + QWebSettings.DeveloperExtrasEnabled, True + ) class WebPage(QWebPage): - def userAgentForUrl(self, url): - return 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0' + def userAgentForUrl(self, url) -> str: + return "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0" if QApplication.instance() is None: QApplication(sys.argv) @@ -66,7 +80,6 @@ def userAgentForUrl(self, url): def get_hash_from_str(text): """Функция возвращает хеш от строки в виде HEX чисел, используя алгоритм sha1.""" - import hashlib alg = hashlib.sha1() alg.update(text.encode()) return alg.hexdigest().upper() @@ -78,15 +91,13 @@ def get_diff(str_1, str_2, full=True): """ - logging.debug('x1') - import difflib - - logging.debug('x2') + logging.debug("x1") + logging.debug("x2") diff_html = "" - logging.debug('x3') + logging.debug("x3") theDiffs = difflib.ndiff(str_1.splitlines(), str_2.splitlines()) - logging.debug('x4') + logging.debug("x4") theDiffs = list(theDiffs) print(theDiffs) for eachDiff in theDiffs: @@ -94,18 +105,20 @@ def get_diff(str_1, str_2, full=True): diff_html += "%s
" % eachDiff[1:].strip() elif eachDiff[0] == "+": diff_html += "%s
" % eachDiff[1:].strip() - logging.debug('x5') + logging.debug("x5") print(112121, diff_html) if full: - return """ """ + diff_html + "" + return ( + """ """ + + diff_html + + "" + ) else: return diff_html -from sqlalchemy import Column, Integer, String, DateTime -from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() @@ -116,7 +129,7 @@ class TextRevision(Base): """ - __tablename__ = 'TextRevision' + __tablename__ = "TextRevision" id = Column(Integer, primary_key=True) @@ -136,7 +149,7 @@ class TextRevision(Base): # Содержимым является только разница diff = Column(String) - def __init__(self, new_text, old_text=''): + def __init__(self, new_text, old_text="") -> None: """ Конструктор принимает контент и сравниваемый контент, запоминает хеш содержимого, текущую дату и время и результат сравнения @@ -153,25 +166,23 @@ def __init__(self, new_text, old_text=''): self.diff_full = get_diff(old_text, new_text) self.diff = get_diff(old_text, new_text, full=False) - def __repr__(self): - return "".format(self.id, self.datetime, self.text_hash) + def __repr__(self) -> str: + return f"" def get_session(): - DB_FILE_NAME = 'sqlite:///' + os.path.join(DIR, 'database') + DB_FILE_NAME = "sqlite:///" + os.path.join(DIR, "database") # DB_FILE_NAME = 'sqlite:///:memory:' # Создаем базу, включаем логирование и автообновление подключения каждые 2 часа (7200 секунд) - from sqlalchemy import create_engine engine = create_engine( DB_FILE_NAME, # echo=True, - pool_recycle=7200 + pool_recycle=7200, ) Base.metadata.create_all(engine) - from sqlalchemy.orm import sessionmaker Session = sessionmaker(bind=engine) return Session() @@ -189,9 +200,7 @@ def add_text_revision(text): """Функция добавляет новую ревизию, предварительно сравнив ее с предыдущей.""" last = get_last_revision() - logging.debug('Последняя запись: %s.', last) - - text_revision = None + logging.debug("Последняя запись: %s.", last) # Если таблица пуста if last is None: @@ -199,32 +208,32 @@ def add_text_revision(text): else: # Если хеши текстов отличаются, добавляем новую ревизию if last.text_hash != get_hash_from_str(text): - logging.debug('Обнаружено изменение, создаю ревизию.') + logging.debug("Обнаружено изменение, создаю ревизию.") text_revision = TextRevision(text, last.text) - logging.debug('-') + logging.debug("-") else: - logging.debug('Одинаковые значения, пропускаю добавление.') + logging.debug("Одинаковые значения, пропускаю добавление.") return - logging.debug('@') + logging.debug("@") if text_revision: - logging.debug('add') + logging.debug("add") session.add(text_revision) - logging.debug('commit') + logging.debug("commit") session.commit() - logging.debug('return') + logging.debug("return") return text_revision -if __name__ == '__main__': - logging.debug('Запуск.') +if __name__ == "__main__": + logging.debug("Запуск.") import time while True: try: - logging.debug('Проверка сайта.') + logging.debug("Проверка сайта.") # text = "dfsdfsdfsdfsdf" # text += get_site_text() @@ -243,6 +252,7 @@ def add_text_revision(text): # "createTime": "2016-06-22T10:13:42.0888396+03:00", # "templateId": "e6635cf6-47c3-4164-8173-cb8fd249604a" import re + text = re.sub(r'"((?i)OperationId)": ".+?"', r'"\1": ""', text) text = re.sub(r'"((?i)CommandId)": ".+?"', r'"\1": ""', text) text = re.sub(r'"((?i)cashierUniqueId)": ".+?"', r'"\1": ""', text) @@ -253,7 +263,7 @@ def add_text_revision(text): continue add_text_revision(text) - logging.debug('Проверка закончена.') + logging.debug("Проверка закончена.") # last = get_last_revision() # print(len(last.text)) @@ -264,7 +274,7 @@ def add_text_revision(text): # Задержка каждые 7 часов time.sleep(60 * 60 * 7) except Exception: - logging.exception('Error:') + logging.exception("Error:") # last = get_last_revision() # print(len(last.text)) diff --git a/detection_of_site_changes_Unistream/print_all_revision.py b/detection_of_site_changes_Unistream/print_all_revision.py index 2b1e665bc..ae9b38c66 100644 --- a/detection_of_site_changes_Unistream/print_all_revision.py +++ b/detection_of_site_changes_Unistream/print_all_revision.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт выводит все текстовые ревизии.""" -if __name__ == '__main__': +if __name__ == "__main__": from main import session, TextRevision text_revisions = session.query(TextRevision).all() diff --git a/detection_of_site_changes_Unistream/remove_last_diff.py b/detection_of_site_changes_Unistream/remove_last_diff.py index e32e6e328..9bca996c2 100644 --- a/detection_of_site_changes_Unistream/remove_last_diff.py +++ b/detection_of_site_changes_Unistream/remove_last_diff.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт удаляет последнюю ревизию.""" -if __name__ == '__main__': +if __name__ == "__main__": from main import session, TextRevision text_revision = session.query(TextRevision).order_by(TextRevision.id.desc()).first() diff --git a/detection_of_site_changes_Unistream/show_last_diff.py b/detection_of_site_changes_Unistream/show_last_diff.py index 7ea360b09..73f8ecb15 100644 --- a/detection_of_site_changes_Unistream/show_last_diff.py +++ b/detection_of_site_changes_Unistream/show_last_diff.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт находит последние две ревизии, сохраняет их текст в файлы и сравнивает с помощью kdiff3.""" -if __name__ == '__main__': +if __name__ == "__main__": + import os from main import session, TextRevision # TODO: нет смысла запрашивать все, если нужны только 2 последнии @@ -15,17 +16,16 @@ if len(text_revisions) >= 2: file_a, file_b = text_revisions[-2:] - file_name_a = 'file_a' - file_name_b = 'file_b' + file_name_a = "file_a" + file_name_b = "file_b" - with open(file_name_a, mode='w', encoding='utf-8') as f: + with open(file_name_a, mode="w", encoding="utf-8") as f: f.write(file_a.text) - with open(file_name_b, mode='w', encoding='utf-8') as f: + with open(file_name_b, mode="w", encoding="utf-8") as f: f.write(file_b.text) - import os - os.system('kdiff3 {} {}'.format(file_name_a, file_name_b)) + os.system(f"kdiff3 {file_name_a} {file_name_b}") if os.path.exists(file_name_a): os.remove(file_name_a) diff --git a/determines the type of image/main.py b/determines the type of image/main.py index 33b7dc80c..c56b0450b 100644 --- a/determines the type of image/main.py +++ b/determines the type of image/main.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import glob -for file_name in glob.glob('img/*.txt'): - import imghdr +import imghdr + + +for file_name in glob.glob("img/*.txt"): img_type = imghdr.what(file_name) - print('{} -> {}'.format(file_name, img_type)) + print(f"{file_name} -> {img_type}") diff --git a/dict_to_url_params.py b/dict_to_url_params.py index dbfc2d303..c30173bf5 100644 --- a/dict_to_url_params.py +++ b/dict_to_url_params.py @@ -1,23 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def dict_to_url_params(json_data, root): - def deep(node, root, items): + def deep(node, root, items) -> None: if isinstance(node, list): for i, value in enumerate(node): - node_root = root + f'[{i}]' + node_root = root + f"[{i}]" deep(value, node_root, items) elif isinstance(node, dict): for key, value in node.items(): - node_root = root + f'[{key}]' + node_root = root + f"[{key}]" deep(value, node_root, items) else: - root += '=' + node + root += "=" + node items.append(root) items = [] @@ -27,7 +27,7 @@ def deep(node, root, items): return items -if __name__ == '__main__': +if __name__ == "__main__": JSON_FORM_DATA = { "add": [ { @@ -36,9 +36,7 @@ def deep(node, root, items): "created_at": "1529007000", "incoming_entities": { "leads": [ - { - "name": "Покупка" - }, + {"name": "Покупка"}, ], "contacts": [ { @@ -48,48 +46,37 @@ def deep(node, root, items): { "id": "382707", "values": [ - { - "value": "+77777777777", - "enum": "WORK" - } - ] + {"value": "+77777777777", "enum": "WORK"} + ], }, { "id": "389993", - "values": [ - { - "value": "sfgh3gh233h3h3h3" - } - ] + "values": [{"value": "sfgh3gh233h3h3h3"}], }, { "id": "389995", - "values": [ - { - "value": "Обратный звонок" - } - ] - } - ] + "values": [{"value": "Обратный звонок"}], + }, + ], } - ] + ], }, "incoming_lead_info": { "form_id": "329248", "form_page": "vdtest.ru", "ip": "127.0.0.1", - "service_code": "QkKwSam8" - } + "service_code": "QkKwSam8", + }, } ] } - items = dict_to_url_params(JSON_FORM_DATA['add'], root='add') + items = dict_to_url_params(JSON_FORM_DATA["add"], root="add") - params = '&'.join(items) - print('Params: ' + params) + params = "&".join(items) + print("Params: " + params) print() - print('Params:') + print("Params:") for x in items: print(x) diff --git a/diff_between_dates.py b/diff_between_dates.py index 6b4d3623a..78b38de22 100644 --- a/diff_between_dates.py +++ b/diff_between_dates.py @@ -1,24 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from datetime import datetime items = [ - '23/03/2007', - '05/12/2007', - '22/08/2008', - '02/10/2009', + "23/03/2007", + "05/12/2007", + "22/08/2008", + "02/10/2009", ] for i in range(len(items) - 1): date_str_1, date_str_2 = items[i], items[i + 1] - date_1 = datetime.strptime(date_str_1, '%d/%m/%Y') - date_2 = datetime.strptime(date_str_2, '%d/%m/%Y') + date_1 = datetime.strptime(date_str_1, "%d/%m/%Y") + date_2 = datetime.strptime(date_str_2, "%d/%m/%Y") days = (date_2 - date_1).days - print('{} - {} -> {} days'.format(date_str_1, date_str_2, days)) + print(f"{date_str_1} - {date_str_2} -> {days} days") diff --git a/diff_iterables.py b/diff_iterables.py new file mode 100644 index 000000000..bde957abb --- /dev/null +++ b/diff_iterables.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass +from typing import Iterable + + +@dataclass +class Result: + added: list + deleted: list + + +def diff_iterables(iterable_1: Iterable, iterable_2: Iterable) -> Result: + return Result( + added=[x for x in iterable_2 if x not in iterable_1], + deleted=[x for x in iterable_1 if x not in iterable_2], + ) + + +if __name__ == "__main__": + items_1 = "01234" + items_2 = "12456" + result = diff_iterables(items_1, items_2) + print(result) + assert ( + Result( + added=["5", "6"], + deleted=["0", "3"], + ) + == result + ) + # Result(added=['5', '6'], deleted=['0', '3']) + + assert ( + Result(added=[], deleted=[]) + == diff_iterables(items_1, items_1) + ) + + assert ( + Result(added=["4"], deleted=[]) + == diff_iterables("123", "1234") + ) + + assert ( + Result(added=[], deleted=["3"]) + == diff_iterables("123", "12") + ) diff --git a/diff_times.py b/diff_times.py index 2df9b7268..c4c843aeb 100644 --- a/diff_times.py +++ b/diff_times.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +from datetime import datetime + check_hours, check_minutes = 16, 29 -dt = DT.datetime.now() +dt = datetime.now() check_dt = dt.replace(hour=check_hours, minute=check_minutes) if dt > check_dt: diff --git a/difflib__examples/hello_world.py b/difflib__examples/hello_world.py index 42d75feca..a31140811 100644 --- a/difflib__examples/hello_world.py +++ b/difflib__examples/hello_world.py @@ -1,18 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import difflib words = [ - 'смыло', 'соло', 'вяло', 'мяло', 'смола', 'поле', 'воля', 'с мола', 'была', - 'стела', 'с мела', 'смело' + "смыло", + "соло", + "вяло", + "мяло", + "смола", + "поле", + "воля", + "с мола", + "была", + "стела", + "с мела", + "смело", ] -word = 'смело' +word = "смело" -print(difflib.get_close_matches(word, words)) # ['смело', 'смыло', 'с мела'] -print(difflib.get_close_matches(word, words, cutoff=0.6)) # ['смело', 'смыло', 'с мела'] -print(difflib.get_close_matches(word, words, cutoff=0.8)) # ['смело', 'смыло'] +print(difflib.get_close_matches(word, words)) +# ['смело', 'смыло', 'с мела'] + +print(difflib.get_close_matches(word, words, cutoff=0.6)) +# ['смело', 'смыло', 'с мела'] + +print(difflib.get_close_matches(word, words, cutoff=0.8)) +# ['смело', 'смыло'] diff --git a/digital_pyramid.py b/digital_pyramid.py index ccb94077c..9a3ad6409 100644 --- a/digital_pyramid.py +++ b/digital_pyramid.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" n = 9 for i in range(1, n + 1): - left = ''.join(map(str, range(1, i))) - spaces = ' ' * (n - i) - print(f'{spaces}{left}{i}{left[::-1]}') + left = "".join(map(str, range(1, i))) + spaces = " " * (n - i) + print(f"{spaces}{left}{i}{left[::-1]}") # 1 # 121 @@ -26,7 +26,7 @@ x = "" for i in range(1, n + 1): x += str(i) - print(' ' * (n-i) + x + x[-2::-1]) + print(" " * (n - i) + x + x[-2::-1]) # 1 # 121 # 12321 diff --git a/dis__bytecode__is_empty_function.py b/dis__bytecode__is_empty_function.py index 3edd59b9f..7f54f9bea 100644 --- a/dis__bytecode__is_empty_function.py +++ b/dis__bytecode__is_empty_function.py @@ -1,27 +1,30 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import dis # https://ru.stackoverflow.com/a/767523/201445 def check_is_empty_function(function): - import dis instructions = dis.get_instructions(function) instr = next(instructions, None) - if instr is None or instr.opname != 'LOAD_CONST' or instr.argrepr != 'None': + if instr is None or instr.opname != "LOAD_CONST" or instr.argrepr != "None": return False instr = next(instructions, None) - return instr and (instr.opname == 'RETURN_VALUE') + return instr and (instr.opname == "RETURN_VALUE") + +if __name__ == "__main__": -if __name__ == '__main__': - def foo(): + def foo() -> None: pass - def foo2(): + def foo2() -> int: return 1 - print('Empty:', check_is_empty_function(foo)) - print('Empty:', check_is_empty_function(foo2)) + print("Empty:", check_is_empty_function(foo)) + print("Empty:", check_is_empty_function(foo2)) diff --git a/disabling_redirection/requests_example.py b/disabling_redirection/requests_example.py index c1d2b86d2..86dbab794 100644 --- a/disabling_redirection/requests_example.py +++ b/disabling_redirection/requests_example.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import requests -rs = requests.get('http://ya.ru/') +rs = requests.get("http://ya.ru/") print(rs.status_code, rs.url) # 200 https://ya.ru/ -rs = requests.get('http://ya.ru/', allow_redirects=False) +rs = requests.get("http://ya.ru/", allow_redirects=False) print(rs.status_code, rs.url) # 302 http://ya.ru/ diff --git a/disabling_redirection/urllib_example.py b/disabling_redirection/urllib_example.py index 15e24979d..57de554d9 100644 --- a/disabling_redirection/urllib_example.py +++ b/disabling_redirection/urllib_example.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import urllib.request @@ -14,13 +14,13 @@ def http_error_301(self, req, fp, code, msg, headers): http_error_302 = http_error_303 = http_error_307 = http_error_301 -rs = urllib.request.urlopen('http://ya.ru/') +rs = urllib.request.urlopen("http://ya.ru/") print(rs.code, rs.url) # 200 https://ya.ru/ opener = urllib.request.build_opener(NoRedirectHandler()) urllib.request.install_opener(opener) -rs = urllib.request.urlopen('http://ya.ru/') +rs = urllib.request.urlopen("http://ya.ru/") print(rs.code, rs.url) # 302 http://ya.ru/ diff --git a/do_krakozabry.py b/do_krakozabry.py index 41beadf97..c4216acd0 100644 --- a/do_krakozabry.py +++ b/do_krakozabry.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -encoding_1 = 'utf-8' -encoding_2 = 'cp1251' +encoding_1 = "utf-8" +encoding_2 = "cp1251" -text = 'Ну и правильно, что обращаешься за помощью на стековерфлоу' +text = "Ну и правильно, что обращаешься за помощью на стековерфлоу" # РќСѓ Рё правильно, что обращаешься Р·Р° помощью РЅР° стековерфлоу print(text.encode(encoding_1).decode(encoding_2)) diff --git a/download_and_solving_algebraic_expressions.py b/download_and_solving_algebraic_expressions.py index 7992a4b7b..8b6a9a02a 100644 --- a/download_and_solving_algebraic_expressions.py +++ b/download_and_solving_algebraic_expressions.py @@ -1,19 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Скачивание файла import requests -rs = requests.get('https://cloclo18.datacloudmail.ru/weblink/view/emCb/gXFkchRJ2?etag=7706DA739680EAC4A5B9044E9767047365988F54&key=a91762c6f1d8a559f6d780934b2509c728b33df4') + + +rs = requests.get( + "https://cloclo18.datacloudmail.ru/weblink/view/emCb/gXFkchRJ2?etag=7706DA739680EAC4A5B9044E9767047365988F54&key=a91762c6f1d8a559f6d780934b2509c728b33df4" +) # Получение текста, разделение его построчно, пронумерование -for i, line in enumerate(rs.text.split('\n'), 1): +for i, line in enumerate(rs.text.split("\n"), 1): # Если строка пустая if not line: continue # Обрезание первого символа, удаление ' ', '\n', '\r' и т.п. line = line[1:].strip() - print('{}. {} = {}'.format(i, line, eval(line))) + print(f"{i}. {line} = {eval(line)}") diff --git a/download_file/download_file.py b/download_file/download_file.py index 901e692c7..bb3e5e297 100644 --- a/download_file/download_file.py +++ b/download_file/download_file.py @@ -1,65 +1,71 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" """Способы скачивания файлов с сайтов.""" +import time +from urllib.request import urlopen, urlretrieve + +# pip install httplib2 +import httplib2 + +# pip install requests +import requests + +# pip install grab +from grab import Grab + + def timer(f): def wrapper(*args, **kwargs): - import time t = time.time() r = f(*args, **kwargs) - print("Время выполнения функции: %f сек." % (time.time() - t)) + print(f"Время выполнения функции: {time.time() - t:f} сек.") return r + return wrapper @timer -def way1(url, file_name): - from urllib.request import urlopen +def way1(url: str, file_name: str) -> None: resource = urlopen(url) - with open(file_name, 'wb') as f: + with open(file_name, "wb") as f: f.write(resource.read()) @timer -def way2(url, file_name): - from urllib.request import urlretrieve +def way2(url: str, file_name: str) -> None: urlretrieve(url, file_name) @timer -def way3(url, file_name): - import requests - +def way3(url: str, file_name: str) -> None: p = requests.get(url) with open(file_name, "wb") as f: f.write(p.content) @timer -def way4(url, file_name): - import httplib2 - - h = httplib2.Http('.cache') +def way4(url: str, file_name: str) -> None: + h = httplib2.Http(".cache") response, content = h.request(url) - with open(file_name, 'wb') as f: + with open(file_name, "wb") as f: f.write(content) @timer -def way5(url, file_name): - from grab import Grab +def way5(url: str, file_name: str) -> None: g = Grab() g.go(url) g.response.save(file_name) -if __name__ == '__main__': - url_img = 'http://shikimori.org/images/character/original/55741.jpg' +if __name__ == "__main__": + url_img = "https://github.com/gil9red/telegram__random_bashim_bot/blob/b24139b536abf5b217b316405bbe224bc473fbfa/screenshots/screenshot_comics.jpg" way1(url_img, "img1.jpg") way2(url_img, "img2.jpg") way3(url_img, "img3.jpg") way4(url_img, "img4.jpg") - way5(url_img, "img5.jpg") \ No newline at end of file + way5(url_img, "img5.jpg") diff --git a/download_file/with_progress.py b/download_file/with_progress.py index 805c5ae9a..4eccbcc5a 100644 --- a/download_file/with_progress.py +++ b/download_file/with_progress.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -9,7 +9,7 @@ from threading import Thread -def reporthook(blocknum, blocksize, totalsize): +def reporthook(blocknum, blocksize, totalsize) -> None: readsofar = blocknum * blocksize if totalsize > 0: percent = readsofar * 100.0 / totalsize @@ -27,8 +27,11 @@ def reporthook(blocknum, blocksize, totalsize): sys.stdout.write(f"read {readsofar}\n") -def download(url: str, file_name: str = None, as_thread=False, callback_func=None) -> str: +def download( + url: str, file_name: str = None, as_thread=False, callback_func=None +) -> str: if as_thread: + def run(url, file_name, reporthook, callback_func): local_file_name, _ = urlretrieve(url, file_name, reporthook=reporthook) if callable(callback_func): @@ -42,17 +45,22 @@ def run(url, file_name, reporthook, callback_func): return urlretrieve(url, file_name, reporthook=reporthook)[0] -if __name__ == '__main__': - URL = 'https://codeload.github.com/gil9red/SimplePyScripts/zip/master' +if __name__ == "__main__": + URL = "https://codeload.github.com/gil9red/SimplePyScripts/zip/master" print(download(URL)) print() - print(download(URL, 'SimplePyScripts.zip')) + print(download(URL, "SimplePyScripts.zip")) + + print("\n") + sys.stderr.write("Threading...\n") - print('\n') - sys.stderr.write('Threading...\n') + print(download(URL, "SimplePyScripts.zip", as_thread=True)) - print(download(URL, 'SimplePyScripts.zip', as_thread=True)) + def callback_func(file_name: str) -> None: + print("File name:", file_name) - def callback_func(file_name: str): - print('File name:', file_name) - print(download(URL, 'SimplePyScripts.zip', as_thread=True, callback_func=callback_func)) + print( + download( + URL, "SimplePyScripts.zip", as_thread=True, callback_func=callback_func + ) + ) diff --git a/download_file_with_progressbar/download_in_file__using_tqdm.py b/download_file_with_progressbar/download_in_file__using_tqdm.py index 92c2a1d6b..9043974a3 100644 --- a/download_file_with_progressbar/download_in_file__using_tqdm.py +++ b/download_file_with_progressbar/download_in_file__using_tqdm.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os @@ -13,26 +13,29 @@ # pip install requests import requests -from utils import sizeof_fmt +# pip install humanize +from humanize import naturalsize as sizeof_fmt -url = 'https://github.com/gil9red/NotesManager/raw/master/bin.rar' +url = "https://github.com/gil9red/NotesManager/raw/master/bin.rar" # Streaming, so we can iterate over the response. rs = requests.get(url, stream=True) # Total size in bytes. -total_size = int(rs.headers.get('content-length', 0)) -print('From content-length:', sizeof_fmt(total_size)) +total_size = int(rs.headers.get("content-length", 0)) +print("From content-length:", sizeof_fmt(total_size)) chunk_size = 1024 num_bars = int(total_size / chunk_size) file_name = os.path.basename(url) -with open(file_name, mode='wb') as f: - for data in tqdm(rs.iter_content(chunk_size), total=num_bars, unit='KB', file=sys.stdout): +with open(file_name, mode="wb") as f: + for data in tqdm( + rs.iter_content(chunk_size), total=num_bars, unit="KB", file=sys.stdout + ): f.write(data) # Read from file -file_data = open(file_name, mode='rb').read() -print('File data size:', sizeof_fmt(len(file_data))) +file_data = open(file_name, mode="rb").read() +print("File data size:", sizeof_fmt(len(file_data))) diff --git a/download_file_with_progressbar/download_in_memory__using_tqdm.py b/download_file_with_progressbar/download_in_memory__using_tqdm.py index 72f57eb58..edf6e48ea 100644 --- a/download_file_with_progressbar/download_in_memory__using_tqdm.py +++ b/download_file_with_progressbar/download_in_memory__using_tqdm.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import io @@ -13,24 +13,27 @@ # pip install requests import requests -from utils import sizeof_fmt +# pip install humanize +from humanize import naturalsize as sizeof_fmt -url = 'https://github.com/gil9red/NotesManager/raw/master/bin.rar' +url = "https://github.com/gil9red/NotesManager/raw/master/bin.rar" # Streaming, so we can iterate over the response. rs = requests.get(url, stream=True) # Total size in bytes. -total_size = int(rs.headers.get('content-length', 0)) -print('From content-length:', sizeof_fmt(total_size)) +total_size = int(rs.headers.get("content-length", 0)) +print("From content-length:", sizeof_fmt(total_size)) bytes_buffer = io.BytesIO() chunk_size = 1024 num_bars = int(total_size / chunk_size) -for data in tqdm(rs.iter_content(chunk_size), total=num_bars, unit='KB', file=sys.stdout): +for data in tqdm( + rs.iter_content(chunk_size), total=num_bars, unit="KB", file=sys.stdout +): bytes_buffer.write(data) file_data = bytes_buffer.getvalue() -print('File data size:', sizeof_fmt(len(file_data))) +print("File data size:", sizeof_fmt(len(file_data))) diff --git a/download_file_with_progressbar/utils.py b/download_file_with_progressbar/utils.py deleted file mode 100644 index 6432bece4..000000000 --- a/download_file_with_progressbar/utils.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import sys -from pathlib import Path - -DIR = Path(__file__).resolve().parent - -sys.path.append(str(DIR.parent)) -from human_byte_size import sizeof_fmt diff --git a/download_volume_readmanga.py b/download_volume_readmanga.py index 5bb3172ba..b29ce0054 100644 --- a/download_volume_readmanga.py +++ b/download_volume_readmanga.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт для скачивания главы по указанному url.""" @@ -9,39 +9,45 @@ import ast import re +import zipfile import requests -PATTERN = re.compile(r'\.init\(.*(\[\[.+\]\]).*\)') + +PATTERN = re.compile(r"\.init\(.*(\[\[.+\]\]).*\)") def get_url_images(url): - print('Start get_url_images with url:', url) + print("Start get_url_images with url:", url) rs = requests.get(url) match = PATTERN.search(rs.text) if not match: - raise Exception('Не получилось из страницы вытащить список картинок главы. ' - 'Используемое регулярное выражение: ', PATTERN.pattern) + raise Exception( + "Не получилось из страницы вытащить список картинок главы. " + "Используемое регулярное выражение: ", + PATTERN.pattern, + ) match = match.group(1) - print('Match:', match) + print("Match:", match) urls = ast.literal_eval(match) - print('After parse match:', urls) + print("After parse match:", urls) return [i[0] + i[2] for i in urls] -def save_urls_to_zip(zip_file_name, urls): +def save_urls_to_zip(zip_file_name, urls) -> None: if not urls: - print('Cписок изображений пустой.') + print("Cписок изображений пустой.") return # Создаем архив, у которого именем будет номер главы - import zipfile - with zipfile.ZipFile(zip_file_name, mode='w', compression=zipfile.ZIP_DEFLATED) as f: + with zipfile.ZipFile( + zip_file_name, mode="w", compression=zipfile.ZIP_DEFLATED + ) as f: import os from urllib.request import urlretrieve @@ -62,21 +68,24 @@ def save_urls_to_zip(zip_file_name, urls): os.remove(file_name) -if __name__ == '__main__': - url = 'https://readmanga.live/one_punch_man__A1bc88e/vol1/1' +if __name__ == "__main__": + import traceback + from pathlib import Path + + url = "https://readmanga.live/one_punch_man__A1bc88e/vol1/1" try: urls = get_url_images(url) - print(f'Urls ({len(urls)}):') + print(f"Urls ({len(urls)}):") for i, x in enumerate(urls, 1): - print(f'{i}. {x}') + print(f"{i}. {x}") - from pathlib import Path - url_file_name = url.replace('http://', '').replace('https://', '').replace('/', '_') - file_name = Path(__file__).resolve().name + ' - ' + url_file_name + '.zip' + url_file_name = ( + url.replace("http://", "").replace("https://", "").replace("/", "_") + ) + file_name = Path(__file__).resolve().name + " - " + url_file_name + ".zip" save_urls_to_zip(file_name, urls) - print('Save to filename:', file_name) + print("Save to filename:", file_name) except Exception as e: - import traceback - print('Error: {}\n\n{}'.format(e, traceback.format_exc())) + print(f"Error: {e}\n\n{traceback.format_exc()}") diff --git a/downloads_database_domains/downloads_database_domains.py b/downloads_database_domains/downloads_database_domains.py index 79e753477..fad9e1222 100644 --- a/downloads_database_domains/downloads_database_domains.py +++ b/downloads_database_domains/downloads_database_domains.py @@ -1,24 +1,25 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт скачивает базу доменов в зоне ru и выводит ее в лог""" -if __name__ == '__main__': - from urllib.request import urlretrieve - from os.path import basename - url = 'https://partner.r01.ru/zones/ru_domains.gz' - file_name = basename(url) +import gzip - # Скачиваем архив - urlretrieve(url, file_name) +from urllib.request import urlretrieve +from os.path import basename - import gzip - # Открытие архива - with gzip.open(file_name, 'rb') as f: - c = 1 - # Построчный вывод архивированного файла - for line in f: - print("{} {}".format(c, line)) - c += 1 \ No newline at end of file +url = "https://partner.r01.ru/zones/ru_domains.gz" +file_name = basename(url) + +# Скачиваем архив +urlretrieve(url, file_name) + +# Открытие архива +with gzip.open(file_name, "rb") as f: + c = 1 + # Построчный вывод архивированного файла + for line in f: + print(f"{c} {line}") + c += 1 diff --git a/dpath__examples__search_filter__dict_json/main.py b/dpath__examples__search_filter__dict_json/main.py index fb3cacfe5..8ad00cabf 100644 --- a/dpath__examples__search_filter__dict_json/main.py +++ b/dpath__examples__search_filter__dict_json/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install dpath @@ -13,15 +13,15 @@ "3": 2, "43": 30, "c": [], - "d": ['red', 'buggy', 'bumpers'], + "d": ["red", "buggy", "bumpers"], } } } -print(dpath.util.get(x, 'a/b/d')) # ['red', 'buggy', 'bumpers'] -print(dpath.util.get(x, 'a/b/d/0')) # red -print(dpath.util.get(x, 'a/b/d/1')) # buggy -print(dpath.util.get(x, 'a/b/43')) # 30 +print(dpath.util.get(x, "a/b/d")) # ['red', 'buggy', 'bumpers'] +print(dpath.util.get(x, "a/b/d/0")) # red +print(dpath.util.get(x, "a/b/d/1")) # buggy +print(dpath.util.get(x, "a/b/43")) # 30 print() print(dpath.util.values(x, "**/43")) # [30] @@ -37,12 +37,12 @@ def afilter(x): return str(x).isdecimal() -result = dpath.util.search(x, '**', afilter=afilter) +result = dpath.util.search(x, "**", afilter=afilter) print(result) # {'a': {'b': {'3': 2, '43': 30}}} # Фильтрация через лябмды: -result = dpath.util.search(x, '**', afilter=lambda x: str(x).isdecimal()) +result = dpath.util.search(x, "**", afilter=lambda x: str(x).isdecimal()) print(result) # {'a': {'b': {'3': 2, '43': 30}}} -result = list(dpath.util.search(x, '**', yielded=True, afilter=afilter)) +result = list(dpath.util.search(x, "**", yielded=True, afilter=afilter)) print(result) # [('a/b/3', 2), ('a/b/43', 30)] diff --git a/draw fractal/Apollon_Set/Apollon_Set__PIL.py b/draw fractal/Apollon_Set/Apollon_Set__PIL.py index 2eaa0b91a..0359ebadc 100644 --- a/draw fractal/Apollon_Set/Apollon_Set__PIL.py +++ b/draw fractal/Apollon_Set/Apollon_Set__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -63,39 +63,41 @@ # ReadKey # end. -from math import * import random +from math import * +from PIL import Image, ImageDraw -def draw_apollon_set(draw_by_image, step): + +def draw_apollon_set(draw_by_image, step) -> None: x = 0.2 y = 0.3 - + r = sqrt(3) sqr = lambda x: x * x for i in range(step): a = random.random() - a0 = 3*(1+r-x)/(sqr(1+r-x)+sqr(y))-(1+r)/(2+r) - b0 = 3*y/(sqr(1+r-x)+sqr(y)) - if 1/3 >= a >= 0: + a0 = 3 * (1 + r - x) / (sqr(1 + r - x) + sqr(y)) - (1 + r) / (2 + r) + b0 = 3 * y / (sqr(1 + r - x) + sqr(y)) + if 1 / 3 >= a >= 0: x1 = a0 y1 = b0 - a1 = -1/2 - b1 = r/2 - a2 = -1/2 - b2 = -r/2 - f1x = a0/(sqr(a0)+sqr(b0)) - f1y = -b0/(sqr(a0)+sqr(b0)) - if 2/3 >= a > 1/3: - x1 = f1x*a1-f1y*b1 - y1 = f1x*b1+f1y*a1 + a1 = -1 / 2 + b1 = r / 2 + a2 = -1 / 2 + b2 = -r / 2 + f1x = a0 / (sqr(a0) + sqr(b0)) + f1y = -b0 / (sqr(a0) + sqr(b0)) + if 2 / 3 >= a > 1 / 3: + x1 = f1x * a1 - f1y * b1 + y1 = f1x * b1 + f1y * a1 - if 3/3 >= a > 2/3: - x1 = f1x*a2-f1y*b2 - y1 = f1x*b2+f1y*a2 + if 3 / 3 >= a > 2 / 3: + x1 = f1x * a2 - f1y * b2 + y1 = f1x * b2 + f1y * a2 x = x1 y = y1 @@ -103,12 +105,11 @@ def draw_apollon_set(draw_by_image, step): draw_by_image.point((320 + x * 50, 240 + y * 50), "red") -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (650, 500), "white") # Каждый step это одна точка step = 50000 draw_apollon_set(ImageDraw.Draw(img), step) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Cantor_dust/Cantor_dust__PIL.py b/draw fractal/Cantor_dust/Cantor_dust__PIL.py index 939861cf6..3dd63d6aa 100644 --- a/draw fractal/Cantor_dust/Cantor_dust__PIL.py +++ b/draw fractal/Cantor_dust/Cantor_dust__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -37,22 +37,24 @@ # end. -def draw_cantor_dust(draw_by_image): - def draw(x, y, size): +from PIL import Image, ImageDraw + + +def draw_cantor_dust(draw_by_image) -> None: + def draw(x, y, size) -> None: if size > 1: s = size / 3 draw(x, y + 20, s) draw(x + s * 2, y + 20, s) - draw_by_image.rectangle((x, y, x + size, y + 5), 'black') + draw_by_image.rectangle((x, y, x + size, y + 5), "black") draw(10, 30, 500) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (520, 160), "white") draw_cantor_dust(ImageDraw.Draw(img)) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Circular_fractal/Circular_fractal__PIL.py b/draw fractal/Circular_fractal/Circular_fractal__PIL.py index 87e1acbb7..28735840b 100644 --- a/draw fractal/Circular_fractal/Circular_fractal__PIL.py +++ b/draw fractal/Circular_fractal/Circular_fractal__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -36,9 +36,10 @@ from math import * +from PIL import Image, ImageDraw -def draw_circular_fractal(draw_by_image, x, y, size): +def draw_circular_fractal(draw_by_image, x, y, size) -> None: m = 6 n = 3 @@ -51,19 +52,18 @@ def draw_circular_fractal(draw_by_image, x, y, size): draw_by_image, x - s2 * sin(2 * pi / m * i), y + s2 * cos(2 * pi / m * i), - s1 + s1, ) draw_circular_fractal(draw_by_image, x, y, s1) bbox = (x - size, y - size, x + size, y + size) - draw_by_image.ellipse(bbox, outline='black') + draw_by_image.ellipse(bbox, outline="black") -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (320 * 2, 240 * 2), "white") draw_circular_fractal(ImageDraw.Draw(img), 320, 240, 200) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Dragon_curve_1/Dragon_curve_1__PIL.py b/draw fractal/Dragon_curve_1/Dragon_curve_1__PIL.py index c093eb58f..dad5d9eb2 100644 --- a/draw fractal/Dragon_curve_1/Dragon_curve_1__PIL.py +++ b/draw fractal/Dragon_curve_1/Dragon_curve_1__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -37,7 +37,10 @@ # End. -def draw_dragon_curve_1(draw_by_image, x1, y1, x2, y2, k): +from PIL import Image, ImageDraw + + +def draw_dragon_curve_1(draw_by_image, x1, y1, x2, y2, k) -> None: if k > 0: xn = (x1 + x2) // 2 + (y2 - y1) // 2 yn = (y1 + y2) // 2 - (x2 - x1) // 2 @@ -45,15 +48,14 @@ def draw_dragon_curve_1(draw_by_image, x1, y1, x2, y2, k): draw_dragon_curve_1(draw_by_image, x2, y2, xn, yn, k - 1) else: - draw_by_image.line((x1, y1, x2, y2), 'black') + draw_by_image.line((x1, y1, x2, y2), "black") -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (700, 512), "white") # Глубина фрактала z = 14 draw_dragon_curve_1(ImageDraw.Draw(img), 200, 300, 500, 300, z) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Fern/Fern__PIL.py b/draw fractal/Fern/Fern__PIL.py index c6540ea94..5353cc35b 100644 --- a/draw fractal/Fern/Fern__PIL.py +++ b/draw fractal/Fern/Fern__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -46,7 +46,10 @@ # end. -def draw_fern(draw_by_image, width, height): +from PIL import Image, ImageDraw + + +def draw_fern(draw_by_image, width, height) -> None: n = 255 cx = 0.251 @@ -70,10 +73,9 @@ def draw_fern(draw_by_image, width, height): draw_by_image.point((ix, iy), color) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (300, 300), "white") draw_fern(ImageDraw.Draw(img), img.width, img.height) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Fern_2/Fern_2__PIL.py b/draw fractal/Fern_2/Fern_2__PIL.py index e8b7c59a5..9662a6311 100644 --- a/draw fractal/Fern_2/Fern_2__PIL.py +++ b/draw fractal/Fern_2/Fern_2__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -43,13 +43,14 @@ from math import * +from PIL import Image, ImageDraw -def draw_fern_2(draw_by_image): - def line_to(x, y, l, u): - draw_by_image.line((x, y, x + l * cos(u), y - l * sin(u)), 'black') +def draw_fern_2(draw_by_image) -> None: + def line_to(x, y, l, u) -> None: + draw_by_image.line((x, y, x + l * cos(u), y - l * sin(u)), "black") - def draw(x, y, l, u): + def draw(x, y, l, u) -> None: if l > 1: line_to(x, y, l, u) @@ -63,10 +64,9 @@ def draw(x, y, l, u): draw(320, 460, 140, pi / 2) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (730, 500), "white") draw_fern_2(ImageDraw.Draw(img)) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Fingerprint/Fingerprint__PIL.py b/draw fractal/Fingerprint/Fingerprint__PIL.py index 08f1960ac..ad6415f4f 100644 --- a/draw fractal/Fingerprint/Fingerprint__PIL.py +++ b/draw fractal/Fingerprint/Fingerprint__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -12,11 +12,11 @@ # Оригинал: http://www.cyberforum.ru/pascalabc/thread994987.html # uses GraphABC; -# +# # const # n=255; # max=10; -# +# # var # x,y,x1,y1,cx,cy: real; # i,ix,iy: integer; @@ -45,7 +45,10 @@ # end. -def draw_fingerprint(draw_by_image, width, height): +from PIL import Image, ImageDraw + + +def draw_fingerprint(draw_by_image, width, height) -> None: n = 255 max = 10 @@ -68,17 +71,16 @@ def draw_fingerprint(draw_by_image, width, height): y = y1 if i >= n: - color = 'red' + color = "red" else: color = (255, 255 - i, 255 - i) draw_by_image.point((ix, iy), color) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (400, 300), "white") draw_fingerprint(ImageDraw.Draw(img), img.width, img.height) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Fractal_tree/Fractal_tree__PIL.py b/draw fractal/Fractal_tree/Fractal_tree__PIL.py index d90b67bcb..97316863e 100644 --- a/draw fractal/Fractal_tree/Fractal_tree__PIL.py +++ b/draw fractal/Fractal_tree/Fractal_tree__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -58,27 +58,29 @@ # End. -from math import * import random +from math import * + +from PIL import Image, ImageDraw -def draw_fractal_tree(draw_by_image, x, y, a, l): +def draw_fractal_tree(draw_by_image, x, y, a, l) -> None: if l < 8: return - x1 = x + l*cos(a) - y1 = y + l*sin(a) + x1 = x + l * cos(a) + y1 = y + l * sin(a) p = 100 if l > 100 else l if p < 40: # Генерация листьев - color = 'lime' if random.random() > 0.5 else 'green' + color = "lime" if random.random() > 0.5 else "green" for i in range(3): draw_by_image.line((x + i, y, x1, y1), color) else: # Генерация веток - color = 'brown' + color = "brown" for i in range(p // 6): draw_by_image.line((x + i - (p / 12), y, x1, y1), color) @@ -96,10 +98,9 @@ def draw_fractal_tree(draw_by_image, x, y, a, l): draw_fractal_tree(draw_by_image, x1, y1, a1, p - 5 - random.randrange(30)) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (700, 600), "white") draw_fractal_tree(ImageDraw.Draw(img), 350, 580, 3 * pi / 2, 200) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Fractal_tree/Fractal_tree__Qt_gui.py b/draw fractal/Fractal_tree/Fractal_tree__Qt_gui.py index 4810af984..7495fbfde 100644 --- a/draw fractal/Fractal_tree/Fractal_tree__Qt_gui.py +++ b/draw fractal/Fractal_tree/Fractal_tree__Qt_gui.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -9,33 +9,65 @@ """ +import math +import io + +from PIL import Image, ImageDraw + try: - from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QVBoxLayout, QSizePolicy + from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QLabel, + QPushButton, + QVBoxLayout, + QSizePolicy, + ) from PyQt5.QtGui import QImage, QPixmap, QPainter from PyQt5.QtCore import Qt except ImportError: try: - from PyQt4.QtGui import (QImage, QPainter, QApplication, QWidget, - QLabel, QPushButton, QVBoxLayout, QSizePolicy, QPixmap) + from PyQt4.QtGui import ( + QImage, + QPainter, + QApplication, + QWidget, + QLabel, + QPushButton, + QVBoxLayout, + QSizePolicy, + QPixmap, + ) from PyQt4.QtCore import Qt except ImportError: - from PySide.QtGui import (QImage, QPainter, QApplication, QWidget, - QLabel, QPushButton, QVBoxLayout, QSizePolicy, QPixmap) + from PySide.QtGui import ( + QImage, + QPainter, + QApplication, + QWidget, + QLabel, + QPushButton, + QVBoxLayout, + QSizePolicy, + QPixmap, + ) from PySide.QtCore import Qt +from Fractal_tree__PIL import draw_fractal_tree + class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('Fractal tree') + self.setWindowTitle("Fractal tree") self.img_label = QLabel() self.img_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.generate_tree_button = QPushButton('Generate Tree') + self.generate_tree_button = QPushButton("Generate Tree") self.generate_tree_button.clicked.connect(self.generate_tree) main_layout = QVBoxLayout() @@ -44,18 +76,13 @@ def __init__(self): self.setLayout(main_layout) - def generate_tree(self): - import math - - from PIL import Image, ImageDraw + def generate_tree(self) -> None: img = Image.new("RGB", (700, 600), "white") - from Fractal_tree__PIL import draw_fractal_tree draw_fractal_tree(ImageDraw.Draw(img), 350, 580, 3 * math.pi / 2, 200) - import io img_bytes_io = io.BytesIO() - img.save(img_bytes_io, format='PNG') + img.save(img_bytes_io, format="PNG") img_bytes = img_bytes_io.getvalue() img = QPixmap() @@ -64,7 +91,7 @@ def generate_tree(self): self.img_label.setPixmap(img) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() diff --git a/draw fractal/Gosper_curve/Gosper_curve__PIL.py b/draw fractal/Gosper_curve/Gosper_curve__PIL.py index caf52bf31..dd23d5b81 100644 --- a/draw fractal/Gosper_curve/Gosper_curve__PIL.py +++ b/draw fractal/Gosper_curve/Gosper_curve__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -58,29 +58,32 @@ import math +from PIL import Image, ImageDraw -def draw_gosper_curve(draw_by_image, step): - def draw(x, y, l, u, t, q): +def draw_gosper_curve(draw_by_image, step) -> None: + def draw(x, y, l, u, t, q) -> None: if t > 0: if q == 1: x += l * math.cos(u) y -= l * math.sin(u) u += math.pi - - u -= 2*math.pi/19 + + u -= 2 * math.pi / 19 l /= math.sqrt(7) - x, y = draw2(x, y, l, u, t-1, 0) - x, y = draw2(x, y, l, u+math.pi/3, t-1, 1) - x, y = draw2(x, y, l, u+math.pi, t-1, 1) - x, y = draw2(x, y, l, u+2*math.pi/3, t-1, 0) - x, y = draw2(x, y, l, u, t-1, 0) - x, y = draw2(x, y, l, u, t-1, 0) - _, _ = draw2(x, y, l, u-math.pi/3, t-1, 1) + x, y = draw2(x, y, l, u, t - 1, 0) + x, y = draw2(x, y, l, u + math.pi / 3, t - 1, 1) + x, y = draw2(x, y, l, u + math.pi, t - 1, 1) + x, y = draw2(x, y, l, u + 2 * math.pi / 3, t - 1, 0) + x, y = draw2(x, y, l, u, t - 1, 0) + x, y = draw2(x, y, l, u, t - 1, 0) + _, _ = draw2(x, y, l, u - math.pi / 3, t - 1, 1) else: - draw_by_image.line((x, y, x + math.cos(u) * l, y - math.sin(u) * l), fill="black") + draw_by_image.line( + (x, y, x + math.cos(u) * l, y - math.sin(u) * l), fill="black" + ) def draw2(x, y, l, u, t, q): draw(x, y, l, u, t, q) @@ -89,12 +92,11 @@ def draw2(x, y, l, u, t, q): draw(100, 355, 400, 0, step, 0) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (650, 500), "white") draw_by_image = ImageDraw.Draw(img) step = 4 draw_gosper_curve(draw_by_image, step) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Ice_fractal_1/Circular_fractal_1__PIL.py b/draw fractal/Ice_fractal_1/Circular_fractal_1__PIL.py index 0df00cc7b..a4628ee1c 100644 --- a/draw fractal/Ice_fractal_1/Circular_fractal_1__PIL.py +++ b/draw fractal/Ice_fractal_1/Circular_fractal_1__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -46,35 +46,35 @@ from math import * +from PIL import Image, ImageDraw -def draw_ice_fractal_1(draw_by_image, step): +def draw_ice_fractal_1(draw_by_image, step) -> None: def draw2(x, y, l, u, t): draw(x, y, l, u, t) return x + l * cos(u), y - l * sin(u) - def draw(x, y, l, u, t): + def draw(x, y, l, u, t) -> None: if t > 0: l *= 0.5 - x, y = draw2(x, y, l, u, t-1) - x, y = draw2(x, y, l*0.8, u+pi/2, t-1) - x, y = draw2(x, y, l*0.8, u-pi/2, t-1) - _, _ = draw2(x, y, l, u, t-1) + x, y = draw2(x, y, l, u, t - 1) + x, y = draw2(x, y, l * 0.8, u + pi / 2, t - 1) + x, y = draw2(x, y, l * 0.8, u - pi / 2, t - 1) + _, _ = draw2(x, y, l, u, t - 1) else: - draw_by_image.line((x, y, x+cos(u)*l, y-sin(u)*l), 'black') + draw_by_image.line((x, y, x + cos(u) * l, y - sin(u) * l), "black") draw(410, 10, 400, -pi, step) draw(10, 410, 400, 0, step) - draw(10, 10, 400, -pi/2, step) - draw(410, 410, 400, pi/2, step) + draw(10, 10, 400, -pi / 2, step) + draw(410, 410, 400, pi / 2, step) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (420, 420), "white") step = 5 draw_ice_fractal_1(ImageDraw.Draw(img), step) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Ice_fractal_2/Circular_fractal_2__PIL.py b/draw fractal/Ice_fractal_2/Circular_fractal_2__PIL.py index 7aa5cbc27..38e5d0d56 100644 --- a/draw fractal/Ice_fractal_2/Circular_fractal_2__PIL.py +++ b/draw fractal/Ice_fractal_2/Circular_fractal_2__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -48,36 +48,36 @@ from math import * +from PIL import Image, ImageDraw -def draw_ice_fractal_2(draw_by_image, step): +def draw_ice_fractal_2(draw_by_image, step) -> None: def draw2(x, y, l, u, t): draw(x, y, l, u, t) return x + l * cos(u), y - l * sin(u) - def draw(x, y, l, u, t): + def draw(x, y, l, u, t) -> None: if t > 0: l *= 0.5 - x, y = draw2(x, y, l, u, t-1) - x, y = draw2(x, y, l*0.45, u+2*pi/3, t-1) - x, y = draw2(x, y, l*0.45, u-pi/3, t-1) - x, y = draw2(x, y, l*0.45, u+pi/3, t-1) - x, y = draw2(x, y, l*0.45, u-2*pi/3, t-1) - _, _ = draw2(x, y, l, u, t-1) + x, y = draw2(x, y, l, u, t - 1) + x, y = draw2(x, y, l * 0.45, u + 2 * pi / 3, t - 1) + x, y = draw2(x, y, l * 0.45, u - pi / 3, t - 1) + x, y = draw2(x, y, l * 0.45, u + pi / 3, t - 1) + x, y = draw2(x, y, l * 0.45, u - 2 * pi / 3, t - 1) + _, _ = draw2(x, y, l, u, t - 1) else: - draw_by_image.line((x, y, x+cos(u)*l, y-sin(u)*l), 'black') + draw_by_image.line((x, y, x + cos(u) * l, y - sin(u) * l), "black") - draw(210, 8, 400, -2*pi/3, step) + draw(210, 8, 400, -2 * pi / 3, step) draw(10, 354, 400, 0, step) - draw(410, 354, 400, 2*pi/3, step) + draw(410, 354, 400, 2 * pi / 3, step) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (420, 420), "white") step = 6 draw_ice_fractal_2(ImageDraw.Draw(img), step) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Koch_curve/Koch_curve__PIL.py b/draw fractal/Koch_curve/Koch_curve__PIL.py index 267672660..db728139f 100644 --- a/draw fractal/Koch_curve/Koch_curve__PIL.py +++ b/draw fractal/Koch_curve/Koch_curve__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -57,7 +57,10 @@ # ?> -def draw_koch(draw, xa, ya, xe, ye, n): +from PIL import Image, ImageDraw + + +def draw_koch(draw, xa, ya, xe, ye, n) -> None: """ Draws koch curve between two points. @@ -70,11 +73,11 @@ def draw_koch(draw, xa, ya, xe, ye, n): # / \ # A---B D---E - xb = xa + (xe - xa) * 1/3 - yb = ya + (ye - ya) * 1/3 + xb = xa + (xe - xa) * 1 / 3 + yb = ya + (ye - ya) * 1 / 3 - xd = xa + (xe - xa) * 2/3 - yd = ya + (ye - ya) * 2/3 + xd = xa + (xe - xa) * 2 / 3 + yd = ya + (ye - ya) * 2 / 3 cos60 = 0.5 sin60 = -0.866 @@ -87,11 +90,10 @@ def draw_koch(draw, xa, ya, xe, ye, n): draw_koch(draw, xd, yd, xe, ye, n - 1) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (600, 200), "white") step = 4 draw_koch(ImageDraw.Draw(img), 0, img.height - 1, img.width, img.height - 1, step) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Koch_curve/Koch_curve__Qt.py b/draw fractal/Koch_curve/Koch_curve__Qt.py index 0eafa962f..90aa52c09 100644 --- a/draw fractal/Koch_curve/Koch_curve__Qt.py +++ b/draw fractal/Koch_curve/Koch_curve__Qt.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -57,7 +57,7 @@ # ?> -def draw_koch(painter, xa, ya, xe, ye, n): +def draw_koch(painter, xa, ya, xe, ye, n) -> None: """ Draws koch curve between two points. @@ -70,11 +70,11 @@ def draw_koch(painter, xa, ya, xe, ye, n): # / \ # A---B D---E - xb = xa + (xe - xa) * 1/3 - yb = ya + (ye - ya) * 1/3 + xb = xa + (xe - xa) * 1 / 3 + yb = ya + (ye - ya) * 1 / 3 - xd = xa + (xe - xa) * 2/3 - yd = ya + (ye - ya) * 2/3 + xd = xa + (xe - xa) * 2 / 3 + yd = ya + (ye - ya) * 2 / 3 cos60 = 0.5 sin60 = -0.866 @@ -101,7 +101,7 @@ def draw_koch(painter, xa, ya, xe, ye, n): from PySide.QtCore import Qt -if __name__ == '__main__': +if __name__ == "__main__": img = QImage(600, 200, QImage.Format_RGB16) img.fill(Qt.white) @@ -111,4 +111,4 @@ def draw_koch(painter, xa, ya, xe, ye, n): painter.setPen(Qt.black) draw_koch(painter, 0, img.height() - 1, img.width(), img.height() - 1, step) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Koch_curve/Koch_curve__Qt_gui.py b/draw fractal/Koch_curve/Koch_curve__Qt_gui.py index 17b98c7cc..0996c3e50 100644 --- a/draw fractal/Koch_curve/Koch_curve__Qt_gui.py +++ b/draw fractal/Koch_curve/Koch_curve__Qt_gui.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -57,7 +57,7 @@ # ?> -def draw_koch(painter, xa, ya, xe, ye, n): +def draw_koch(painter, xa, ya, xe, ye, n) -> None: """ Draws koch curve between two points. @@ -70,11 +70,11 @@ def draw_koch(painter, xa, ya, xe, ye, n): # / \ # A---B D---E - xb = xa + (xe - xa) * 1/3 - yb = ya + (ye - ya) * 1/3 + xb = xa + (xe - xa) * 1 / 3 + yb = ya + (ye - ya) * 1 / 3 - xd = xa + (xe - xa) * 2/3 - yd = ya + (ye - ya) * 2/3 + xd = xa + (xe - xa) * 2 / 3 + yd = ya + (ye - ya) * 2 / 3 cos60 = 0.5 sin60 = -0.866 @@ -86,28 +86,54 @@ def draw_koch(painter, xa, ya, xe, ye, n): draw_koch(painter, xc, yc, xd, yd, n - 1) draw_koch(painter, xd, yd, xe, ye, n - 1) + try: - from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QSpinBox, QVBoxLayout, QSizePolicy + from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QLabel, + QSpinBox, + QVBoxLayout, + QSizePolicy, + ) from PyQt5.QtGui import QImage, QPixmap, QPainter from PyQt5.QtCore import Qt except ImportError: try: - from PyQt4.QtGui import (QImage, QPainter, QApplication, QWidget, - QLabel, QSpinBox, QVBoxLayout, QSizePolicy, QPixmap) + from PyQt4.QtGui import ( + QImage, + QPainter, + QApplication, + QWidget, + QLabel, + QSpinBox, + QVBoxLayout, + QSizePolicy, + QPixmap, + ) from PyQt4.QtCore import Qt except ImportError: - from PySide.QtGui import (QImage, QPainter, QApplication, QWidget, - QLabel, QSpinBox, QVBoxLayout, QSizePolicy, QPixmap) + from PySide.QtGui import ( + QImage, + QPainter, + QApplication, + QWidget, + QLabel, + QSpinBox, + QVBoxLayout, + QSizePolicy, + QPixmap, + ) from PySide.QtCore import Qt class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('Koch_curve snowflake') + self.setWindowTitle("Koch_curve snowflake") self.img_label = QLabel() self.img_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) @@ -118,7 +144,7 @@ def __init__(self): self.step_spinbox.valueChanged.connect(self.draw_by_step) commands_layout = QVBoxLayout() - commands_layout.addWidget(QLabel('Step:')) + commands_layout.addWidget(QLabel("Step:")) commands_layout.addWidget(self.step_spinbox) main_layout = QVBoxLayout() @@ -129,7 +155,7 @@ def __init__(self): self.draw_by_step(self.step_spinbox.value()) - def draw_by_step(self, step): + def draw_by_step(self, step) -> None: img = QImage(600, 200, QImage.Format_RGB16) img.fill(Qt.white) @@ -143,7 +169,7 @@ def draw_by_step(self, step): painter.end() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() diff --git a/draw fractal/Koch_snowflake/Koch_snowflake__PIL.py b/draw fractal/Koch_snowflake/Koch_snowflake__PIL.py index dc3153225..8a97d9a14 100644 --- a/draw fractal/Koch_snowflake/Koch_snowflake__PIL.py +++ b/draw fractal/Koch_snowflake/Koch_snowflake__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -47,17 +47,20 @@ import math +from PIL import Image, ImageDraw -def draw_snowflake_koch(draw_by_image, step): +def draw_snowflake_koch(draw_by_image, step) -> None: """ Draws koch snowflake. """ - def draw(x, y, l, u, t): + def draw(x, y, l, u, t) -> None: if t == 0: - draw_by_image.line((x, y, x + math.cos(u) * l, y - math.sin(u) * l), fill="black") + draw_by_image.line( + (x, y, x + math.cos(u) * l, y - math.sin(u) * l), fill="black" + ) else: l /= 3 x, y = draw2(x, y, l, u, t - 1) @@ -74,12 +77,11 @@ def draw2(x, y, l, u, t): draw(210, 8, 400, -math.pi / 3, step) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (425, 500), "white") draw_by_image = ImageDraw.Draw(img) step = 4 draw_snowflake_koch(draw_by_image, step) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Levy_curve/Levy_curve__PIL.py b/draw fractal/Levy_curve/Levy_curve__PIL.py index 5484a06d5..50cb3752d 100644 --- a/draw fractal/Levy_curve/Levy_curve__PIL.py +++ b/draw fractal/Levy_curve/Levy_curve__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -53,9 +53,10 @@ import random +from PIL import Image, ImageDraw -def draw_levy(draw): +def draw_levy(draw) -> None: iter = 50000 mx = 200 @@ -68,22 +69,21 @@ def draw_levy(draw): for k in range(iter): p = random.random() t = x - if p <= 1/2: - x = 0.5*x - 0.5*y - y = 0.5*t + 0.5*y + if p <= 1 / 2: + x = 0.5 * x - 0.5 * y + y = 0.5 * t + 0.5 * y else: - x = 0.5*x + 0.5*y + 0.5; - y = -0.5*t + 0.5*y + 0.5; + x = 0.5 * x + 0.5 * y + 0.5 + y = -0.5 * t + 0.5 * y + 0.5 draw.point((mx + rad * x, my - rad * y), "blue") -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (650, 450), "white") step = 10 draw_levy(ImageDraw.Draw(img)) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Mandelbrot_Set_1/Mandelbrot_Set_1__PIL.py b/draw fractal/Mandelbrot_Set_1/Mandelbrot_Set_1__PIL.py index 09da2d747..5c6a3ed5b 100644 --- a/draw fractal/Mandelbrot_Set_1/Mandelbrot_Set_1__PIL.py +++ b/draw fractal/Mandelbrot_Set_1/Mandelbrot_Set_1__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -45,24 +45,27 @@ # end. -def draw_mandelbrot_set_1(draw_by_image, width, height): +from PIL import Image, ImageDraw + + +def draw_mandelbrot_set_1(draw_by_image, width, height) -> None: n = 255 max = 10 - + for ix in range(width): for iy in range(height): x = 0 y = 0 cx = 0.002 * (ix - 720) cy = 0.002 * (iy - 150) - + for i in range(n): x1 = x * x - y * y + cx y1 = 2 * x * y + cy - + if x1 > max or y1 > max: break - + x = x1 y = y1 @@ -74,10 +77,9 @@ def draw_mandelbrot_set_1(draw_by_image, width, height): draw_by_image.point((ix, iy), color) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (400, 300), "white") draw_mandelbrot_set_1(ImageDraw.Draw(img), img.width, img.height) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Mandelbrot_Set_2/Mandelbrot_Set_2__PIL.py b/draw fractal/Mandelbrot_Set_2/Mandelbrot_Set_2__PIL.py index 285d37654..d03467eb2 100644 --- a/draw fractal/Mandelbrot_Set_2/Mandelbrot_Set_2__PIL.py +++ b/draw fractal/Mandelbrot_Set_2/Mandelbrot_Set_2__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -45,24 +45,27 @@ # end. -def draw_mandelbrot_set_2(draw_by_image, width, height): +from PIL import Image, ImageDraw + + +def draw_mandelbrot_set_2(draw_by_image, width, height) -> None: n = 255 max = 10 - + for ix in range(width): for iy in range(height): x = 0 y = 0 cx = 0.005 * (ix - 365) cy = 0.005 * (iy - 300) - + for i in range(n): x1 = x * x - y * y + cx y1 = 2 * x * y + cy - + if x1 > max or y1 > max: break - + x = x1 y = y1 @@ -74,10 +77,9 @@ def draw_mandelbrot_set_2(draw_by_image, width, height): draw_by_image.point((ix, iy), color) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (600, 600), "white") draw_mandelbrot_set_2(ImageDraw.Draw(img), img.width, img.height) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Minkowski_curve/Minkowski_curve__PIL.py b/draw fractal/Minkowski_curve/Minkowski_curve__PIL.py index fd7e89990..f8f631a4e 100644 --- a/draw fractal/Minkowski_curve/Minkowski_curve__PIL.py +++ b/draw fractal/Minkowski_curve/Minkowski_curve__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -76,7 +76,10 @@ # ?> -def draw_minkowski(draw, xa, ya, xi, yi, n): +from PIL import Image, ImageDraw + + +def draw_minkowski(draw, xa, ya, xi, yi, n) -> None: """ Draws minkowski curve between two points. @@ -91,14 +94,14 @@ def draw_minkowski(draw, xa, ya, xi, yi, n): # | | # F---G - xb = xa + (xi - xa) * 1/4 - yb = ya + (yi - ya) * 1/4 + xb = xa + (xi - xa) * 1 / 4 + yb = ya + (yi - ya) * 1 / 4 - xe = xa + (xi - xa) * 2/4 - ye = ya + (yi - ya) * 2/4 + xe = xa + (xi - xa) * 2 / 4 + ye = ya + (yi - ya) * 2 / 4 - xh = xa + (xi - xa) * 3/4 - yh = ya + (yi - ya) * 3/4 + xh = xa + (xi - xa) * 3 / 4 + yh = ya + (yi - ya) * 3 / 4 cos90 = 0 sin90 = -1 @@ -125,11 +128,12 @@ def draw_minkowski(draw, xa, ya, xi, yi, n): draw_minkowski(draw, xh, yh, xi, yi, n - 1) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (600, 400), "white") step = 3 - draw_minkowski(ImageDraw.Draw(img), 0, img.height / 2, img.width, img.height / 2, step) + draw_minkowski( + ImageDraw.Draw(img), 0, img.height / 2, img.width, img.height / 2, step + ) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Monkey_tree/Monkey_tree__PIL.py b/draw fractal/Monkey_tree/Monkey_tree__PIL.py index 3e9ce4b9f..e1d71f847 100644 --- a/draw fractal/Monkey_tree/Monkey_tree__PIL.py +++ b/draw fractal/Monkey_tree/Monkey_tree__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -74,56 +74,56 @@ from math import * +from PIL import Image, ImageDraw -def draw_monkey_tree(draw_by_image): - def Draw2(x, y, l, u, t, q, s): - Draw(x, y, l, u, t, q, s) - return x + l*cos(u), y - l*sin(u) - - def Draw(x, y, l, u, t, q,s): +def draw_monkey_tree(draw_by_image) -> None: + def draw2(x, y, l, u, t, q, s): + draw(x, y, l, u, t, q, s) + return x + l * cos(u), y - l * sin(u) + + def draw(x, y, l, u, t, q, s) -> None: if t > 0: if q == 1: - x += l*cos(u) - y -= l*sin(u) + x += l * cos(u) + y -= l * sin(u) s = -s u = u + pi elif q == 3: - x += l*cos(u) - y -= l*sin(u) + x += l * cos(u) + y -= l * sin(u) s = s u = u + pi elif q == 2: s = -s - + elif q == 0: s = s l /= 3 - x, y = Draw2(x, y, l, u+s*pi/3, t-1, 2, s) - x, y = Draw2(x, y, l, u+s*pi/3, t-1, 1, s) - x, y = Draw2(x, y, l, u, t-1, 0, s) - x, y = Draw2(x, y, l, u-s*pi/3, t-1, 1, s) - x, y = Draw2(x, y, l*sqrt(3)/3, u-s*7*pi/6, t-1, 1, s) - x, y = Draw2(x, y, l*sqrt(3)/3, u-s*7*pi/6, t-1, 2, s) - x, y = Draw2(x, y, l*sqrt(3)/3, u-s*5*pi/6, t-1, 3, s) - x, y = Draw2(x, y, l*sqrt(3)/3, u-s*pi/2, t-1, 3, s) - x, y = Draw2(x, y, l*sqrt(3)/3, u-s*pi/2, t-1, 0, s) - x, y = Draw2(x, y, l, u, t-1, 3, s) - _, _ = Draw2(x, y, l, u, t-1, 0, s) + x, y = draw2(x, y, l, u + s * pi / 3, t - 1, 2, s) + x, y = draw2(x, y, l, u + s * pi / 3, t - 1, 1, s) + x, y = draw2(x, y, l, u, t - 1, 0, s) + x, y = draw2(x, y, l, u - s * pi / 3, t - 1, 1, s) + x, y = draw2(x, y, l * sqrt(3) / 3, u - s * 7 * pi / 6, t - 1, 1, s) + x, y = draw2(x, y, l * sqrt(3) / 3, u - s * 7 * pi / 6, t - 1, 2, s) + x, y = draw2(x, y, l * sqrt(3) / 3, u - s * 5 * pi / 6, t - 1, 3, s) + x, y = draw2(x, y, l * sqrt(3) / 3, u - s * pi / 2, t - 1, 3, s) + x, y = draw2(x, y, l * sqrt(3) / 3, u - s * pi / 2, t - 1, 0, s) + x, y = draw2(x, y, l, u, t - 1, 3, s) + _, _ = draw2(x, y, l, u, t - 1, 0, s) else: - draw_by_image.line((x, y, x + cos(u)*l, y - sin(u)*l), 'black') + draw_by_image.line((x, y, x + cos(u) * l, y - sin(u) * l), "black") - Draw(50, 365, 430, 0, 3, 0, 1) + draw(50, 365, 430, 0, 3, 0, 1) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (520, 500), "white") draw_monkey_tree(ImageDraw.Draw(img)) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Pythagoras_tree_2/Pythagoras_tree_2__PIL.py b/draw fractal/Pythagoras_tree_2/Pythagoras_tree_2__PIL.py index 547753bb9..05c44e2a6 100644 --- a/draw fractal/Pythagoras_tree_2/Pythagoras_tree_2__PIL.py +++ b/draw fractal/Pythagoras_tree_2/Pythagoras_tree_2__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -42,13 +42,14 @@ from math import * +from PIL import Image, ImageDraw -def draw_pythagoras_tree_2(draw_by_image): - def line_to(x, y, l, u): - draw_by_image.line((x, y, x + l * cos(u), y - l * sin(u)), 'black') +def draw_pythagoras_tree_2(draw_by_image) -> None: + def line_to(x, y, l, u) -> None: + draw_by_image.line((x, y, x + l * cos(u), y - l * sin(u)), "black") - def draw(x, y, l, u): + def draw(x, y, l, u) -> None: if l > 3: l *= 0.7 line_to(x, y, l, u) @@ -62,10 +63,9 @@ def draw(x, y, l, u): draw(320, 460, 200, pi / 2) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (730, 500), "white") draw_pythagoras_tree_2(ImageDraw.Draw(img)) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Sierpinski_carpet/Sierpinski_carpet__PIL.py b/draw fractal/Sierpinski_carpet/Sierpinski_carpet__PIL.py index d58b68422..4ecafccf6 100644 --- a/draw fractal/Sierpinski_carpet/Sierpinski_carpet__PIL.py +++ b/draw fractal/Sierpinski_carpet/Sierpinski_carpet__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -47,31 +47,33 @@ # End. -def draw_sierpinski_carpet(draw_by_image, Z): - def serp(x1, y1, x2, y2, n): +from PIL import Image, ImageDraw + + +def draw_sierpinski_carpet(draw_by_image, Z) -> None: + def serp(x1, y1, x2, y2, n) -> None: if n > 0: - x1n = 2*x1/3 + x2 / 3 - x2n = x1/3 + 2*x2 / 3 - y1n = 2*y1/3 + y2 / 3 - y2n = y1/3+2*y2 / 3 + x1n = 2 * x1 / 3 + x2 / 3 + x2n = x1 / 3 + 2 * x2 / 3 + y1n = 2 * y1 / 3 + y2 / 3 + y2n = y1 / 3 + 2 * y2 / 3 draw_by_image.rectangle((x1n, y1n, x2n, y2n), fill="white", outline="black") - serp(x1, y1, x1n, y1n, n-1) - serp(x1n, y1, x2n, y1n, n-1) - serp(x2n, y1, x2, y1n, n-1) - serp(x1, y1n, x1n, y2n, n-1) - serp(x2n, y1n, x2, y2n, n-1) - serp(x1, y2n, x1n, y2, n-1) - serp(x1n, y2n, x2n, y2, n-1) - serp(x2n, y2n, x2, y2, n-1) + serp(x1, y1, x1n, y1n, n - 1) + serp(x1n, y1, x2n, y1n, n - 1) + serp(x2n, y1, x2, y1n, n - 1) + serp(x1, y1n, x1n, y2n, n - 1) + serp(x2n, y1n, x2, y2n, n - 1) + serp(x1, y2n, x1n, y2, n - 1) + serp(x1n, y2n, x2n, y2, n - 1) + serp(x2n, y2n, x2, y2, n - 1) draw_by_image.rectangle((20, 20, 460, 460), fill="white", outline="black") serp(20, 20, 460, 460, Z) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (500, 500), "white") # Глубина фрактала @@ -79,4 +81,4 @@ def serp(x1, y1, x2, y2, n): draw_sierpinski_carpet(ImageDraw.Draw(img), Z) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Sierpinski_triangle/Sierpinski_triangle__PIL.py b/draw fractal/Sierpinski_triangle/Sierpinski_triangle__PIL.py index 2034e9e08..7037a8d6e 100644 --- a/draw fractal/Sierpinski_triangle/Sierpinski_triangle__PIL.py +++ b/draw fractal/Sierpinski_triangle/Sierpinski_triangle__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -49,15 +49,18 @@ # End. -def draw_sierpinski_triangle(draw_by_image, Z): +from PIL import Image, ImageDraw + + +def draw_sierpinski_triangle(draw_by_image, Z) -> None: color = "black" - def tr(x1, y1, x2, y2, x3, y3): + def tr(x1, y1, x2, y2, x3, y3) -> None: draw_by_image.line((x1, y1, x2, y2), color) draw_by_image.line((x2, y2, x3, y3), color) draw_by_image.line((x3, y3, x1, y1), color) - def draw(x1, y1, x2, y2, x3, y3, n): + def draw(x1, y1, x2, y2, x3, y3, n) -> None: if n > 0: x1n = (x1 + x2) / 2 y1n = (y1 + y2) / 2 @@ -75,8 +78,7 @@ def draw(x1, y1, x2, y2, x3, y3, n): draw(320, 10, 600, 470, 40, 470, Z) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (650, 500), "white") # Глубина фрактала @@ -84,4 +86,4 @@ def draw(x1, y1, x2, y2, x3, y3, n): draw_sierpinski_triangle(ImageDraw.Draw(img), Z) - img.save('img.png') + img.save("img.png") diff --git a/draw fractal/Snowflake/Snowflake__PIL.py b/draw fractal/Snowflake/Snowflake__PIL.py index 1ba2a3985..28ca5b02c 100644 --- a/draw fractal/Snowflake/Snowflake__PIL.py +++ b/draw fractal/Snowflake/Snowflake__PIL.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -36,31 +36,31 @@ from math import * +from PIL import Image, ImageDraw -def draw_snowflake(draw_by_image, width, height, count): - def draw(x0, y0, r, n): +def draw_snowflake(draw_by_image, width, height, count) -> None: + def draw(x0, y0, r, n) -> None: t = 2 * pi / count for i in range(count): x = x0 + r * cos(i * t) y = y0 - r * sin(i * t) - draw_by_image.line((x0, y0, x, y), 'black') + draw_by_image.line((x0, y0, x, y), "black") if n > 1: - draw(x, y, r // 5, n-1) + draw(x, y, r // 5, n - 1) x = width // 2 y = height // 2 draw(x, y, 180, 4) -if __name__ == '__main__': - from PIL import Image, ImageDraw +if __name__ == "__main__": img = Image.new("RGB", (500, 500), "white") # Количество повторений count = 8 draw_snowflake(ImageDraw.Draw(img), img.width, img.height, count) - img.save('img.png') + img.save("img.png") diff --git a/draw_handman.py b/draw_handman.py index 943865c0d..d218b19d6 100644 --- a/draw_handman.py +++ b/draw_handman.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def draw_handman(number_wrong_answer, max_wrong_answer=5): @@ -20,18 +20,18 @@ def draw_handman(number_wrong_answer, max_wrong_answer=5): return handman_text part = int(max_wrong_answer / number_wrong_answer) - return '\n'.join(handman_text.split('\n')[:max_wrong_answer - part + 1]) + return "\n".join(handman_text.split("\n")[: max_wrong_answer - part + 1]) -if __name__ == '__main__': +if __name__ == "__main__": print(draw_handman(0)) - print('-' * 15) + print("-" * 15) print(draw_handman(1)) - print('-' * 15) + print("-" * 15) print(draw_handman(2)) - print('-' * 15) + print("-" * 15) print(draw_handman(3)) - print('-' * 15) + print("-" * 15) print(draw_handman(4)) - print('-' * 15) + print("-" * 15) print(draw_handman(5)) diff --git a/draw_wave.py b/draw_wave.py index cef4b86ea..a84976c18 100644 --- a/draw_wave.py +++ b/draw_wave.py @@ -2,26 +2,21 @@ # -*- coding: utf-8 -*- -__author__ = 'igrishaev' +__author__ = "igrishaev" """Рисование волны wav-файла. Взято из http://habrahabr.ru/post/113239/""" -import wave - import math +import wave import numpy as np - import matplotlib.pyplot as plt import matplotlib.ticker as ticker -types = { - 1: np.int8, - 2: np.int16, - 4: np.int32 -} + +types = {1: np.int8, 2: np.int16, 4: np.int32} duration = nframes = k = peak = None @@ -49,7 +44,7 @@ def format_db(x, pos=None): return int(db) -def draw(file_name): +def draw(file_name) -> None: wav = wave.open(file_name, mode="r") global nframes, k, peak, duration (nchannels, sampwidth, framerate, nframes, comptype, compname) = wav.getparams() @@ -58,7 +53,7 @@ def draw(file_name): w, h = 800, 300 k = nframes / w / 32 DPI = 72 - peak = 256 ** sampwidth / 2 + peak = 256**sampwidth / 2 content = wav.readframes(nframes) samples = np.fromstring(content, dtype=types[sampwidth]) diff --git a/dynamic_methods_link_call.py b/dynamic_methods_link_call.py index af2c0562b..7755ae318 100644 --- a/dynamic_methods_link_call.py +++ b/dynamic_methods_link_call.py @@ -1,33 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" class CallBuilder: - def __init__(self, part=None, sep=''): + def __init__(self, part=None, sep="") -> None: self._part = part self._sep = sep def __getattr__(self, part): return CallBuilder( - part=((self._part + self._sep) if self._part else '') + part, - sep=self._sep + part=((self._part + self._sep) if self._part else "") + part, + sep=self._sep, ) def __call__(self, **kwargs): return self._part -if __name__ == '__main__': +if __name__ == "__main__": builder = CallBuilder() result = builder.H.e.l.l.o._.World() print(result) # Hello_World - builder = CallBuilder(sep='.') + builder = CallBuilder(sep=".") result = builder.H.e.l.l.o._.World() print(result) # H.e.l.l.o._.World - builder = CallBuilder(sep='.') + builder = CallBuilder(sep=".") result = builder.H.e.l.l.o._.W.o.r.l.d() print(result) # H.e.l.l.o._.W.o.r.l.d diff --git a/earth_console_animations.py b/earth_console_animations.py index b57d38460..02a11123c 100644 --- a/earth_console_animations.py +++ b/earth_console_animations.py @@ -1,15 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/Dark-Princ3/X-tra-Telegram/blob/aa628465dddaf06383c84914df70c89a4d4fbf15/userbot/plugins/Earth.py#L13 + import time + items = "🌏🌍🌎🌎🌍🌏🌍🌎" for i in range(48): - print('\r' + ' ' * 100 + '\r', end='') - print(items[i % len(items)], end='') + print("\r" + " " * 100 + "\r", end="") + print(items[i % len(items)], end="") time.sleep(0.1) diff --git a/effect_of_vanishing_photos/effect_of_vanishing_photos.py b/effect_of_vanishing_photos/effect_of_vanishing_photos.py index f769b1725..37ec67af4 100644 --- a/effect_of_vanishing_photos/effect_of_vanishing_photos.py +++ b/effect_of_vanishing_photos/effect_of_vanishing_photos.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -"""Эффект исчезновения фотографии +""" +Эффект исчезновения фотографии Кликая на области на фотографии запускаются процессы плавного увеличения прозрачности пикселей, эффект как круги воды, будут расходиться пока не -закончатся непрозрачные пиксели""" +закончатся непрозрачные пиксели +""" import sys @@ -29,12 +31,12 @@ from PySide.QtCore import * -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) @@ -43,14 +45,14 @@ def log_uncaught_exceptions(ex_cls, ex, tb): class Timer(QTimer): class Circle: - def __init__(self, pos_center): + def __init__(self, pos_center) -> None: self.pos_center = pos_center self.radii = 1 - def next(self): + def next(self) -> None: self.radii += 1 - def __init__(self, widget, image): + def __init__(self, widget, image) -> None: super().__init__() self.circle_list = list() @@ -66,10 +68,10 @@ def __init__(self, widget, image): self.painter.setPen(Qt.NoPen) self.painter.setBrush(Qt.transparent) - def add(self, pos_center): + def add(self, pos_center) -> None: self.circle_list.append(Timer.Circle(pos_center)) - def tick(self): + def tick(self) -> None: for circle in self.circle_list: self.painter.drawEllipse(circle.pos_center, circle.radii, circle.radii) circle.next() @@ -78,23 +80,23 @@ def tick(self): class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('effect_of_vanishing_photos.py') + self.setWindowTitle("effect_of_vanishing_photos.py") - self.im = QImage('im.png') + self.im = QImage("im.png") self.resize(self.im.size()) self.timer = Timer(self, self.im) self.timer.start() - def mouseReleaseEvent(self, event): + def mouseReleaseEvent(self, event) -> None: super().mouseReleaseEvent(event) self.timer.add(event.pos()) - def paintEvent(self, event): + def paintEvent(self, event) -> None: super().paintEvent(event) p = QPainter(self) @@ -102,11 +104,13 @@ def paintEvent(self, event): p.drawRect(self.rect()) p.setBrush(Qt.yellow) - p.drawRect(self.width() // 6, self.width() // 5, self.width() // 3, self.height() // 4) + p.drawRect( + self.width() // 6, self.width() // 5, self.width() // 3, self.height() // 4 + ) p.drawImage(0, 0, self.im) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) w = Widget() diff --git a/email/attach.py b/email/attach.py index 0a61ead27..15bec41de 100644 --- a/email/attach.py +++ b/email/attach.py @@ -3,7 +3,7 @@ from email.mime.image import MIMEImage import smtplib -__author__ = 'ipetrash' +__author__ = "ipetrash" """Пример отсылки письма, содержащего прикрепленные файлы, "самому себе".""" @@ -11,15 +11,15 @@ # http://www.tutorialspoint.com/python/python_sending_email.htm # https://docs.python.org/3.4/library/email-examples.html -if __name__ == '__main__': - mail_sender = 'USERNAME@DOMAIN' # Например: vasyapupkin@mail.ru - mail_passwd = 'PASSWORD' # Пароль к почте +if __name__ == "__main__": + mail_sender = "USERNAME@DOMAIN" # Например: vasyapupkin@mail.ru + mail_passwd = "PASSWORD" # Пароль к почте - smtp_server = 'YOUR.MAIL.SERVER' # Например: smtp.mail.ru + smtp_server = "YOUR.MAIL.SERVER" # Например: smtp.mail.ru port = 587 - mail_text = 'Example!\nFirst!\nSecond!\n\nРаз!\nДва!\nТри!' - mail_subject = 'Здарова чувак! Hello!!!' + mail_text = "Example!\nFirst!\nSecond!\n\nРаз!\nДва!\nТри!" + mail_subject = "Здарова чувак! Hello!!!" mail_from = mail_sender mail_to = [ mail_sender @@ -34,26 +34,26 @@ # Create a text/plain message msg = MIMEMultipart() - msg['From'] = mail_from - msg['To'] = ', '.join(mail_to) + msg["From"] = mail_from + msg["To"] = ", ".join(mail_to) # msg['Cc'] = ', '.join(mail_cc) # Получатели копии письма - msg['Subject'] = mail_subject + msg["Subject"] = mail_subject msg.attach(MIMEText(mail_text)) - with open('attach_files/im1.png', mode='rb') as f: + with open("attach_files/im1.png", mode="rb") as f: im = MIMEImage(f.read()) - im.add_header('content-disposition', 'attachment', filename='pict') + im.add_header("content-disposition", "attachment", filename="pict") msg.attach(im) - with open('attach_files/im2.jpg', mode='rb') as f: + with open("attach_files/im2.jpg", mode="rb") as f: im = MIMEImage(f.read()) - im.add_header('content-disposition', 'attachment', filename='im2.jpg') + im.add_header("content-disposition", "attachment", filename="im2.jpg") msg.attach(im) - with open('attach_files/hello.html', mode='rb') as f: - t = MIMEText(f.read(), _charset='utf-8') - t.add_header('content-disposition', 'attachment', filename='hello.html') + with open("attach_files/hello.html", mode="rb") as f: + t = MIMEText(f.read(), _charset="utf-8") + t.add_header("content-disposition", "attachment", filename="hello.html") msg.attach(t) try: @@ -63,7 +63,7 @@ s.login(mail_sender, mail_passwd) s.send_message(msg) - print('Email sent') + print("Email sent") except Exception as e: - print('Error sending mail: ' + str(e)) \ No newline at end of file + print("Error sending mail: " + str(e)) diff --git a/email/html.py b/email/html.py index d02a23034..59adafa5b 100644 --- a/email/html.py +++ b/email/html.py @@ -1,23 +1,26 @@ -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -import smtplib +__author__ = "ipetrash" -__author__ = 'ipetrash' """Пример отсылки письма, содержащего обычный текст и html, "самому себе".""" +import smtplib + +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + + # http://www.tutorialspoint.com/python/python_sending_email.htm # https://docs.python.org/3.4/library/email-examples.html -if __name__ == '__main__': - mail_sender = 'USERNAME@DOMAIN' # Например: vasyapupkin@mail.ru - mail_passwd = 'PASSWORD' # Пароль к почте +if __name__ == "__main__": + mail_sender = "USERNAME@DOMAIN" # Например: vasyapupkin@mail.ru + mail_passwd = "PASSWORD" # Пароль к почте - smtp_server = 'YOUR.MAIL.SERVER' # Например: smtp.mail.ru + smtp_server = "YOUR.MAIL.SERVER" # Например: smtp.mail.ru port = 587 - mail_subject = 'Здарова чувак! Hello!!!' + mail_subject = "Здарова чувак! Hello!!!" mail_from = mail_sender mail_to = [ mail_sender @@ -32,10 +35,10 @@ # Create a text/plain message msg = MIMEMultipart() - msg['From'] = mail_from - msg['To'] = ', '.join(mail_to) + msg["From"] = mail_from + msg["To"] = ", ".join(mail_to) # msg['Cc'] = ', '.join(mail_cc) # Получатели копии письма - msg['Subject'] = mail_subject + msg["Subject"] = mail_subject # Create the body of the message (a plain-text and an HTML version). text = "Hi!\nHow are you?\nHere is the link you wanted:\nhttps://www.python.org" @@ -52,7 +55,7 @@ """ msg.attach(MIMEText(text)) - msg.attach(MIMEText(html, _subtype='html')) + msg.attach(MIMEText(html, _subtype="html")) msg.attach(MIMEText(html)) try: @@ -62,7 +65,7 @@ s.login(mail_sender, mail_passwd) s.send_message(msg) - print('Email sent') + print("Email sent") except Exception as e: - print('Error sending mail: ' + str(e)) \ No newline at end of file + print("Error sending mail: " + str(e)) diff --git a/email/last_from.py b/email/last_from.py index e323b3fe5..a0978de4c 100644 --- a/email/last_from.py +++ b/email/last_from.py @@ -1,21 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import email +import imaplib import sys import logging + +from datetime import date, timedelta + + logging.basicConfig( level=logging.DEBUG, stream=sys.stdout, - format='[%(asctime)s] %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s' + format="[%(asctime)s] %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s", ) -username = '' -password = '' -smtp_server = '' -from_email = '' +username = "" +password = "" +smtp_server = "" +from_email = "" def get_last_lunch_menu(): @@ -24,27 +30,24 @@ def get_last_lunch_menu(): """ - logging.debug('Check last email.') + logging.debug("Check last email.") - import imaplib connect = imaplib.IMAP4(smtp_server) connect.login(username, password) connect.select() # Если не ограничивать датой, соберет все письма и запрос будет дольше выполняться - from datetime import date, timedelta today = date.today() week_ago = today - timedelta(weeks=1) - since = week_ago.strftime('%d-%b-%Y') + since = week_ago.strftime("%d-%b-%Y") - logging.debug('Search emails from %s.', from_email) - typ, msgnums = connect.search(None, 'HEADER From', from_email, 'SINCE', since) - logging.debug('Search result: %s.', msgnums[0].split()) + logging.debug("Search emails from %s.", from_email) + typ, msgnums = connect.search(None, "HEADER From", from_email, "SINCE", since) + logging.debug("Search result: %s.", msgnums[0].split()) last_id = msgnums[0].split()[-1] - typ, data = connect.fetch(last_id, '(RFC822)') + typ, data = connect.fetch(last_id, "(RFC822)") - import email msg = email.message_from_bytes(data[0][1]) connect.close() @@ -53,6 +56,6 @@ def get_last_lunch_menu(): return msg -if __name__ == '__main__': +if __name__ == "__main__": msg = get_last_lunch_menu() print(msg) diff --git a/email/simple.py b/email/simple.py index 9baadce6d..40875db3d 100644 --- a/email/simple.py +++ b/email/simple.py @@ -2,112 +2,45 @@ # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Пример отсылки простого письма самому себе.""" -import smtplib -from email.mime.text import MIMEText - - # http://www.tutorialspoint.com/python/python_sending_email.htm # https://docs.python.org/3.4/library/email-examples.html -# if __name__ == '__main__': -# mail_sender = 'USERNAME@DOMAIN' # Например: vasyapupkin@mail.ru -# mail_passwd = 'PASSWORD' # Пароль к почте -# -# smtp_server = 'YOUR.MAIL.SERVER' # Например: smtp.mail.ru -# port = 587 -# -# mail_text = 'Example!\nFirst!\nSecond!\n\nРаз!\nДва!\nТри!' -# mail_subject = 'Здарова чувак! Hello!!!' -# mail_from = mail_sender -# mail_to = [ -# mail_sender -# # , '*****@mail.com', -# # ... -# ] -# # mail_cc = [ -# # # '*****@mail.com', -# # # '*****@gmail.com', -# # ... -# # ] -# -# # Create a text/plain message -# msg = MIMEText(mail_text) -# msg['From'] = mail_from -# msg['To'] = ', '.join(mail_to) -# # msg['Cc'] = ', '.join(mail_cc) # Получатели копии письма -# msg['Subject'] = mail_subject -# -# try: -# # Send the message on SMTP server. -# with smtplib.SMTP(smtp_server, port) as s: -# s.starttls() -# s.login(mail_sender, mail_passwd) -# s.send_message(msg) -# -# print('Email sent') -# -# except Exception as e: -# print('Error sending mail: ' + str(e)) - - -# import smtplib -# from email.MIMEMultipart import MIMEMultipart -# from email.MIMEText import MIMEText -# -# msg = MIMEMultipart() -# msg['From'] = 'me@gmail.com' -# msg['To'] = 'you@gmail.com' -# msg['Subject'] = 'simple email in python' -# message = 'here is the email' -# msg.attach(MIMEText(message)) -# -# mailserver = smtplib.SMTP('smtp.gmail.com',587) -# # identify ourselves to smtp gmail client -# mailserver.ehlo() -# # secure our email with tls encryption -# mailserver.starttls() -# # re-identify ourselves as an encrypted connection -# mailserver.ehlo() -# mailserver.login('me@gmail.com', 'mypassword') -# -# mailserver.sendmail('me@gmail.com','you@gmail.com',msg.as_string()) -# -# mailserver.quit() - - -SMTPserver = 'smtp.att.yahoo.com' -sender = 'me@my_email_domain.net' -destination = ['recipient@her_email_domain.com'] + +import sys + +# this invokes the secure SMTP protocol (port 465, uses SSL) +from smtplib import SMTP_SSL as SMTP + +from email.mime.text import MIMEText + + +SMTPserver = "smtp.att.yahoo.com" +sender = "me@my_email_domain.net" +destination = ["recipient@her_email_domain.com"] USERNAME = "USER_NAME_FOR_INTERNET_SERVICE_PROVIDER" PASSWORD = "PASSWORD_INTERNET_SERVICE_PROVIDER" # typical values for text_subtype are plain, html, xml -text_subtype = 'plain' +text_subtype = "plain" -content="""\ +content = """\ Test message """ -subject="Sent from Python" - -import sys - -from smtplib import SMTP_SSL as SMTP # this invokes the secure SMTP protocol (port 465, uses SSL) -# from smtplib import SMTP # use this for standard SMTP protocol (port 25, no encryption) -from email.mime.text import MIMEText +subject = "Sent from Python" try: msg = MIMEText(content, text_subtype) - msg['Subject']= subject - msg['From'] = sender # some SMTP servers will do this automatically, not all + msg["Subject"] = subject + msg["From"] = sender # some SMTP servers will do this automatically, not all conn = SMTP(SMTPserver) conn.set_debuglevel(False) @@ -118,4 +51,4 @@ conn.close() except Exception as e: - sys.exit( "mail failed; %s" % str(e) ) # give a error message + sys.exit("mail failed; %s" % str(e)) # give a error message diff --git a/email_validator__examples/hello_world.py b/email_validator__examples/hello_world.py new file mode 100644 index 000000000..68212391a --- /dev/null +++ b/email_validator__examples/hello_world.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install email-validator +from email_validator import validate_email, EmailNotValidError + + +emails = [ + "my+address@mydomain.tld", + "мой-адрес@mail.ru", +] +is_new_account = True # False for login pages + +for email in emails: + try: + # Check that the email address is valid. + validation = validate_email(email, check_deliverability=is_new_account) + + # Take the normalized form of the email address + # for all logic beyond this point (especially + # before going to a database query where equality + # may not take into account Unicode normalization). + email = validation.email + + except EmailNotValidError as e: + # Email is not valid. + # The exception message is human-readable. + print(f"{email!r}, error: {e!r}") + # 'my+address@mydomain.tld', error: EmailUndeliverableError('The domain name mydomain.tld does not exist.') + + print(email) +""" +my+address@mydomain.tld +мой-адрес@mail.ru +""" + +print() + +is_new_account = False +for email in emails: + validation = validate_email(email, check_deliverability=is_new_account) + print(validation.email) +""" +my+address@mydomain.tld +мой-адрес@mail.ru +""" diff --git a/emulating input().py b/emulating input().py index ab2757eb9..471560e04 100644 --- a/emulating input().py +++ b/emulating input().py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from io import StringIO -buffer = StringIO( - "dfdfdfdf\n12\n34", - newline='\n' -) + + +buffer = StringIO("dfdfdfdf\n12\n34", newline="\n") + def input(*args, **kwargs): - return next(buffer).rstrip('\n') + return next(buffer).rstrip("\n") text = input() diff --git a/enum__examples.py b/enum__examples.py index ef2717063..2c9cbae63 100644 --- a/enum__examples.py +++ b/enum__examples.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # https://docs.python.org/3.5/library/enum.html @@ -22,11 +22,12 @@ class Animal(Enum): print() print(Animal) # -print(list(Animal)) # [, , , ] +print(list(Animal)) +# [, , , ] print() for x in Animal: - print('{}: "{}" = {}'.format(x, x.name, x.value)) + print(f'{x}: "{x.name}" = {x.value}') print() @@ -50,23 +51,23 @@ class Animal2(IntEnum): # https://docs.python.org/3.5/library/enum.html#planet class Planet(Enum): - MERCURY = (3.303e+23, 2.4397e6) - VENUS = (4.869e+24, 6.0518e6) - EARTH = (5.976e+24, 6.37814e6) - MARS = (6.421e+23, 3.3972e6) - JUPITER = (1.9e+27, 7.1492e7) - SATURN = (5.688e+26, 6.0268e7) - URANUS = (8.686e+25, 2.5559e7) - NEPTUNE = (1.024e+26, 2.4746e7) - - def __init__(self, mass, radius): + MERCURY = (3.303e23, 2.4397e6) + VENUS = (4.869e24, 6.0518e6) + EARTH = (5.976e24, 6.37814e6) + MARS = (6.421e23, 3.3972e6) + JUPITER = (1.9e27, 7.1492e7) + SATURN = (5.688e26, 6.0268e7) + URANUS = (8.686e25, 2.5559e7) + NEPTUNE = (1.024e26, 2.4746e7) + + def __init__(self, mass, radius) -> None: self.mass = mass # in kilograms self.radius = radius # in meters @property def surface_gravity(self): # universal gravitational constant (m3 kg-1 s-2) - G = 6.67300E-11 + G = 6.67300e-11 return G * self.mass / (self.radius * self.radius) @@ -81,40 +82,41 @@ class StrEnum(str, Enum): @unique class Color(StrEnum): - RED = 'red' - GREEN = 'green' - BLUE = 'blue' + RED = "red" + GREEN = "green" + BLUE = "blue" -print(Color.RED == 'red') # True -print(Color.GREEN == 'green') # True -print(Color.GREEN == 'red') # False +print(Color.RED == "red") # True +print(Color.GREEN == "green") # True +print(Color.GREEN == "red") # False print() print(Color.RED in Color) # True print("red" in Color) # False print("red" in Color) # False print() -print('Color is ' + Color.RED) # Color is red -print('Color is ' + Color.RED + Color.GREEN) # Color is redgreen -print('Colors: ' + ', '.join(Color)) # Colors: red, green, blue +print("Color is " + Color.RED) # Color is red +print("Color is " + Color.RED + Color.GREEN) # Color is redgreen +print("Colors: " + ", ".join(Color)) # Colors: red, green, blue print() data = [ { - 'name': 'car', - 'color': Color.RED, + "name": "car", + "color": Color.RED, }, { - 'name': 'dog', - 'color': Color.BLUE, + "name": "dog", + "color": Color.BLUE, }, ] print(data) -print(Color('red')) # Color.red -print(Color('blue')) # Color.blue +print(Color("red")) # Color.red +print(Color("blue")) # Color.blue # print(Color('yellow')) # ValueError: 'yellow' is not a valid Color print() import json + print(json.dumps(data, indent=4)) diff --git a/enum__examples/hello_world__IntEnum__auto_value.py b/enum__examples/hello_world__IntEnum__auto_value.py index bd185ee51..4b2d3ca71 100644 --- a/enum__examples/hello_world__IntEnum__auto_value.py +++ b/enum__examples/hello_world__IntEnum__auto_value.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from enum import IntEnum, auto @@ -14,21 +14,21 @@ class Direction(IntEnum): RIGHT = auto() -print(Direction) # -print(Direction.UP) # Direction.UP -print(Direction.DOWN) # Direction.DOWN +print(Direction) # +print(Direction.UP) # Direction.UP +print(Direction.DOWN) # Direction.DOWN print() print(Direction.UP.value) # 1 -print(int(Direction.UP)) # 1 +print(int(Direction.UP)) # 1 print() -print(Direction.UP == 1) # True -print(Direction.UP == 2) # False +print(Direction.UP == 1) # True +print(Direction.UP == 2) # False print() direction = Direction.UP -print(direction == Direction.UP) # True +print(direction == Direction.UP) # True print(direction == Direction.DOWN) # False print() @@ -36,5 +36,8 @@ class Direction(IntEnum): print(Direction(3)) # Direction.LEFT print() -print(list(Direction)) # [, , , ] -print([x for x in Direction]) # [, , , ] +print(list(Direction)) +# [, , , ] + +print([x for x in Direction]) +# [, , , ] diff --git a/enum__examples/hello_world__auto_value.py b/enum__examples/hello_world__auto_value.py index ac03e56df..9d61e07c5 100644 --- a/enum__examples/hello_world__auto_value.py +++ b/enum__examples/hello_world__auto_value.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from enum import Enum, auto @@ -14,18 +14,18 @@ class Direction(Enum): RIGHT = auto() -print(Direction) # -print(Direction.UP) # Direction.UP -print(Direction.DOWN) # Direction.DOWN +print(Direction) # +print(Direction.UP) # Direction.UP +print(Direction.DOWN) # Direction.DOWN print() print(Direction.UP.value) # 1 -print(Direction.UP == 1) # False -print(Direction.UP == 2) # False +print(Direction.UP == 1) # False +print(Direction.UP == 2) # False print() direction = Direction.UP -print(direction == Direction.UP) # True +print(direction == Direction.UP) # True print(direction == Direction.DOWN) # False print() @@ -33,5 +33,8 @@ class Direction(Enum): print(Direction(3)) # Direction.LEFT print() -print(list(Direction)) # [, , , ] -print([x for x in Direction]) # [, , , ] +print(list(Direction)) +# [, , , ] + +print([x for x in Direction]) +# [, , , ] diff --git a/enum__examples/random_value.py b/enum__examples/random_value.py index b11f1129a..d9df1a41b 100644 --- a/enum__examples/random_value.py +++ b/enum__examples/random_value.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from enum import Enum, auto @@ -17,7 +17,7 @@ class MoveEnum(Enum): for _ in range(5): button = choice(list(MoveEnum)) - print(f'{button:<14} {button.name:<5} {button.value}') + print(f"{button:<14} {button.name:<5} {button.value}") print() print(choices(list(MoveEnum), k=5)) diff --git a/enum__examples/value_as_title.py b/enum__examples/value_as_title.py index ab3e18fe1..a93d989da 100644 --- a/enum__examples/value_as_title.py +++ b/enum__examples/value_as_title.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from enum import Enum, auto +from random import choice class KeyboardEnum(Enum): @@ -19,12 +20,11 @@ def title(self): return self.name.replace("_", " ") -print(KeyboardEnum.GET_ALL) # KeyboardEnum.GET_ALL +print(KeyboardEnum.GET_ALL) # KeyboardEnum.GET_ALL print(KeyboardEnum.GET_ALL.title()) # GET ALL print() -from random import choice button = choice(list(KeyboardEnum)) print(f'{button}: "{button.title()}"') -print(KeyboardEnum.SEND_ALL) \ No newline at end of file +print(KeyboardEnum.SEND_ALL) diff --git a/eval_expr_total_time.py b/eval_expr_total_time.py new file mode 100644 index 000000000..10fa6bdc0 --- /dev/null +++ b/eval_expr_total_time.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from seconds_to_str import seconds_to_str + + +PATTERN_TIME: re.Pattern = re.compile(r"(\d\d:\d\d:\d\d)") +PATTERN_EXPR_WITH_TIME: re.Pattern = re.compile( + f"^{PATTERN_TIME.pattern}(?: *[+-] *{PATTERN_TIME.pattern})*$" +) +PATTERN_COMMENT: re.Pattern = re.compile("#.+$") +PATTERN_IGNORE_CHARS: re.Pattern = re.compile(r"[^\d+-:]+") + + +def get_seconds(hh_mm_ss: str) -> int: + hh, mm, ss = map(int, hh_mm_ss.split(":")) + return hh * 3600 + mm * 60 + ss + + +def preprocess_expr_with_time(text: str) -> str: + return PATTERN_TIME.sub(lambda m: str(get_seconds(m[1])), text) + + +def preprocess_text(text: str) -> str: + lines: list[str] = [] + for line in text.splitlines(): + line = PATTERN_COMMENT.sub("", line) + line = PATTERN_IGNORE_CHARS.sub("", line) + lines.append(line) + + return "".join(lines) + + +def eval_expr_with_time(text: str) -> str: + text: str = preprocess_text(text) + if not PATTERN_EXPR_WITH_TIME.match(text): + raise Exception(f"Expression {text!r} not valid!") + + text: str = preprocess_expr_with_time(text) + total_seconds: int = eval(text) + return seconds_to_str(total_seconds) + + +if __name__ == "__main__": + text = """ + 08:53:11 - 07:15:00 + + 08:56:12 + """ + result: str = eval_expr_with_time(text) + print(result) + assert result == "10:34:23" + + text = """ + # This is comment + 08:53:11 - 07:15:00 + # Day 2 + + 08:56:12 # Comment 2 + """ + result: str = eval_expr_with_time(text) + print(result) + assert result == "10:34:23" + + assert get_seconds("00:00:01") == 1 + assert get_seconds("00:01:01") == 61 + assert get_seconds("01:01:01") == 3661 + + assert preprocess_expr_with_time("00:00:01") == "1" + assert ( + preprocess_expr_with_time("00:00:01 + 00:01:01 + 01:01:01") == "1 + 61 + 3661" + ) + + assert eval_expr_with_time("08:53:11") == "08:53:11" + assert eval_expr_with_time("00:00:01 + 00:01:01 + 01:01:01") == "01:02:03" diff --git a/exchange_rates/banki_ru.py b/exchange_rates/banki_ru.py index f28f1076e..350442793 100644 --- a/exchange_rates/banki_ru.py +++ b/exchange_rates/banki_ru.py @@ -1,32 +1,39 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import requests def exchange_rate(currency_id, timestamp=None): if timestamp is None: from datetime import datetime + timestamp = int(datetime.today().timestamp()) data = { - 'currency_id': currency_id, - 'date': timestamp + "currency_id": currency_id, + "date": timestampб } headers = { - 'X-Requested-With': 'XMLHttpRequest', - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0' + "X-Requested-With": "XMLHttpRequest", + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0", } - import requests - rs = requests.post('http://www.banki.ru/products/currency/ajax/quotations/value/cbr/', json=data, headers=headers) - return rs.json()['value'] + rs = requests.post( + "http://www.banki.ru/products/currency/ajax/quotations/value/cbr/", + json=data, + headers=headers, + ) + return rs.json()["value"] -if __name__ == '__main__': +if __name__ == "__main__": # 840 -- USD - print('USD:', exchange_rate(840)) + print("USD:", exchange_rate(840)) # 978 -- EUR - print('EUR:', exchange_rate(978)) + print("EUR:", exchange_rate(978)) diff --git a/exchange_rates/cbr_ru.py b/exchange_rates/cbr_ru.py index e58c4075b..5ec019b5a 100644 --- a/exchange_rates/cbr_ru.py +++ b/exchange_rates/cbr_ru.py @@ -1,7 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +from datetime import date +from urllib.request import urlopen + +from lxml import etree def exchange_rate(currency, date_req=None): @@ -15,19 +21,16 @@ def exchange_rate(currency, date_req=None): # Example: # http://www.cbr.ru/scripts/XML_daily.asp # http://www.cbr.ru/scripts/XML_daily.asp?date_req=21.10.2016 - from datetime import date if date_req is None: date_req = date.today() if isinstance(date_req, date): - date_req = date_req.strftime('%d.%m.%Y') + date_req = date_req.strftime("%d.%m.%Y") - url = 'http://www.cbr.ru/scripts/XML_daily.asp?date_req=' + date_req + url = "http://www.cbr.ru/scripts/XML_daily.asp?date_req=" + date_req # url = 'http://www.cbr.ru/scripts/XML_daily.asp' - from urllib.request import urlopen with urlopen(url) as f: - from lxml import etree root = etree.XML(f.read()) # @@ -41,33 +44,33 @@ def exchange_rate(currency, date_req=None): # ... for valute in root: - ccy = valute.xpath('child::CharCode/text()')[0] - value = valute.xpath('child::Value/text()')[0] + ccy = valute.xpath("child::CharCode/text()")[0] + value = valute.xpath("child::Value/text()")[0] if currency == ccy: - return float(value.replace(',', '.')), root.attrib['Date'] + return float(value.replace(",", ".")), root.attrib["Date"] return None, None -if __name__ == '__main__': - print('USD:', exchange_rate('USD')) - print('EUR:', exchange_rate('EUR')) +if __name__ == "__main__": + print("USD:", exchange_rate("USD")) + print("EUR:", exchange_rate("EUR")) from datetime import date, timedelta date_req = date.today() - value, rate_date = exchange_rate('USD', date_req) - print('{}: USD: {}'.format(rate_date, value)) + value, rate_date = exchange_rate("USD", date_req) + print(f"{rate_date}: USD: {value}") date_req = date.today() - timedelta(days=1) - value, rate_date = exchange_rate('USD', date_req) - print('{}: USD: {}'.format(rate_date, value)) + value, rate_date = exchange_rate("USD", date_req) + print(f"{rate_date}: USD: {value}") date_req = date.today() + timedelta(days=1) - value, rate_date = exchange_rate('USD', date_req) - print('{}: USD: {}'.format(rate_date, value)) + value, rate_date = exchange_rate("USD", date_req) + print(f"{rate_date}: USD: {value}") date_req = date.today() + timedelta(days=2) - value, rate_date = exchange_rate('USD', date_req) - print('{}: USD: {}'.format(rate_date, value)) + value, rate_date = exchange_rate("USD", date_req) + print(f"{rate_date}: USD: {value}") diff --git a/exchange_rates/cbr_ru_parse.py b/exchange_rates/cbr_ru_parse.py index 08aa971ef..f2c2b3b64 100644 --- a/exchange_rates/cbr_ru_parse.py +++ b/exchange_rates/cbr_ru_parse.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Парсер курса доллара и евро за текущую дату от сайта центробанка России.""" @@ -14,12 +14,12 @@ from robobrowser import RoboBrowser -date_req = date.today().strftime('%d.%m.%Y') -url = 'http://www.cbr.ru/scripts/XML_daily.asp?date_req=' + date_req +date_req = date.today().strftime("%d.%m.%Y") +url = "http://www.cbr.ru/scripts/XML_daily.asp?date_req=" + date_req browser = RoboBrowser( - user_agent='Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0', - parser='html.parser' + user_agent="Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0", + parser="html.parser", ) browser.open(url) rs = browser.response @@ -28,9 +28,9 @@ print(rs.status_code, rs.reason) sys.exit() -for valute_el in browser.select('Valute'): - char_code = valute_el.select_one('CharCode').get_text(strip=True) - value = valute_el.select_one('Value').get_text(strip=True) +for valute_el in browser.select("Valute"): + char_code = valute_el.select_one("CharCode").get_text(strip=True) + value = valute_el.select_one("Value").get_text(strip=True) - if char_code in ['USD', 'EUR']: + if char_code in ["USD", "EUR"]: print(char_code, value) diff --git a/exchange_rates/yahoo.py b/exchange_rates/yahoo.py index a32446aa2..2861f0900 100644 --- a/exchange_rates/yahoo.py +++ b/exchange_rates/yahoo.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Получение курса доллара и евро от yahoo.""" -# TODO: непонятно за какую дату находит -if __name__ == '__main__': - url = 'https://query.yahooapis.com/v1/public/yql?q=select+*+from+yahoo.finance.xchange+where+pair+=+%22USDRUB,EURRUB%22&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys&callback=' +import requests + - import requests - rs = requests.get(url) +# TODO: непонятно за какую дату находит +url = "https://query.yahooapis.com/v1/public/yql?q=select+*+from+yahoo.finance.xchange+where+pair+=+%22USDRUB,EURRUB%22&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys&callback=" +rs = requests.get(url) - text = 'Курс:' - for rate in rs.json()['query']['results']['rate']: - text += '\n' + rate['Name'].split('/')[0] + ' ' + rate['Rate'] +text = "Курс:" +for rate in rs.json()["query"]["results"]["rate"]: + text += "\n" + rate["Name"].split("/")[0] + " " + rate["Rate"] - print(text) +print(text) diff --git a/exit_handler.py b/exit_handler.py index 7d92419f8..b4a31d9a0 100644 --- a/exit_handler.py +++ b/exit_handler.py @@ -1,16 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import atexit from timeit import default_timer as timer + + start_time = timer() -def exit_handler(): - print('Execution time: {:.3f} secs.'.format(timer() - start_time)) +def exit_handler() -> None: + print(f"Execution time: {timer() - start_time:.3f} secs.") atexit.register(exit_handler) @@ -18,9 +20,9 @@ def exit_handler(): # OR with decorator: @atexit.register -def exit_handler(): - print('Execution time: {:.3f} secs.'.format(timer() - start_time)) +def exit_handler() -> None: + print(f"Execution time: {timer() - start_time:.3f} secs.") -number = int(input('Input number: ')) -print('My super sum:', sum(range(number ** 2))) +number = int(input("Input number: ")) +print("My super sum:", sum(range(number**2))) diff --git a/explore__windows.py b/explore__windows.py index d0ff58b97..818614c91 100644 --- a/explore__windows.py +++ b/explore__windows.py @@ -1,27 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from pathlib import Path import subprocess -from typing import Union + +from pathlib import Path -def explore(path: Union[str, Path], select=True): +def explore(path: str | Path, select=True) -> None: path = Path(path).resolve() if path.is_dir() or path.is_file(): args = ["explorer"] if select: - args.append('/select,') + args.append("/select,") args.append(str(path)) subprocess.run(args, shell=True) -if __name__ == '__main__': +if __name__ == "__main__": current_dir = Path(__file__).resolve().parent # Open parent dir and select diff --git a/extract__one_file_exe__pyinstaller/_source_test_file.py b/extract__one_file_exe__pyinstaller/_source_test_file.py index 437b01bc5..0c8d714e5 100644 --- a/extract__one_file_exe__pyinstaller/_source_test_file.py +++ b/extract__one_file_exe__pyinstaller/_source_test_file.py @@ -2,11 +2,12 @@ # Python bytecode 3.7 (3394) # Decompiled from: Python 3.7.3 (default, Apr 24 2019, 15:29:51) [MSC v.1915 64 bit (AMD64)] # Embedded file name: extract__one_file_exe__pyinstaller\_test_file.py -__author__ = 'ipetrash' +__author__ = "ipetrash" + def say(): - print('Hello World!') + print("Hello World!") -if __name__ == '__main__': - say() \ No newline at end of file +if __name__ == "__main__": + say() diff --git a/extract__one_file_exe__pyinstaller/_test_file.py b/extract__one_file_exe__pyinstaller/_test_file.py index e846ca7f8..b9c0207b7 100644 --- a/extract__one_file_exe__pyinstaller/_test_file.py +++ b/extract__one_file_exe__pyinstaller/_test_file.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Prints def say(): - print('Hello World!') + print("Hello World!") -if __name__ == '__main__': +if __name__ == "__main__": say() diff --git a/extract__one_file_exe__pyinstaller/main.py b/extract__one_file_exe__pyinstaller/main.py index 472296960..bea0ddc2e 100644 --- a/extract__one_file_exe__pyinstaller/main.py +++ b/extract__one_file_exe__pyinstaller/main.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import subprocess +import os import shutil +import subprocess import sys + from pathlib import Path -import os from urllib.request import urlretrieve @@ -16,44 +17,41 @@ # SOURCE: https://github.com/rocky/python-uncompyle6 -print('START: INSTALL SCRIPTS') -subprocess.call([ - 'pip', 'install', 'uncompyle6' -]) +print("START: INSTALL SCRIPTS") +subprocess.call(["pip", "install", "uncompyle6"]) urlretrieve( - 'https://raw.githubusercontent.com/extremecoders-re/pyinstxtractor/master/pyinstxtractor.py', - 'pyinstxtractor.py' + "https://raw.githubusercontent.com/extremecoders-re/pyinstxtractor/master/pyinstxtractor.py", + "pyinstxtractor.py", ) -print('FINISH: INSTALL SCRIPTS') +print("FINISH: INSTALL SCRIPTS") -print('\n' + '-' * 100 + '\n') +print("\n" + "-" * 100 + "\n") -print('START: BUILD EXE') -subprocess.call([ - "pyinstaller", '--clean', "--onefile", '_test_file.py' -]) +print("START: BUILD EXE") +subprocess.call(["pyinstaller", "--clean", "--onefile", "_test_file.py"]) -shutil.rmtree('build') -os.remove('_test_file.spec') -print('FINISH: BUILD EXE') +shutil.rmtree("build") +os.remove("_test_file.spec") +print("FINISH: BUILD EXE") -print('\n' + '-' * 100 + '\n') +print("\n" + "-" * 100 + "\n") -print('START: UNPACK EXE') -subprocess.call([ - sys.executable, 'pyinstxtractor.py', 'dist/_test_file.exe' -]) -print('FINISH: UNPACK EXE') +print("START: UNPACK EXE") +subprocess.call([sys.executable, "pyinstxtractor.py", "dist/_test_file.exe"]) +print("FINISH: UNPACK EXE") -print('\n' + '-' * 100 + '\n') +print("\n" + "-" * 100 + "\n") -print('START: CONVERT PYC TO PY') -source_test_file = Path('_source_test_file.py') +print("START: CONVERT PYC TO PY") +source_test_file = Path("_source_test_file.py") subprocess.call([ - 'uncompyle6', '-o', str(source_test_file), '_test_file.exe_extracted/_test_file.pyc' + "uncompyle6", + "-o", + str(source_test_file), + "_test_file.exe_extracted/_test_file.pyc", ]) -print('FINISH: CONVERT PYC TO PY') +print("FINISH: CONVERT PYC TO PY") print() print(f"Content {source_test_file.name}:") -print(source_test_file.read_text('utf-8')) +print(source_test_file.read_text("utf-8")) diff --git a/extract_from_doc.qt.io/main.py b/extract_from_doc.qt.io/main.py index 1d33eff9f..77beb4572 100644 --- a/extract_from_doc.qt.io/main.py +++ b/extract_from_doc.qt.io/main.py @@ -1,47 +1,45 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт для скачивания примеров как проекты: сохранение в папки, в файлы""" -def get_path_and_content(url): - import requests - rs = requests.get(url) +from urllib.parse import urljoin + +import requests +from bs4 import BeautifulSoup - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'lxml') - path = root.select_one('.subtitle').text.strip() - content = root.select_one('.descr').text.strip() +def get_path_and_content(url: str) -> tuple[str, str]: + rs = requests.get(url) + root = BeautifulSoup(rs.content, "lxml") + + path = root.select_one(".subtitle").text.strip() + content = root.select_one(".descr").text.strip() return path, content -def get_content(url): +def get_content(url: str) -> str: _, content = get_path_and_content(url) return content -def get_list_urls_files(url): - import requests +def get_list_urls_files(url: str) -> list[tuple[str, str]]: rs = requests.get(url) + root = BeautifulSoup(rs.content, "lxml") - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'lxml') + urls_files_list = [] - urls_files_list = list() - - for p in root.select('p'): - if p.text.strip() != 'Files:': + for p in root.select("p"): + if p.text.strip() != "Files:": continue - ul = p.find_next_sibling('ul') - for a in ul.select('li > a'): - from urllib.parse import urljoin - abs_url = urljoin(url, a['href']) - + ul = p.find_next_sibling("ul") + for a in ul.select("li > a"): + abs_url = urljoin(url, a["href"]) file_name = a.text.strip() urls_files_list.append((abs_url, file_name)) @@ -49,26 +47,27 @@ def get_list_urls_files(url): return urls_files_list -if __name__ == '__main__': - url = 'http://doc.qt.io/qt-5/qtwebengine-webenginewidgets-simplebrowser-simplebrowser-pro.html' +if __name__ == "__main__": + import os + + url = "http://doc.qt.io/qt-5/qtwebengine-webenginewidgets-simplebrowser-simplebrowser-pro.html" path, content = get_path_and_content(url) - print(path, content.encode('utf-8')) + print(path, content.encode("utf-8")) content = get_content(url) - print(content.encode('utf-8')) + print(content.encode("utf-8")) print() - url = 'http://doc.qt.io/qt-5/qtwebengine-webenginewidgets-simplebrowser-example.html' + url = "http://doc.qt.io/qt-5/qtwebengine-webenginewidgets-simplebrowser-example.html" urls_files_list = get_list_urls_files(url) - print('Files: {}'.format(len(urls_files_list))) + print(f"Files: {len(urls_files_list)}") for i, (url, path) in enumerate(urls_files_list, 1): - print('{}. "{}": {}'.format(i, path, url)) + print(f'{i}. "{path}": {url}') - import os dirs = os.path.dirname(path) os.makedirs(dirs, exist_ok=True) - with open(path, 'w', encoding='utf-8') as f: + with open(path, "w", encoding="utf-8") as f: content = get_content(url) f.write(content) diff --git a/f-strings__formatted string literals__PEP 498/example_from__python_docs.py b/f-strings__formatted string literals__PEP 498/example_from__python_docs.py index 538af82db..e320daade 100644 --- a/f-strings__formatted string literals__PEP 498/example_from__python_docs.py +++ b/f-strings__formatted string literals__PEP 498/example_from__python_docs.py @@ -1,25 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # # SOURCE: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals # -print('# Some examples of formatted string literals:') +import decimal +from datetime import datetime + + +print("# Some examples of formatted string literals:") name = "Fred" print(f"He said his name is {name!r}.") print(f"He said his name is {repr(name)}.") # repr() is equivalent to !r width = 10 precision = 4 -import decimal + value = decimal.Decimal("12.34567") print(f"result: {value:{width}.{precision}}") # nested fields -from datetime import datetime today = datetime(year=2017, month=1, day=27) print(f"{today:%b %d, %Y}") # using date format specifier @@ -27,28 +30,31 @@ print(f"{number:#0x}") # using integer format specifier print() -print('# A consequence of sharing the same syntax as regular string literals is that characters in the replacement ' - 'fields must not conflict with the quoting used in the outer formatted string literal:') +print( + "# A consequence of sharing the same syntax as regular string literals is that characters in the replacement " + "fields must not conflict with the quoting used in the outer formatted string literal:" +) -a = {'x': 'abd'} +a = {"x": "abd"} # print(f"abc {a["x"]} def") # error: outer string literal ended prematurely -print(f"abc {a['x']} def") # workaround: use different quoting +print(f"abc {a['x']} def") # workaround: use different quoting # print('# Backslashes are not allowed in format expressions and will raise an error:') # f"newline: {ord('\n')}" # raises SyntaxError print() -print('# To include a value in which a backslash escape is required, create a temporary variable.') -newline = ord('\n') +print( + "# To include a value in which a backslash escape is required, create a temporary variable." +) +newline = ord("\n") print(f"newline: {newline}") # # Formatted string literals cannot be used as docstrings, even if they do not include expressions. -def foo(): - f"Not a docstring" - -print(foo.__doc__ is None) +def foo() -> None: + f"Not a docstring" +print(foo.__doc__ is None) diff --git a/f-strings__formatted string literals__PEP 498/example_from__shultais.education.py b/f-strings__formatted string literals__PEP 498/example_from__shultais.education.py index 9e9fc6723..af3b00ca7 100644 --- a/f-strings__formatted string literals__PEP 498/example_from__shultais.education.py +++ b/f-strings__formatted string literals__PEP 498/example_from__shultais.education.py @@ -1,54 +1,60 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://shultais.education/blog/python-f-strings +from datetime import datetime as dt +from math import pi + + name = "Дмитрий" age = 25 print(f"Меня зовут {name} Мне {age} лет.") print() -print('# f-строки также поддерживают расширенное форматирование чисел:') -from math import pi +print("# f-строки также поддерживают расширенное форматирование чисел:") print(f"Значение числа pi: {pi:.2f}") print() -print('# С помощью f-строк можно форматировать дату без вызова метода strftime():') -from datetime import datetime as dt +print("# С помощью f-строк можно форматировать дату без вызова метода strftime():") + print(f"Текущее время {dt.now():%d.%m.%Y %H:%M}") now = dt.now() print(f"Текущее время {now:%d.%m.%Y %H:%M}") print() -print('# Они поддерживают базовые арифметические операции. Да, прямо в строках:') +print("# Они поддерживают базовые арифметические операции. Да, прямо в строках:") x = 10 y = 5 print(f"{x} x {y} / 2 = {x * y / 2}") print() -print('# Позволяют обращаться к значениям списков по индексу:') +print("# Позволяют обращаться к значениям списков по индексу:") planets = ["Меркурий", "Венера", "Земля", "Марс"] print(f"Мы живим не планете {planets[2]}") print() -print('# А также к элементам словаря по ключу:') +print("# А также к элементам словаря по ключу:") planet = {"name": "Земля", "radius": 6378000} print(f"Планета {planet['name']}. Радиус {planet['radius']/1000} км.") print() -print('# Причем вы можете использовать как строковые, так и числовые ключи. Точно также как в обычном Python коде:') -digits = {0: 'ноль', 'one': 'один'} +print( + "# Причем вы можете использовать как строковые, так и числовые ключи. " + "Точно также как в обычном Python коде:" +) +digits = {0: "ноль", "one": "один"} print(f"0 - {digits[0]}, 1 - {digits['one']}") print() -print('# Вы можете вызывать в f-строках методы объектов:') +print("# Вы можете вызывать в f-строках методы объектов:") name = "Дмитрий" print(f"Имя: {name.upper()}") print() -print('# А также вызывать функции:') +print("# А также вызывать функции:") print(f"13 / 3 = {round(13/3)}") diff --git a/f-strings__formatted string literals__PEP 498/my_example.py b/f-strings__formatted string literals__PEP 498/my_example.py index 2d3feafdd..b8e6168e4 100644 --- a/f-strings__formatted string literals__PEP 498/my_example.py +++ b/f-strings__formatted string literals__PEP 498/my_example.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import re print(f"My cool string is called {__author__}.") @@ -10,66 +13,66 @@ name = __author__ print(f"My cool string is called {name}.") print(f"My cool string is called {name.upper()}.") -print(f"My cool string is called {''.join(c.lower() if i % 2 else c.upper() for i, c in enumerate(__author__))}.") +print( + f"My cool string is called {''.join(c.lower() if i % 2 else c.upper() for i, c in enumerate(__author__))}." +) print(f"My cool string is called {name.replace('etr', '123')}.") print() print() a = 1 -f = f'{a + 1}' +f = f"{a + 1}" print(f"fff{f + f'{f}' + f'{a}'}") # fff221 print() def strange_1(text): - return ''.join(c.lower() if i % 2 else c.upper() for i, c in enumerate(text)) + return "".join(c.lower() if i % 2 else c.upper() for i, c in enumerate(text)) -print('Use function:') + +print("Use function:") print(f"My cool string is called {strange_1(name)}.") print() - -import re -print('Use regexp:') +print("Use regexp:") print(f"My cool string is called {re.sub(r'[a, e]', '0', name)}.") print() # Use class class Foo: - def __init__(self, text=''): + def __init__(self, text="") -> None: self.text = text def strange_2(self, text): - import re - return re.sub(r'[aeo]', ' ', text) + return re.sub(r"[aeo]", " ", text) @staticmethod - def strange_3(text): - return '"{}"'.format(text) + def strange_3(text) -> str: + return f'"{text}"' - def __format__(self, format_spec): + def __format__(self, format_spec) -> str: format_spec = format_spec.strip() if not format_spec: return self.__str__() - if format_spec == 'upper :)': + if format_spec == "upper :)": return self.__str__().upper() - if format_spec == 'revert': + if format_spec == "revert": return self.__str__()[::-1] return self.__str__() - def __str__(self): + def __str__(self) -> str: return f'' - def __repr__(self): + def __repr__(self) -> str: return f'' -print('Use class:') +print("Use class:") print(f"My cool string is called {Foo().strange_2(name)}.") print(f"My cool string is called {Foo.strange_3(name)}.") f = Foo(name) @@ -78,22 +81,24 @@ def __repr__(self): print(f"My cool string is called {f!r}.") print() -print('Use class __format__:') +print("Use class __format__:") print(f"My cool string is called {f:upper :)}.") print(f"My cool string is called {f: upper :)}.") print(f"My cool string is called {f:revert}.") print() -print('Loop:') +print("Loop:") for i in range(1, 10): - print(f' {i}: {i * i}') + print(f" {i}: {i * i}") print() -print('Lambda:') +print("Lambda:") print(f"My cool string is called {(lambda x: x.upper())(name)}.") print() -print('Raw and f-strings may be combined. For example, they could be used to build up regular expressions:') -header = 'Subject' -print(fr'{header}:\s+') +print( + "Raw and f-strings may be combined. For example, they could be used to build up regular expressions:" +) +header = "Subject" +print(rf"{header}:\s+") diff --git a/fable3_backup_save/fable3_backup_save.py b/fable3_backup_save/fable3_backup_save.py deleted file mode 100644 index 3192a615d..000000000 --- a/fable3_backup_save/fable3_backup_save.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# Backups - - -if __name__ == '__main__': - dir_path = r'C:\Users\ipetrash\Saved Games\Lionhead Studios\Fable 3\1000100010001000' - - from datetime import datetime as dt - import os - - save_name = os.path.basename(dir_path) - - backup_file_name = '{}_{}.backup.zip'.format(save_name, dt.today().strftime('%Y%m%d_%H%M%S')) - backup_full_file_name = os.path.join(os.path.dirname(dir_path), backup_file_name) - - import zipfile - - with zipfile.ZipFile(backup_full_file_name, mode="w") as f: - for file in os.listdir(dir_path): - f.write(os.path.join(dir_path, file), file) diff --git a/fable3_backup_save/run.py b/fable3_backup_save/run.py deleted file mode 100644 index ea9014180..000000000 --- a/fable3_backup_save/run.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import time -import os - -while True: - os.system('fable3_backup_save.py') - - # Каждые 12 часов - time.sleep(60 * 60 * 12) diff --git a/fastapi__examples/README.md b/fastapi__examples/README.md new file mode 100644 index 000000000..948d174e1 --- /dev/null +++ b/fastapi__examples/README.md @@ -0,0 +1,8 @@ +# Example + +http://127.0.0.1:7777/docs +http://127.0.0.1:7777/redoc + +``` +python -m uvicorn main:app --reload --port=7777 +``` diff --git a/fastapi__examples/blog_from_stepic/.gitignore b/fastapi__examples/blog_from_stepic/.gitignore new file mode 100644 index 000000000..87811cb1f --- /dev/null +++ b/fastapi__examples/blog_from_stepic/.gitignore @@ -0,0 +1,8 @@ +venv +*__pycache__* +.DS_Store +.idea +*.db +.bak +.dat +.dir \ No newline at end of file diff --git a/fastapi__examples/blog_from_stepic/README.md b/fastapi__examples/blog_from_stepic/README.md new file mode 100644 index 000000000..34c1a27a1 --- /dev/null +++ b/fastapi__examples/blog_from_stepic/README.md @@ -0,0 +1,4 @@ +https://stepik.org/lesson/1186984/step/7?unit=1222202 + +http://127.0.0.1:8000/docs +http://127.0.0.1:8000/redoc diff --git a/fastapi__examples/blog_from_stepic/requirements.txt b/fastapi__examples/blog_from_stepic/requirements.txt new file mode 100644 index 000000000..3af9cb384 --- /dev/null +++ b/fastapi__examples/blog_from_stepic/requirements.txt @@ -0,0 +1,2 @@ +fastapi==0.111.0 +uvicorn[standard]==0.30.1 diff --git a/fastapi__examples/blog_from_stepic/run.bat b/fastapi__examples/blog_from_stepic/run.bat new file mode 100644 index 000000000..98e4066f8 --- /dev/null +++ b/fastapi__examples/blog_from_stepic/run.bat @@ -0,0 +1 @@ +uvicorn blog.main:app --reload --app-dir src/ \ No newline at end of file diff --git a/fastapi__examples/blog_from_stepic/src/blog/__init__.py b/fastapi__examples/blog_from_stepic/src/blog/__init__.py new file mode 100644 index 000000000..f25732790 --- /dev/null +++ b/fastapi__examples/blog_from_stepic/src/blog/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" diff --git a/fastapi__examples/blog_from_stepic/src/blog/domains.py b/fastapi__examples/blog_from_stepic/src/blog/domains.py new file mode 100644 index 000000000..0822e86af --- /dev/null +++ b/fastapi__examples/blog_from_stepic/src/blog/domains.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass + + +@dataclass +class User: + """Обычный пользователь""" + id: str + + +@dataclass +class Admin(User): + """Пользователь, наделенный правами администратора""" + username: str + password: str + + +@dataclass +class Article: + """Сущность статьи""" + id: str + title: str + content: str diff --git a/fastapi__examples/blog_from_stepic/src/blog/main.py b/fastapi__examples/blog_from_stepic/src/blog/main.py new file mode 100644 index 000000000..46bd9e226 --- /dev/null +++ b/fastapi__examples/blog_from_stepic/src/blog/main.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from fastapi import FastAPI +from blog.resources import router + + +def get_app(): + app = FastAPI() + + app.include_router(router) # <- вот тут мы зарегистрировали роутер + + return app + + +app = get_app() diff --git a/fastapi__examples/blog_from_stepic/src/blog/repositories.py b/fastapi__examples/blog_from_stepic/src/blog/repositories.py new file mode 100644 index 000000000..c2926d033 --- /dev/null +++ b/fastapi__examples/blog_from_stepic/src/blog/repositories.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import shelve +from abc import ABC, abstractmethod + +from blog.domains import Admin, Article, User + + +class UsersRepository(ABC): + """ + Абстрактный репозиторий для пользователей. + От него нужно наследоваться в случае, когда нужно сделать другое хранилище, старое переписывать не нужно. + """ + @abstractmethod + def get_users( + self, username: str | None = None, password: str | None = None + ) -> list[User]: + pass + + +class MemoryUsersRepository(UsersRepository): + """ + Реализация пользовательского хранилища в оперативной памяти. + Пользователи инициализируются во время инициализации репозитория + """ + def __init__(self) -> None: + self.users = [ + Admin( + id="29ae7ebf-4445-42f2-9548-a3a54f095220", # это uuid4 – уникальный идентификатор пользователя + username="admin", + password="Admin_4321!", + ) + ] + + def get_users( + self, username: str | None = None, password: str | None = None + ) -> list[User]: + """ + :param username: фильтр по логину + :param password: фильтр по паролю + :return: отфильтрованные пользователи + """ + filtered_users = [] # тут собираются отфильтрованные пользователи + for user in self.users: # перебираем всех пользователей и осталвяем только тех, кто прошел фильтры + if username is not None and user.username != username: + continue + if password is not None and user.password != password: + continue + filtered_users.append(user) + return filtered_users + + +class ArticlesRepository(ABC): + """ + Абстрактный репозиторий для статей. + Он содержит методы, которые нужно реализовать в случае если захочется сделать новую реализацию репозитория. + Принцип такой же как и у пользователей. + """ + @abstractmethod + def get_articles(self) -> list[Article]: + pass + + @abstractmethod + def create_article(self, article: Article): + pass + + +class ShelveArticlesRepository(ArticlesRepository): + def __init__(self) -> None: + self.db_name = "articles" + + def get_articles(self) -> list[Article]: + with shelve.open(self.db_name) as db: + return list(db.values()) + + def create_article(self, article: Article) -> None: + with shelve.open(self.db_name) as db: + db[article.id] = article diff --git a/fastapi__examples/blog_from_stepic/src/blog/resources.py b/fastapi__examples/blog_from_stepic/src/blog/resources.py new file mode 100644 index 000000000..d46096528 --- /dev/null +++ b/fastapi__examples/blog_from_stepic/src/blog/resources.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from fastapi import APIRouter, status, HTTPException +from fastapi.responses import HTMLResponse + +from blog.domains import Admin +from blog.schemas import ( + GetArticlesModel, + CreateArticleModel, + LoginModel, + GetArticleModel, + ErrorModel, +) +from blog import services +from blog.repositories import ShelveArticlesRepository, MemoryUsersRepository + + +router = APIRouter() # это роутер, он нужен для FastAPI, чтобы определять эндпоинты + + +@router.get("/", response_class=HTMLResponse) +def index() -> str: + return """ + +
+ + + """ + + +@router.get("/articles", response_model=GetArticlesModel) +def get_articles() -> GetArticlesModel: + # во всех представлениях всегда происходит одно и то же: + # 1. получили данные + # 2. вызвали сервисный метод и получили из него результат + # 3. вернули результат клиенту в виде ответа + articles = services.get_articles(articles_repository=ShelveArticlesRepository()) + return GetArticlesModel( + items=[ + GetArticleModel(id=article.id, title=article.title, content=article.content) + for article in articles + ] + ) + + +@router.post( + "/articles", + response_model=GetArticleModel, + # 201 статус код потому что мы создаем объект – стандарт HTTP + status_code=status.HTTP_201_CREATED, + # Это нужно для сваггера. Мы перечисляем ответы эндпоинта, чтобы получить четкую документацию. + responses={201: {"model": GetArticleModel}, 401: {"model": ErrorModel}, 403: {"model": ErrorModel}}, +) +def create_article( + article: CreateArticleModel, +# credentials – тело с логином и паролем. Обычно аутентификация выглядит сложнее, но для нашего случая пойдет и так. + credentials: LoginModel, +): + current_user = services.login( + username=credentials.username, + password=credentials.password, + users_repository=MemoryUsersRepository(), + ) + + # Это аутентификация + if not current_user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized user" + ) + # а это авторизация + if not isinstance(current_user, Admin): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden resource" + ) + + article = services.create_article( + title=article.title, + content=article.content, + articles_repository=ShelveArticlesRepository(), + ) + + return GetArticleModel(id=article.id, title=article.title, content=article.content) diff --git a/fastapi__examples/blog_from_stepic/src/blog/schemas.py b/fastapi__examples/blog_from_stepic/src/blog/schemas.py new file mode 100644 index 000000000..f3cf72f94 --- /dev/null +++ b/fastapi__examples/blog_from_stepic/src/blog/schemas.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pydantic import BaseModel + + +class GetArticleModel(BaseModel): + id: str + title: str + content: str + + +class GetArticlesModel(BaseModel): + items: list[GetArticleModel] + + +class CreateArticleModel(BaseModel): + title: str + content: str + + +class LoginModel(BaseModel): + username: str + password: str + + +class ErrorModel(BaseModel): + detail: str diff --git a/fastapi__examples/blog_from_stepic/src/blog/services.py b/fastapi__examples/blog_from_stepic/src/blog/services.py new file mode 100644 index 000000000..7e285ed62 --- /dev/null +++ b/fastapi__examples/blog_from_stepic/src/blog/services.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from uuid import uuid4 + +from blog.domains import Article, User +from blog.repositories import ArticlesRepository, UsersRepository + + +def get_articles(articles_repository: ArticlesRepository) -> list[Article]: + return articles_repository.get_articles() + + +def create_article( + title: str, content: str, articles_repository: ArticlesRepository +) -> Article: + article = Article(id=str(uuid4()), title=title, content=content) + articles_repository.create_article(article=article) + return article + + +def login( + username: str, password: str, users_repository: UsersRepository +) -> User | None: + users = users_repository.get_users(username=username, password=password) + if users: + return users[0] diff --git a/fastapi__examples/blog_from_stepic/src/tests/__init__.py b/fastapi__examples/blog_from_stepic/src/tests/__init__.py new file mode 100644 index 000000000..f25732790 --- /dev/null +++ b/fastapi__examples/blog_from_stepic/src/tests/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" diff --git a/fastapi__examples/logging/config.py b/fastapi__examples/logging/config.py new file mode 100644 index 000000000..408959e9b --- /dev/null +++ b/fastapi__examples/logging/config.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from typing import Any + + +# SOURCE: https://github.com/encode/uvicorn/blob/d79f285184404694c77f7ca649858e7488270cf7/uvicorn/config.py#L66 +# Added fmt="[%(asctime)s] " +LOGGING_CONFIG: dict[str, Any] = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "()": "uvicorn.logging.DefaultFormatter", + "fmt": "[%(asctime)s] %(levelprefix)s %(message)s", + "use_colors": None, + }, + "access": { + "()": "uvicorn.logging.AccessFormatter", + "fmt": '[%(asctime)s] %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501 + }, + }, + "handlers": { + "default": { + "formatter": "default", + "class": "logging.StreamHandler", + "stream": "ext://sys.stderr", + }, + "access": { + "formatter": "access", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + }, + }, + "loggers": { + "uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False}, + "uvicorn.error": {"level": "INFO"}, + "uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False}, + }, +} + + +if __name__ == "__main__": + import yaml + yaml.dump( + LOGGING_CONFIG, + open("logging_config.yml", mode="w", encoding="utf-8"), + sort_keys=False, + ) diff --git a/fastapi__examples/logging/logging_config.yml b/fastapi__examples/logging/logging_config.yml new file mode 100644 index 000000000..a4fcd4d2d --- /dev/null +++ b/fastapi__examples/logging/logging_config.yml @@ -0,0 +1,32 @@ +version: 1 +disable_existing_loggers: false +formatters: + default: + (): uvicorn.logging.DefaultFormatter + fmt: '[%(asctime)s] %(levelprefix)s %(message)s' + use_colors: null + access: + (): uvicorn.logging.AccessFormatter + fmt: '[%(asctime)s] %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s' +handlers: + default: + formatter: default + class: logging.StreamHandler + stream: ext://sys.stderr + access: + formatter: access + class: logging.StreamHandler + stream: ext://sys.stdout +loggers: + uvicorn: + handlers: + - default + level: INFO + propagate: false + uvicorn.error: + level: INFO + uvicorn.access: + handlers: + - access + level: INFO + propagate: false diff --git a/fastapi__examples/logging/main.py b/fastapi__examples/logging/main.py new file mode 100644 index 000000000..0f55b572b --- /dev/null +++ b/fastapi__examples/logging/main.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from fastapi import FastAPI +from config import LOGGING_CONFIG + + +app = FastAPI() + + +@app.get("/") +def index(): + return {"text": "Hello World!"} + + +if __name__ == "__main__": + from pathlib import Path + import uvicorn + + uvicorn.run( + app=f"{Path(__file__).stem}:app", + host="127.0.0.1", + port=8000, + log_config=LOGGING_CONFIG, + reload=True, + ) diff --git a/fastapi__examples/logging/run_with_log_config.bat b/fastapi__examples/logging/run_with_log_config.bat new file mode 100644 index 000000000..9c340920d --- /dev/null +++ b/fastapi__examples/logging/run_with_log_config.bat @@ -0,0 +1 @@ +uvicorn main:app --reload --port=7777 --log-config=logging_config.yml \ No newline at end of file diff --git a/fastapi__examples/market_from_stepic/.gitignore b/fastapi__examples/market_from_stepic/.gitignore new file mode 100644 index 000000000..466b5ad6c --- /dev/null +++ b/fastapi__examples/market_from_stepic/.gitignore @@ -0,0 +1,12 @@ +venv +*__pycache__* +.DS_Store +.idea +*.db +.bak +.dat +.dir + +SECRET_KEY.txt +database/ +database-test/ diff --git a/fastapi__examples/market_from_stepic/README.md b/fastapi__examples/market_from_stepic/README.md new file mode 100644 index 000000000..46460a6e9 --- /dev/null +++ b/fastapi__examples/market_from_stepic/README.md @@ -0,0 +1,18 @@ +https://stepik.org/lesson/1186984/step/8?unit=1222202 + +# Полезные ссылки + +http://127.0.0.1:7777/docs +http://127.0.0.1:7777/redoc + +# Авторизация + +Токен Bearer + +Используется POST запрос на "/api/v1/token". + +Пример: [src/examples/auth_to_api.py](./src/examples/auth_to_api.py) + +# База данных + +Как и [прошлый учебный проект](../blog_from_stepic) использует shelve - ~~для прикола~~ в образовательных целях. diff --git a/fastapi__examples/market_from_stepic/requirements.txt b/fastapi__examples/market_from_stepic/requirements.txt new file mode 100644 index 000000000..72cc557f6 --- /dev/null +++ b/fastapi__examples/market_from_stepic/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.111.0 +uvicorn[standard]==0.30.1 +PyJWT==2.13.0 +passlib==1.7.4 +bcrypt==4.0.1 \ No newline at end of file diff --git a/fastapi__examples/market_from_stepic/run.bat b/fastapi__examples/market_from_stepic/run.bat new file mode 100644 index 000000000..1cf083ea8 --- /dev/null +++ b/fastapi__examples/market_from_stepic/run.bat @@ -0,0 +1 @@ +uvicorn market.main:app --reload --app-dir=src/ --port=7777 \ No newline at end of file diff --git a/fastapi__examples/market_from_stepic/run_python12.bat b/fastapi__examples/market_from_stepic/run_python12.bat new file mode 100644 index 000000000..a48ffed0e --- /dev/null +++ b/fastapi__examples/market_from_stepic/run_python12.bat @@ -0,0 +1 @@ +C:\Users\ipetrash\AppData\Local\Programs\Python\Python312\python.exe -m uvicorn market.main:app --reload --app-dir=src/ --port=7777 \ No newline at end of file diff --git a/fastapi__examples/market_from_stepic/src/examples/auth_to_api.py b/fastapi__examples/market_from_stepic/src/examples/auth_to_api.py new file mode 100644 index 000000000..7e0b646c3 --- /dev/null +++ b/fastapi__examples/market_from_stepic/src/examples/auth_to_api.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import json +import urllib.request +import urllib.parse + + +URL = "http://127.0.0.1:7777" + + +def get_token(username: str, password: str) -> str: + login_data = { + "username": username, + "password": password, + } + + req = urllib.request.Request( + f"{URL}/api/v1/token", + method="POST", + data=urllib.parse.urlencode(login_data).encode("utf-8"), + ) + with urllib.request.urlopen(req) as rs: + rs_data = json.loads(rs.read().decode("utf-8")) + return rs_data["token"] + + +def get_users(token: str) -> dict[str, dict]: + req = urllib.request.Request( + f"{URL}/api/v1/users", + method="GET", + ) + req.add_header("Authorization", f"Bearer {token}") + + with urllib.request.urlopen(req) as rs: + return json.loads(rs.read().decode("utf-8")) + + +if __name__ == "__main__": + token = get_token(username="admin", password="Admin_4321!") + print(token) + # eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyOWFlN2ViZi00NDQ1LTQyZjItOTU0OC1hM2E1NGYwOTUyMjAiLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3MjE5MTg0MTh9.rTETSqUw0hRWJQFrBcHo9NiHwhfeUqZ0brDMuPp70xw + + print(get_users(token)) + # {'items': [{'id': '29ae7ebf-4445-42f2-9548-a3a54f095220', 'role': 'admin', 'username': 'admin'}]} diff --git a/fastapi__examples/market_from_stepic/src/market/__init__.py b/fastapi__examples/market_from_stepic/src/market/__init__.py new file mode 100644 index 000000000..f25732790 --- /dev/null +++ b/fastapi__examples/market_from_stepic/src/market/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" diff --git a/fastapi__examples/market_from_stepic/src/market/auth.py b/fastapi__examples/market_from_stepic/src/market/auth.py new file mode 100644 index 000000000..f38fc4da2 --- /dev/null +++ b/fastapi__examples/market_from_stepic/src/market/auth.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass, asdict +from datetime import datetime, timedelta, timezone +from typing import Any, Annotated + +import jwt +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer + +from market import models +from market import services +from market.config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES + + +optional_security = HTTPBearer(auto_error=False) + + +not_authenticated_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer"}, +) +credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, +) +signature_has_expired_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Signature has expired", + headers={"WWW-Authenticate": "Bearer"}, +) + + +@dataclass +class TokenPayload: + sub: str + role: models.UserRoleEnum + exp: datetime | None = None + + +def create_access_token( + token: TokenPayload, + expires_delta: timedelta = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES), +) -> str: + data: dict[str, Any] = asdict(token) + data["exp"] = datetime.now(timezone.utc) + expires_delta + return jwt.encode(data, SECRET_KEY, algorithm=ALGORITHM) + + +def parse_access_token(token_data: str) -> TokenPayload: + try: + payload: dict[str, Any] = jwt.decode( + token_data, SECRET_KEY, algorithms=[ALGORITHM] + ) + return TokenPayload(**payload) + + except jwt.exceptions.ExpiredSignatureError: + raise signature_has_expired_exception + + except Exception: + raise credentials_exception + + +def get_current_user_or_none( + credentials: HTTPAuthorizationCredentials | None = Depends(optional_security), +) -> models.User | None: + if not credentials: + return + + token = credentials.credentials + token_data = parse_access_token(token) + + user: models.User = services.get_user(token_data.sub) + if not user: + raise credentials_exception + + # NOTE: Если было понижение в роли? :D + if token_data.role != user.role: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Role in the token and in the database does not match", + ) + + return user + + +def get_current_user( + current_user: Annotated[models.User | None, Depends(get_current_user_or_none)], +) -> models.User: + if not current_user: + raise not_authenticated_exception + + return current_user + + +def get_current_user_admin( + current_user: Annotated[models.User, Depends(get_current_user)], +): + if current_user.role != models.UserRoleEnum.ADMIN: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Allowed only for admin", + ) + return current_user + + +def get_current_user_manager_or_admin( + current_user: Annotated[models.User, Depends(get_current_user)], +): + if current_user.role not in [models.UserRoleEnum.MANAGER, models.UserRoleEnum.ADMIN]: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Allowed only for admin and manager", + ) + return current_user + + +if __name__ == "__main__": + # TODO: в тесты + token_data = create_access_token( + TokenPayload(sub="dfsdfsdfsdfsdf", role=models.UserRoleEnum.ADMIN) + ) + print(token_data) + print(parse_access_token(token_data)) diff --git a/fastapi__examples/market_from_stepic/src/market/config.py b/fastapi__examples/market_from_stepic/src/market/config.py new file mode 100644 index 000000000..6478e4ee6 --- /dev/null +++ b/fastapi__examples/market_from_stepic/src/market/config.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os + +from pathlib import Path + + +DIR: Path = Path(__file__).resolve().parent + +DB_DIR_NAME: Path = DIR / "database" +DB_DIR_NAME.mkdir(parents=True, exist_ok=True) + +DB_FILE_NAME: Path = DB_DIR_NAME / "db.shelve" + +DB_TEST_DIR_NAME: Path = DIR / "database-test" +DB_TEST_DIR_NAME.mkdir(parents=True, exist_ok=True) + +DB_TEST_FILE_NAME: Path = DB_TEST_DIR_NAME / "db.shelve" + +SECRET_KEY_FILE_NAME = DIR / "SECRET_KEY.txt" +SECRET_KEY = ( + os.environ.get("SECRET_KEY") + or SECRET_KEY_FILE_NAME.read_text("utf-8").strip() +) +if not SECRET_KEY: + raise Exception("SECRET_KEY must be set in the SECRET_KEY.txt file or in an environment variable") + +ALGORITHM: str = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 30 # 30 days diff --git a/fastapi__examples/market_from_stepic/src/market/db.py b/fastapi__examples/market_from_stepic/src/market/db.py new file mode 100644 index 000000000..8a164a2ef --- /dev/null +++ b/fastapi__examples/market_from_stepic/src/market/db.py @@ -0,0 +1,509 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import functools +import threading +import shelve + +from datetime import datetime +from pathlib import Path +from typing import Any +from uuid import uuid4 + +from market import models +from market.config import DB_FILE_NAME +from market.security import get_password_hash + + +class DbException(Exception): + pass + + +class NotFoundException(DbException): + pass + + +class InvalidException(DbException): + pass + + +class InvalidOrderStatusException(InvalidException): + def __init__(self, prev_status: models.StatusOrderEnum, new_status: models.StatusOrderEnum) -> None: + super().__init__(f"Unable to change order status {prev_status.value!r} to {new_status.value!r}") + + +class DB: + KEY_USERS: str = "users" + KEY_PRODUCTS: str = "products" + KEY_SHOPPING_CARTS: str = "shopping_carts" + KEY_ORDERS: str = "orders" + KEY_INDEXES: str = "[indexes]" + + _mutex = threading.RLock() + + def session(*decorator_args, **decorator_kwargs): + def actual_decorator(func): + @functools.wraps(func) + def wrapped(self, *args, **kwargs): + with self._mutex: + has_db: bool = self.db is not None + try: + if not has_db: + self.db = shelve.open(self.file_name, writeback=True) + return func(self, *args, **kwargs) + finally: + if not has_db and self.db is not None: + self.db.close() + self.db = None + return wrapped + return actual_decorator + + def lock(*decorator_args, **decorator_kwargs): + def actual_decorator(func): + @functools.wraps(func) + def wrapped(self, *args, **kwargs): + with self._mutex: + return func(self, *args, **kwargs) + return wrapped + return actual_decorator + + @session() + def get_value(self, name: str, default: Any = None) -> Any: + if not name: + return dict(self.db) + + if name not in self.db: + return default + return self.db.get(name) + + @session() + def set_value(self, name: str, value: Any) -> None: + self.db[name] = value + + def __init__(self, file_name: Path | str = DB_FILE_NAME) -> None: + self.file_name: str = str(file_name) + self.db: shelve.Shelf | None = None + + self._do_init_db_objects() + + def _generate_id(self) -> str: + return str(uuid4()) + + @lock() + def rebuild_indexes(self, clear: bool = True) -> None: + indexes: dict[str, dict[str, str]] = self.get_value(self.KEY_INDEXES, default=dict()) + + if self.KEY_USERS not in indexes: + indexes[self.KEY_USERS] = dict() + + if self.KEY_PRODUCTS not in indexes: + indexes[self.KEY_PRODUCTS] = dict() + + if clear: + indexes.clear() + + indexes[self.KEY_USERS] = {obj.username: obj.id for obj in self.get_users()} + indexes[self.KEY_PRODUCTS] = {obj.name: obj.id for obj in self.get_products()} + + self.set_value(self.KEY_INDEXES, indexes) + + @lock() + def add_index(self, table: str, key: str, id: str) -> None: + indexes: dict[str, dict[str, str]] = self.get_value(self.KEY_INDEXES) + indexes[table][key] = id + + self.set_value(self.KEY_INDEXES, indexes) + + @lock() + def remove_index(self, table: str, key: str) -> None: + indexes: dict[str, dict[str, str]] = self.get_value(self.KEY_INDEXES) + indexes[table].pop(key) + + self.set_value(self.KEY_INDEXES, indexes) + + @lock() + def get_id_from_index(self, table: str, key: str) -> str | None: + indexes: dict[str, dict[str, str]] = self.get_value(self.KEY_INDEXES) + return indexes[table].get(key) + + @lock() + def _do_init_db_objects(self) -> None: + self.rebuild_indexes(clear=False) + + if self.KEY_USERS not in self.get_value(""): + self.set_value(self.KEY_USERS, dict()) + + if self.KEY_PRODUCTS not in self.get_value(""): + self.set_value(self.KEY_PRODUCTS, dict()) + + if self.KEY_SHOPPING_CARTS not in self.get_value(""): + self.set_value(self.KEY_SHOPPING_CARTS, dict()) + + if self.KEY_ORDERS not in self.get_value(""): + self.set_value(self.KEY_ORDERS, dict()) + + if not self.get_value(self.KEY_USERS): + self.create_user( + role=models.UserRoleEnum.ADMIN, + username="admin", + password="Admin_4321!", + id="29ae7ebf-4445-42f2-9548-a3a54f095220", + ) + + if not self.get_value(self.KEY_PRODUCTS): + self.create_product( + name="Coca Cola 1л.", + price_minor=8000, + description="Газированный напиток", + ) + self.create_product( + name="Coca Cola 2л.", + price_minor=13500, + description="Газированный напиток", + ) + self.create_product( + name="Pepsi 1л.", + price_minor=8000, + description="Газированный напиток", + ) + self.create_product( + name="Сникерс", + price_minor=4000, + description="Шоколадный батончик", + ) + + @lock() + def get_users( + self, + username: str | None = None, + ) -> list[models.UserInDb]: + """ + :param username: фильтр по логину + + :return: отфильтрованные пользователи + """ + + filtered_users = [] # Тут собираются отфильтрованные пользователи + + # Перебираем всех пользователей и оставляем только тех, кто прошел фильтры + for user in self.get_value(self.KEY_USERS).values(): + if username is not None and user.username != username: + continue + filtered_users.append(user) + + return filtered_users + + @lock() + def get_user(self, id: str, check_exists: bool = False) -> models.UserInDb | None: + obj = self.get_value(self.KEY_USERS).get(id) + if obj is None and check_exists: + raise NotFoundException(f"User #{id} not found!") + return obj + + @lock() + def get_user_by_username( + self, + username: str, + check_exists: bool = False, + ) -> models.UserInDb | None: + obj_id: str | None = self.get_id_from_index(self.KEY_USERS, username) + return self.get_user( + id=obj_id, + check_exists=check_exists, + ) + + @lock() + def create_user( + self, + role: models.UserRoleEnum, + username: str, + password: str, + id: str | None = None, + ) -> models.UserInDb: + obj_id: str | None = self.get_id_from_index(self.KEY_USERS, username) + if obj_id: + raise DbException(f"Cannot create user {username!r} - this nickname is taken") + + obj = models.UserInDb( + id=id if id else self._generate_id(), + role=role, + username=username, + hashed_password=get_password_hash(password), + ) + self.add_index(self.KEY_USERS, username, obj.id) + + users = self.get_value(self.KEY_USERS) + users[obj.id] = obj + self.set_value(self.KEY_USERS, users) + return obj + + @lock() + def create_product( + self, + name: str, + price_minor: int, + description: str, + ) -> models.Product: + obj_id: str | None = self.get_id_from_index(self.KEY_PRODUCTS, name) + if obj_id: + raise DbException(f"Cannot create product {name!r} - this name is taken") + + obj = models.Product( + id=self._generate_id(), + name=name, + price_minor=price_minor, + description=description, + ) + self.add_index(self.KEY_PRODUCTS, name, obj.id) + + products = self.get_value(self.KEY_PRODUCTS) + products[obj.id] = obj + self.set_value(self.KEY_PRODUCTS, products) + return obj + + @lock() + def update_product( + self, + id: str, + name: str | None = None, + price_minor: int | None = None, + description: str | None = None, + ) -> None: + product = self.get_product(id, check_exists=True) + + if name is not None: + product.name = name + + if price_minor is not None: + product.price_minor = price_minor + + if description is not None: + product.description = description + + products = self.get_value(self.KEY_PRODUCTS) + products[id] = product + + self.set_value(self.KEY_PRODUCTS, products) + + @lock() + def get_products(self) -> list[models.Product]: + return list(self.get_value(self.KEY_PRODUCTS).values()) + + @lock() + def get_product(self, id: str, check_exists: bool = False) -> models.Product | None: + obj = self.get_value(self.KEY_PRODUCTS).get(id) + if obj is None and check_exists: + raise NotFoundException(f"Product #{id} not found!") + return obj + + @lock() + def create_shopping_cart(self, product_ids: list[str]) -> models.ShoppingCart: + obj = models.ShoppingCart( + id=self._generate_id(), + product_ids=product_ids, + ) + shopping_carts = self.get_value(self.KEY_SHOPPING_CARTS) + shopping_carts[obj.id] = obj + self.set_value(self.KEY_SHOPPING_CARTS, shopping_carts) + return obj + + @lock() + def delete_shopping_cart(self, shopping_cart_id: str) -> None: + # Проверка наличия + self.get_shopping_cart(shopping_cart_id, check_exists=True) + + shopping_carts: dict = self.get_value(self.KEY_SHOPPING_CARTS) + shopping_carts.pop(shopping_cart_id) + + self.set_value(self.KEY_SHOPPING_CARTS, shopping_carts) + + @lock() + def update_shopping_cart( + self, + shopping_cart_id: str, + product_ids: list[str], + ) -> None: + shopping_cart: models.ShoppingCart = self.get_shopping_cart( + shopping_cart_id, check_exists=True + ) + shopping_cart.product_ids = product_ids + + shopping_carts = self.get_value(self.KEY_SHOPPING_CARTS) + shopping_carts[shopping_cart_id] = shopping_cart + + self.set_value(self.KEY_SHOPPING_CARTS, shopping_carts) + + @lock() + def get_shopping_carts(self) -> list[models.ShoppingCart]: + return list(self.get_value(self.KEY_SHOPPING_CARTS).values()) + + @lock() + def get_shopping_cart( + self, + id: str, + check_exists: bool = False, + ) -> models.ShoppingCart | None: + obj = self.get_value(self.KEY_SHOPPING_CARTS).get(id) + if obj is None and check_exists: + raise NotFoundException(f"Shopping cart #{id} not found!") + return obj + + @lock() + def add_product_in_shopping_cart( + self, + shopping_cart_id: str, + product_id: str, + ) -> None: + shopping_cart: models.ShoppingCart = self.get_shopping_cart( + shopping_cart_id, check_exists=True + ) + + # Проверка наличия + self.get_product(product_id, check_exists=True) + + shopping_cart.product_ids.append(product_id) + self.update_shopping_cart(shopping_cart_id, shopping_cart.product_ids) + + @lock() + def remove_product_from_shopping_cart( + self, + shopping_cart_id: str, + product_id: str, + ) -> None: + shopping_cart: models.ShoppingCart = self.get_shopping_cart( + shopping_cart_id, check_exists=True + ) + + # Проверка наличия + self.get_product(product_id, check_exists=True) + + if product_id in shopping_cart.product_ids: + shopping_cart.product_ids.remove(product_id) + + self.update_shopping_cart(shopping_cart_id, shopping_cart.product_ids) + + @lock() + def create_order( + self, + email: str, + shopping_cart_id: str, + ) -> models.Order: + # Проверка наличия + self.get_shopping_cart(shopping_cart_id, check_exists=True) + + obj = models.Order( + id=self._generate_id(), + email=email, + shopping_cart_id=shopping_cart_id, + ) + orders = self.get_value(self.KEY_ORDERS) + orders[obj.id] = obj + self.set_value(self.KEY_ORDERS, orders) + return obj + + @lock() + def update_order( + self, + id: str, + email: str | None = None, + shopping_cart_id: str | None = None, + status: models.StatusOrderEnum | None = None, + cancel_reason: str | None = None, + ): + order = self.get_order(id, check_exists=True) + + complete_statuses = (models.StatusOrderEnum.FINISHED, models.StatusOrderEnum.CANCELED) + if order.status in complete_statuses: + raise DbException(f"It is forbidden to update an order with status {order.status.value!r}") + + if email is not None: + order.email = email + + if shopping_cart_id is not None: + # Проверка наличия + self.get_shopping_cart(shopping_cart_id, check_exists=True) + + order.shopping_cart_id = shopping_cart_id + + if status is not None and status != order.status: + invalid_status_exception = InvalidOrderStatusException(order.status, status) + + match order.status: + case models.StatusOrderEnum.CREATED: + pass + case models.StatusOrderEnum.IN_PROCESSED: + # Если текущий статус "в процессе", то следующий может быть или отмена, или завершение + if status not in complete_statuses: + raise invalid_status_exception + case models.StatusOrderEnum.CANCELED: + raise invalid_status_exception + case models.StatusOrderEnum.FINISHED: + raise invalid_status_exception + case _: + raise InvalidException(f"Unsupported status {status.value!r}!") + + order.status = status + + if status in complete_statuses: + order.closed_date = datetime.now() + + if cancel_reason is not None: + order.cancel_reason = cancel_reason + + orders = self.get_value(self.KEY_ORDERS) + orders[id] = order + + self.set_value(self.KEY_ORDERS, orders) + + @lock() + def get_orders(self) -> list[models.Order]: + return list(self.get_value(self.KEY_ORDERS).values()) + + @lock() + def get_order( + self, + id: str, + check_exists: bool = False, + ) -> models.Order | None: + obj = self.get_value(self.KEY_ORDERS).get(id) + if obj is None and check_exists: + raise NotFoundException(f"Order #{id} not found!") + return obj + + +db = DB() + + +if __name__ == "__main__": + # db.rebuild_indexes() + print(db.get_value("")) + + # # TODO: В тесты + # from market.config import DB_TEST_FILE_NAME + # db_test = DB(file_name=DB_TEST_FILE_NAME) + # + # value = db_test.get_value("counter", default=1) + # print(f"Counter: {value}") + # + # def inc_counter(): + # value = db_test.get_value("counter", default=1) + # db_test.set_value("counter", value + 1) + # + # inc_counter() + + # TODO: не рабочий вариант, нужно использовать методы самого DB + # current_value = db_test.get_value("counter", default=1) + # + # max_workers = 5 + # number = 50 + # expected_value = current_value + number + # + # import concurrent.futures + # with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + # futures = [executor.submit(inc_counter) for _ in range(number)] + # concurrent.futures.wait(futures) + # + # print(expected_value, db_test.get_value("counter", default=1)) diff --git a/fastapi__examples/market_from_stepic/src/market/main.py b/fastapi__examples/market_from_stepic/src/market/main.py new file mode 100644 index 000000000..2e1ecb5a4 --- /dev/null +++ b/fastapi__examples/market_from_stepic/src/market/main.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from fastapi import FastAPI, Request, status +from fastapi.responses import HTMLResponse, JSONResponse + +from market.db import DbException, NotFoundException +from market.resources import router + + +app = FastAPI() + + +@app.exception_handler(DbException) +async def unicorn_exception_handler(_: Request, exc: DbException): + status_code = ( + status.HTTP_404_NOT_FOUND + if isinstance(exc, NotFoundException) + else status.HTTP_400_BAD_REQUEST + ) + return JSONResponse( + status_code=status_code, + content={"detail": str(exc)}, + ) + + +@app.get("/", response_class=HTMLResponse) +def index() -> str: + return """\ + + + + + + + + +
+ + + + + """ + + +app.include_router(router, prefix="/api/v1") diff --git a/fastapi__examples/market_from_stepic/src/market/models.py b/fastapi__examples/market_from_stepic/src/market/models.py new file mode 100644 index 000000000..5eb290742 --- /dev/null +++ b/fastapi__examples/market_from_stepic/src/market/models.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum + +from dataclasses import dataclass, field, fields +from datetime import datetime +from typing import Any + + +# TODO: аннотации +def create_from(cls, other): + data: dict[str, Any] = dict.fromkeys( + f.name for f in fields(cls) + ) + for f in fields(other): + name = f.name + if name in data: + data[name] = getattr(other, name) + + return cls(**data) + + +class UserRoleEnum(enum.StrEnum): + MANAGER = enum.auto() + ADMIN = enum.auto() + + +class StatusOrderEnum(enum.StrEnum): + CREATED = enum.auto() + IN_PROCESSED = enum.auto() + FINISHED = enum.auto() + CANCELED = enum.auto() + + +@dataclass +class IdBasedObj: + id: str + + +@dataclass +class User(IdBasedObj): + role: UserRoleEnum + username: str = None + + +@dataclass +class UserInDb(User): + hashed_password: str = None + + +@dataclass +class LoginResponse: + token: str + user: User + + +@dataclass +class CreateUser: + username: str + password: str + role: UserRoleEnum + + +@dataclass +class Users: + items: list[User] + + +@dataclass +class CreateProduct: + """Товар""" + + name: str + price_minor: int # Копейки + description: str + + +@dataclass +class Product(CreateProduct, IdBasedObj): + pass + + +@dataclass +class UpdateProduct: + """Товар""" + + name: str | None = None + price_minor: int | None = None # Копейки + description: str | None = None + + +@dataclass +class Product(IdBasedObj): + """Товар""" + + name: str + price_minor: int # Копейки + description: str + + +@dataclass +class Products: + items: list[Product] + + +@dataclass +class ProductsBasedObj: + product_ids: list[str] = field(default_factory=list) + + +@dataclass +class CreateShoppingCart(ProductsBasedObj): + """Корзина с товарами""" + + name: str = "" + + +@dataclass +class ShoppingCart(ProductsBasedObj, IdBasedObj): + """Корзина с товарами""" + + +@dataclass +class ShoppingCarts: + items: list[ShoppingCart] + + +@dataclass +class BaseOrder: + """Заказ""" + + email: str + shopping_cart_id: str + + +@dataclass +class Order(BaseOrder, IdBasedObj): + """Заказ""" + + status: StatusOrderEnum = StatusOrderEnum.CREATED + created_date: datetime = datetime.now() + cancel_reason: str | None = None + closed_date: datetime | None = None + + +@dataclass +class UpdateOrder: + """Заказ""" + + email: str | None = None + shopping_cart_id: str | None = None + status: StatusOrderEnum | None = None + cancel_reason: str | None = None + + +@dataclass +class SubmitOrder: + """Заказ""" + + status: StatusOrderEnum + + +@dataclass +class Orders: + items: list[Order] + + +# TODO: в тесты +# user1 = UserInDb("123", UserRoleEnum.ADMIN, "dfsf", "dddd") +# print(user1) +# user2 = User("123", UserRoleEnum.ADMIN, "dfsf") +# print(user2) +# # print(User(**user1)) +# print() +# +# print(*fields(user1), sep="\n") +# print(fields(User)) +# +# print(create_from(User, user1)) diff --git a/fastapi__examples/market_from_stepic/src/market/resources.py b/fastapi__examples/market_from_stepic/src/market/resources.py new file mode 100644 index 000000000..cb3467622 --- /dev/null +++ b/fastapi__examples/market_from_stepic/src/market/resources.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from typing import Annotated + +from fastapi import APIRouter, status, Depends, HTTPException +from fastapi.security import OAuth2PasswordRequestForm + +from market import auth +from market import models +from market import services +from market.security import verify_password + + +router = APIRouter() + + +@router.post("/token") +def login_for_access_token( + credentials: Annotated[OAuth2PasswordRequestForm, Depends()], +) -> models.LoginResponse: + exception_400 = HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Incorrect username or password", + ) + + if not credentials.username or not credentials.password: + raise exception_400 + + user: models.UserInDb = services.get_user_by_username(credentials.username) + if verify_password(credentials.password, user.hashed_password): + # Generate a JWT token + access_token = auth.create_access_token( + token=auth.TokenPayload(user.id, user.role), + ) + + # Return the access token and user details + return models.LoginResponse( + token=access_token, + user=models.User( + id=user.id, + username=user.username, + role=user.role, + ), + ) + + raise exception_400 + + +@router.get("/users/me/") +def read_users_me( + current_user: Annotated[models.User, Depends(auth.get_current_user)], +) -> models.User: + return current_user + + +@router.get("/users") +def get_users( + _: Annotated[models.Users, Depends(auth.get_current_user_admin)], +) -> models.Users: + return services.get_users() + + +@router.get("/user/{id}") +def get_user(id: str) -> models.User: + return services.get_user(id) + + +@router.post( + "/users", + status_code=status.HTTP_201_CREATED, +) +def create_user( + user: models.CreateUser, + _: Annotated[models.User, Depends(auth.get_current_user_admin)], +) -> models.IdBasedObj: + return services.create_user( + role=user.role, + username=user.username, + password=user.password, + ) + + +@router.get("/products") +def get_products() -> models.Products: + return services.get_products() + + +@router.get("/product/{id}") +def get_product(id: str) -> models.Product: + return services.get_product(id) + + +@router.patch("/product/{id}") +def update_product( + id: str, + other: models.UpdateProduct, + current_user: Annotated[models.User, Depends(auth.get_current_user_manager_or_admin)], +) -> models.Product: + if current_user.role == models.UserRoleEnum.MANAGER: + # У менеджера нет прав на переименование продукта + if other.name is not None: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="No rights to edit the name field", + ) + + services.update_product( + id=id, + name=other.name, + price_minor=other.price_minor, + description=other.description, + ) + + return services.get_product(id) + + +@router.post( + "/products", + status_code=status.HTTP_201_CREATED, +) +def create_product( + product: models.CreateProduct, + _: Annotated[models.User, Depends(auth.get_current_user_admin)], +) -> models.IdBasedObj: + return services.create_product( + name=product.name, + price_minor=product.price_minor, + description=product.description, + ) + + +@router.get("/shopping-carts") +def get_shopping_carts( + _: Annotated[models.User, Depends(auth.get_current_user_manager_or_admin)], +) -> models.ShoppingCarts: + return services.get_shopping_carts() + + +@router.get("/shopping-cart/{id}") +def get_shopping_cart(id: str) -> models.ShoppingCart: + return services.get_shopping_cart(id) + + +@router.post("/shopping-cart/{id}/products") +def add_product_in_shopping_cart(id: str, add_to: models.ProductsBasedObj) -> models.ShoppingCart: + for product_id in add_to.product_ids: + services.add_product_in_shopping_cart( + shopping_cart_id=id, product_id=product_id + ) + + return services.get_shopping_cart(id) + + +@router.delete("/shopping-cart/{id}/products") +def remove_product_from_shopping_cart(id: str, remove_from: models.ProductsBasedObj) -> models.ShoppingCart: + for product_id in remove_from.product_ids: + services.remove_product_from_shopping_cart( + shopping_cart_id=id, product_id=product_id + ) + + return services.get_shopping_cart(id) + + +@router.post( + "/shopping-carts", + status_code=status.HTTP_201_CREATED, +) +def create_shopping_cart( + shopping_cart: models.CreateShoppingCart, +) -> models.IdBasedObj: + return services.create_shopping_cart( + product_ids=shopping_cart.product_ids, + ) + + +@router.delete("/shopping-cart/{id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_shopping_cart(id: str) -> None: + services.delete_shopping_cart(id) + + +@router.get("/orders") +def get_orders( + _: Annotated[models.User, Depends(auth.get_current_user_manager_or_admin)], +) -> models.Orders: + return services.get_orders() + + +@router.get("/order/{id}") +def get_order(id: str) -> models.Order: + return services.get_order(id) + + +@router.post( + "/orders", + status_code=status.HTTP_201_CREATED, +) +def create_order( + order: models.BaseOrder, +) -> models.IdBasedObj: + return services.create_order( + email=order.email, + shopping_cart_id=order.shopping_cart_id, + ) + + +@router.patch("/order/{id}") +def update_order( + id: str, + other: models.UpdateOrder, + current_user: Annotated[models.User | None, Depends(auth.get_current_user_or_none)] = None, +) -> models.Order: + services.update_order( + id=id, + email=other.email, + shopping_cart_id=other.shopping_cart_id, + status=other.status, + cancel_reason=other.cancel_reason, + context_user=current_user, + ) + + return services.get_order(id) + + +@router.post("/order/{id}/submit") +def submit_order( + id: str, + other: models.SubmitOrder, + current_user: Annotated[ + models.User | None, Depends(auth.get_current_user_or_none) + ] = None, +) -> models.Order: + services.submit_order( + id=id, + status=other.status, + context_user=current_user, + ) + + return services.get_order(id) diff --git a/fastapi__examples/market_from_stepic/src/market/security.py b/fastapi__examples/market_from_stepic/src/market/security.py new file mode 100644 index 000000000..2f2689b2b --- /dev/null +++ b/fastapi__examples/market_from_stepic/src/market/security.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from passlib.context import CryptContext + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) diff --git a/fastapi__examples/market_from_stepic/src/market/services.py b/fastapi__examples/market_from_stepic/src/market/services.py new file mode 100644 index 000000000..e0f838b71 --- /dev/null +++ b/fastapi__examples/market_from_stepic/src/market/services.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import fastapi + +from market import models +from market.db import db + + +def get_users() -> models.Users: + return models.Users( + items=[models.create_from(models.User, user) for user in db.get_users()] + ) + + +def get_user(id: str) -> models.User: + return models.create_from( + models.User, + db.get_user(id, check_exists=True), + ) + + +def get_user_by_username(username: str) -> models.UserInDb: + return db.get_user_by_username(username, check_exists=True) + + +def create_user( + role: models.UserRoleEnum, + username: str, + password: str, +) -> models.IdBasedObj: + user = db.create_user( + role=role, + username=username, + password=password, + ) + return models.IdBasedObj(id=user.id) + + +def get_products() -> models.Products: + return models.Products(items=db.get_products()) + + +def get_product(id: str) -> models.Product: + return db.get_product(id, check_exists=True) + + +def create_product( + name: str, + price_minor: int, # Копейки + description: str, +) -> models.IdBasedObj: + product = db.create_product( + name=name, + price_minor=price_minor, + description=description, + ) + return models.IdBasedObj(id=product.id) + + +def update_product( + id: str, + name: str | None = None, + price_minor: int | None = None, # Копейки + description: str | None = None, +) -> None: + db.update_product( + id=id, + name=name, + price_minor=price_minor, + description=description, + ) + + +def create_shopping_cart(product_ids: list[str] = None) -> models.IdBasedObj: + if product_ids is None: + product_ids = [] + + shopping_cart = db.create_shopping_cart( + product_ids=product_ids, + ) + return models.IdBasedObj(id=shopping_cart.id) + + +def delete_shopping_cart(shopping_cart_id: str) -> None: + db.delete_shopping_cart(shopping_cart_id) + + +def add_product_in_shopping_cart( + shopping_cart_id: str, + product_id: str, +) -> None: + db.add_product_in_shopping_cart( + shopping_cart_id=shopping_cart_id, + product_id=product_id, + ) + + +def remove_product_from_shopping_cart( + shopping_cart_id: str, + product_id: str, +) -> None: + db.remove_product_from_shopping_cart( + shopping_cart_id=shopping_cart_id, + product_id=product_id, + ) + + +def get_shopping_carts() -> models.ShoppingCarts: + return models.ShoppingCarts(items=db.get_shopping_carts()) + + +def get_shopping_cart(id: str) -> models.ShoppingCart: + return db.get_shopping_cart(id, check_exists=True) + + +def get_orders() -> models.Orders: + return models.Orders(items=db.get_orders()) + + +def get_order(id: str) -> models.Order: + return db.get_order(id, check_exists=True) + + +def create_order( + email: str, + shopping_cart_id: str, +) -> models.IdBasedObj: + obj = db.create_order( + email=email, + shopping_cart_id=shopping_cart_id, + ) + return models.IdBasedObj(id=obj.id) + + +def update_order( + id: str, + email: str | None = None, + shopping_cart_id: str | None = None, + status: models.StatusOrderEnum | None = None, + cancel_reason: str | None = None, + context_user: models.User | None = None, +) -> None: + # Клиент не может сам запускать выполнение заказа или завершать его + if context_user is None and status in (models.StatusOrderEnum.IN_PROCESSED, models.StatusOrderEnum.FINISHED): + raise fastapi.HTTPException( + status_code=fastapi.status.HTTP_403_FORBIDDEN, + detail=f"No rights to set status {status.value!r}", + ) + + cancel_reason: str | None = cancel_reason + + # Если причина отмены не задана и статус отмена + if cancel_reason is None and status == models.StatusOrderEnum.CANCELED: + user_role: models.UserRoleEnum | None = context_user.role if context_user else None + match user_role: + case models.UserRoleEnum.ADMIN: + cancel_reason = f"Canceled by admin {context_user.username!r}" + case models.UserRoleEnum.MANAGER: + cancel_reason = f"Canceled by manager {context_user.username!r}" + case _: + cancel_reason = "Canceled by user" + + db.update_order( + id=id, + email=email, + shopping_cart_id=shopping_cart_id, + status=status, + cancel_reason=cancel_reason, + ) + + +def submit_order( + id: str, + status: models.StatusOrderEnum, + context_user: models.User | None = None, +) -> None: + update_order( + id=id, + status=status, + context_user=context_user, + ) + diff --git a/fastapi__examples/market_from_stepic/src/tests/__init__.py b/fastapi__examples/market_from_stepic/src/tests/__init__.py new file mode 100644 index 000000000..f25732790 --- /dev/null +++ b/fastapi__examples/market_from_stepic/src/tests/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" diff --git a/fastapi__examples/running-programmatically.py b/fastapi__examples/running-programmatically.py new file mode 100644 index 000000000..6d558312a --- /dev/null +++ b/fastapi__examples/running-programmatically.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from fastapi import FastAPI + + +app = FastAPI() + + +@app.get("/") +def index(): + return {"text": "Hello World!"} + + +if __name__ == "__main__": + from pathlib import Path + import uvicorn + + uvicorn.run( + app=f"{Path(__file__).stem}:app", + host="127.0.0.1", + port=8000, + reload=True, + ) diff --git a/fastapi__examples/show_my_ip.py b/fastapi__examples/show_my_ip.py new file mode 100644 index 000000000..90434e702 --- /dev/null +++ b/fastapi__examples/show_my_ip.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass +from fastapi import FastAPI, Request + + +@dataclass +class Ip: + host: str + + +app = FastAPI() + + +@app.get("/") +def index(request: Request) -> str: + return request.client.host + + +@app.get("/json") +def index(request: Request) -> Ip: + return Ip( + host=request.client.host, + ) + + +if __name__ == "__main__": + from pathlib import Path + import uvicorn + + uvicorn.run( + app=f"{Path(__file__).stem}:app", + host="127.0.0.1", + port=8000, + reload=True, + ) diff --git a/fb2__parsing/common.py b/fb2__parsing/common.py index 14df1fae1..dba5d3a0a 100644 --- a/fb2__parsing/common.py +++ b/fb2__parsing/common.py @@ -1,34 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import os -import sys - -from typing import Optional -from pathlib import Path - -sys.path.append(str(Path(__file__).resolve().parent.parent)) -from human_byte_size import sizeof_fmt - - -def get_file_name_from_binary(binary_id: str, binary_content_type: str) -> str: - fmt = os.path.splitext(binary_id)[-1].lower() - - # Если формат файла есть, хорошо - if fmt in ['.png', '.jpg', '.jpeg']: - return binary_id - - content_type = binary_content_type.split('/')[-1] - return binary_id + '.' + {'jpeg': 'jpg', 'png': 'png'}[content_type] - - -def get_attribute_value_by_local_name(node, attr_name: str) -> Optional[str]: +def get_attribute_value_by_local_name(node, attr_name: str) -> str | None: for name, value in node.attrs.items(): # Получаем имя атрибута - name = name.split(':')[-1] + name = name.split(":")[-1] if name == attr_name: return value diff --git a/fb2__parsing/extract_cover_page_image.py b/fb2__parsing/extract_cover_page_image.py index 7c2b060c4..0b05eed5e 100644 --- a/fb2__parsing/extract_cover_page_image.py +++ b/fb2__parsing/extract_cover_page_image.py @@ -1,51 +1,50 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from bs4 import BeautifulSoup import base64 - +from bs4 import BeautifulSoup from common import get_attribute_value_by_local_name def get_cover_page_image(root) -> (bytes, str): - cover_page_image = root.select_one('coverpage > image') + cover_page_image = root.select_one("coverpage > image") # Вытаскиваем значение атрибута href - id_image = get_attribute_value_by_local_name(cover_page_image, 'href') + id_image = get_attribute_value_by_local_name(cover_page_image, "href") # "#cover.jpg" -> "cover.jpg" id_image = id_image[1:] # Получится, например, такой css-селектор: "[id='cover.jpg']" - binary = root.select_one("[id='{}']".format(id_image)) + binary = root.select_one(f"[id='{id_image}']") # image/jpeg -> jpeg - content_type = binary.attrs['content-type'].split('/')[-1] + content_type = binary.attrs["content-type"].split("/")[-1] # Содержимое тега будет извлечено и представлено в виде байтов - data = binary.text.encode('utf-8') + data = binary.text.encode("utf-8") - return base64.b64decode(data), {'jpeg': 'jpg', 'png': 'png'}[content_type] + return base64.b64decode(data), {"jpeg": "jpg", "png": "png"}[content_type] -if __name__ == '__main__': +if __name__ == "__main__": import glob import os - output_dir = 'output' + output_dir = "output" os.makedirs(output_dir, exist_ok=True) - for fb2_file_name in glob.glob('input/*.fb2'): - with open(fb2_file_name, encoding='utf-8') as f: - root = BeautifulSoup(f, 'html.parser') + for fb2_file_name in glob.glob("input/*.fb2"): + with open(fb2_file_name, encoding="utf-8") as f: + root = BeautifulSoup(f, "html.parser") img_data, fmt = get_cover_page_image(root) - file_name = os.path.basename(fb2_file_name) + '.' + fmt + file_name = os.path.basename(fb2_file_name) + "." + fmt file_name = os.path.join(output_dir, file_name) - with open(file_name, 'wb') as f: + with open(file_name, "wb") as f: f.write(img_data) diff --git a/fb2__parsing/extract_pictures_from_fb2/common.py b/fb2__parsing/extract_pictures_from_fb2/common.py new file mode 100644 index 000000000..64394198d --- /dev/null +++ b/fb2__parsing/extract_pictures_from_fb2/common.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os + + +def get_file_name_from_binary(binary_id: str, binary_content_type: str) -> str: + fmt = os.path.splitext(binary_id)[-1].lower() + + # Если формат файла есть, хорошо + if fmt in [".png", ".jpg", ".jpeg"]: + return binary_id + + content_type = binary_content_type.split("/")[-1] + return binary_id + "." + {"jpeg": "jpg", "png": "png"}[content_type] diff --git a/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_bs4.py b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_bs4.py index f02ef2eaf..c64aaff27 100644 --- a/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_bs4.py +++ b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_bs4.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт парсит файл формата fb2, вытаскивает из него картинки и сохраняет их в папке с таким же названием, @@ -11,32 +11,34 @@ import os import base64 import io +import traceback from bs4 import BeautifulSoup -from PIL import Image -import sys -sys.path.append('..') +# pip install humanize +from humanize import naturalsize as sizeof_fmt + +from PIL import Image -from common import sizeof_fmt, get_file_name_from_binary +from common import get_file_name_from_binary -def do(file_name, output_dir='output', debug=True): +def do(file_name, output_dir="output", debug=True) -> None: dir_fb2 = os.path.basename(file_name) dir_im = os.path.join(output_dir, dir_fb2) os.makedirs(dir_im, exist_ok=True) - debug and print(dir_im + ':') + debug and print(dir_im + ":") total_image_size = 0 - with open(file_name, 'rb') as fb2: - root = BeautifulSoup(fb2, 'html.parser') + with open(file_name, "rb") as fb2: + root = BeautifulSoup(fb2, "html.parser") binaries = root.select("binary") for i, binary in enumerate(binaries, 1): try: - im_id = binary.attrs['id'] - content_type = binary.attrs['content-type'] + im_id = binary.attrs["id"] + content_type = binary.attrs["content-type"] im_file_name = get_file_name_from_binary(im_id, content_type) im_file_name = os.path.join(dir_im, im_file_name) @@ -46,26 +48,25 @@ def do(file_name, output_dir='output', debug=True): count_bytes = len(im_data) total_image_size += count_bytes - with open(im_file_name, mode='wb') as f: + with open(im_file_name, mode="wb") as f: f.write(im_data) im = Image.open(io.BytesIO(im_data)) - debug and print(' {}. {} {} format={} size={}'.format( - i, im_id, sizeof_fmt(count_bytes), im.format, im.size - )) + debug and print( + f" {i}. {im_id} {sizeof_fmt(count_bytes)} format={im.format} size={im.size}" + ) except: - import traceback traceback.print_exc() file_size = os.path.getsize(file_name) debug and print() - debug and print('fb2 file size =', sizeof_fmt(file_size)) - debug and print('total image size = {} ({:.2f}%)'.format( - sizeof_fmt(total_image_size), total_image_size / file_size * 100 - )) + debug and print("fb2 file size =", sizeof_fmt(file_size)) + debug and print( + f"total image size = {sizeof_fmt(total_image_size)} ({total_image_size / file_size * 100:.2f}%)" + ) -if __name__ == '__main__': - fb2_file_name = '../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2' +if __name__ == "__main__": + fb2_file_name = "../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2" do(fb2_file_name) diff --git a/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_lxml.py b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_lxml.py index 863016545..80f3eb0a4 100644 --- a/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_lxml.py +++ b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_lxml.py @@ -1,42 +1,44 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт парсит файл формата fb2, вытаскивает из него картинки и сохраняет их в папке с таким же названием, как файл fb2.""" -import os import base64 import io +import os +import traceback from lxml import etree -from PIL import Image -import sys -sys.path.append('..') +# pip install humanize +from humanize import naturalsize as sizeof_fmt + +from PIL import Image -from common import sizeof_fmt, get_file_name_from_binary +from common import get_file_name_from_binary -def do(file_name, output_dir='output', debug=True): +def do(file_name, output_dir="output", debug=True) -> None: dir_fb2 = os.path.basename(file_name) dir_im = os.path.join(output_dir, dir_fb2) os.makedirs(dir_im, exist_ok=True) - debug and print(dir_im + ':') + debug and print(dir_im + ":") total_image_size = 0 - with open(file_name, 'rb') as fb2: + with open(file_name, "rb") as fb2: tree = etree.XML(fb2.read()) - binaries = tree.xpath("//*[local-name()='binary']") + binaries = tree.xpath("./*[local-name()='binary']") for i, binary in enumerate(binaries, 1): try: - im_id = binary.attrib['id'] - content_type = binary.attrib['content-type'] + im_id = binary.attrib["id"] + content_type = binary.attrib["content-type"] im_file_name = get_file_name_from_binary(im_id, content_type) im_file_name = os.path.join(dir_im, im_file_name) @@ -46,26 +48,25 @@ def do(file_name, output_dir='output', debug=True): count_bytes = len(im_data) total_image_size += count_bytes - with open(im_file_name, mode='wb') as f: + with open(im_file_name, mode="wb") as f: f.write(im_data) im = Image.open(io.BytesIO(im_data)) - debug and print(' {}. {} {} format={} size={}'.format( - i, im_id, sizeof_fmt(count_bytes), im.format, im.size - )) + debug and print( + f" {i}. {im_id} {sizeof_fmt(count_bytes)} format={im.format} size={im.size}" + ) except: - import traceback traceback.print_exc() file_size = os.path.getsize(file_name) debug and print() - debug and print('fb2 file size =', sizeof_fmt(file_size)) - debug and print('total image size = {} ({:.2f}%)'.format( - sizeof_fmt(total_image_size), total_image_size / file_size * 100 - )) + debug and print("fb2 file size =", sizeof_fmt(file_size)) + debug and print( + f"total image size = {sizeof_fmt(total_image_size)} ({total_image_size / file_size * 100:.2f}%)" + ) -if __name__ == '__main__': - fb2_file_name = '../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2' +if __name__ == "__main__": + fb2_file_name = "../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2" do(fb2_file_name) diff --git a/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_lxml_iterwalk.py b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_lxml_iterwalk.py new file mode 100644 index 000000000..c896e749b --- /dev/null +++ b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_lxml_iterwalk.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +"""Скрипт парсит файл формата fb2, вытаскивает из него картинки и сохраняет их в папке с таким же названием, +как файл fb2.""" + + +import base64 +import io +import os +import traceback + +from lxml import etree + +# pip install humanize +from humanize import naturalsize as sizeof_fmt + +from PIL import Image + +from common import get_file_name_from_binary + + +def do(file_name, output_dir="output", debug=True) -> None: + dir_fb2 = os.path.basename(file_name) + dir_im = os.path.join(output_dir, dir_fb2) + os.makedirs(dir_im, exist_ok=True) + debug and print(dir_im + ":") + + total_image_size = 0 + + with open(file_name, "rb") as fb2: + tree = etree.XML(fb2.read()) + + binaries = etree.iterwalk( + tree, + events=["end"], + tag="{http://www.gribuser.ru/xml/fictionbook/2.0}binary", + ) + for i, (_, binary) in enumerate(binaries, 1): + try: + im_id = binary.attrib["id"] + content_type = binary.attrib["content-type"] + + im_file_name = get_file_name_from_binary(im_id, content_type) + im_file_name = os.path.join(dir_im, im_file_name) + + im_data = base64.b64decode(binary.text.encode()) + + count_bytes = len(im_data) + total_image_size += count_bytes + + with open(im_file_name, mode="wb") as f: + f.write(im_data) + + im = Image.open(io.BytesIO(im_data)) + debug and print( + f" {i}. {im_id} {sizeof_fmt(count_bytes)} format={im.format} size={im.size}" + ) + + except: + traceback.print_exc() + + file_size = os.path.getsize(file_name) + debug and print() + debug and print("fb2 file size =", sizeof_fmt(file_size)) + debug and print( + f"total image size = {sizeof_fmt(total_image_size)} ({total_image_size / file_size * 100:.2f}%)" + ) + + +if __name__ == "__main__": + fb2_file_name = "../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2" + do(fb2_file_name) diff --git a/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_re.py b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_re.py index 904f56016..480e171d1 100644 --- a/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_re.py +++ b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_re.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт парсит файл формата fb2, вытаскивает из него картинки и сохраняет их в папке с таким же названием, @@ -15,26 +15,30 @@ import io import os import re +import traceback -from PIL import Image +# pip install humanize +from humanize import naturalsize as sizeof_fmt -import sys -sys.path.append('..') +from PIL import Image -from common import sizeof_fmt, get_file_name_from_binary +from common import get_file_name_from_binary -def do(file_name, output_dir='output', debug=True): +def do(file_name, output_dir="output", debug=True) -> None: dir_fb2 = os.path.basename(file_name) dir_im = os.path.join(output_dir, dir_fb2) os.makedirs(dir_im, exist_ok=True) - debug and print(dir_im + ':') + debug and print(dir_im + ":") total_image_size = 0 - with open(file_name, encoding='utf8') as fb2: - pattern = re.compile('(.+?)', re.DOTALL) + with open(file_name, encoding="utf-8") as fb2: + pattern = re.compile( + '(.+?)', + re.DOTALL, + ) find_content_type = re.compile('content-type="(.+?)"') find_id = re.compile('id="(.+?)"') @@ -66,26 +70,25 @@ def do(file_name, output_dir='output', debug=True): count_bytes = len(im_data) total_image_size += count_bytes - with open(im_file_name, mode='wb') as f: + with open(im_file_name, mode="wb") as f: f.write(im_data) im = Image.open(io.BytesIO(im_data)) - debug and print(' {}. {} {} format={} size={}'.format( - i, im_id, sizeof_fmt(count_bytes), im.format, im.size - )) + debug and print( + f" {i}. {im_id} {sizeof_fmt(count_bytes)} format={im.format} size={im.size}" + ) except: - import traceback traceback.print_exc() file_size = os.path.getsize(file_name) debug and print() - debug and print('fb2 file size =', sizeof_fmt(file_size)) - debug and print('total image size = {} ({:.2f}%)'.format( - sizeof_fmt(total_image_size), total_image_size / file_size * 100 - )) + debug and print("fb2 file size =", sizeof_fmt(file_size)) + debug and print( + f"total image size = {sizeof_fmt(total_image_size)} ({total_image_size / file_size * 100:.2f}%)" + ) -if __name__ == '__main__': - fb2_file_name = '../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2' +if __name__ == "__main__": + fb2_file_name = "../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2" do(fb2_file_name) diff --git a/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_str_find.py b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_str_find.py new file mode 100644 index 000000000..cbe234673 --- /dev/null +++ b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_str_find.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +""" +Скрипт парсит файл формата fb2, вытаскивает из него картинки и сохраняет их в папке с таким же названием, как файл fb2. +""" + + +import base64 +import io +import os +import traceback + +from typing import Iterator + +# pip install humanize +from humanize import naturalsize as sizeof_fmt + +from PIL import Image + +from common import get_file_name_from_binary + + +def find_inner(text: str, start_str: str, end_str: str) -> tuple[str | None, int]: + idx_start = text.find(start_str) + if idx_start == -1: + return None, -1 + + idx_end = text.find(end_str, idx_start + len(start_str)) + if idx_end == -1: + return None, -1 + + return ( + text[idx_start + len(start_str) : idx_end], + idx_end + len(end_str) + ) + + +def iter_blocks(text: str, start_str: str, end_str: str) -> Iterator[str]: + block, idx_next_start = find_inner(text, start_str, end_str) + if not block: + return + yield block + + while block: + text = text[idx_next_start:] + block, idx_next_start = find_inner(text, start_str, end_str) + if not block: + break + + yield block + + +def do(file_name, output_dir="output", debug=True) -> None: + dir_fb2 = os.path.basename(file_name) + dir_im = os.path.join(output_dir, dir_fb2) + os.makedirs(dir_im, exist_ok=True) + debug and print(dir_im + ":") + + total_image_size = 0 + + with open(file_name, encoding="utf-8") as fb2: + for i, binary in enumerate( + iter_blocks(fb2.read(), ""), + start=1, + ): + try: + binary_header, idx_next_start = find_inner(binary, "", ">") + + im_id, _ = find_inner(binary_header, 'id="', '"') + content_type, _ = find_inner(binary_header, 'content-type="', '"') + + im_base64 = binary[idx_next_start:] + + im_file_name = get_file_name_from_binary(im_id, content_type) + im_file_name = os.path.join(dir_im, im_file_name) + + im_data = base64.b64decode(im_base64.encode()) + + count_bytes = len(im_data) + total_image_size += count_bytes + + with open(im_file_name, mode="wb") as f: + f.write(im_data) + + im = Image.open(io.BytesIO(im_data)) + debug and print( + f" {i}. {im_id} {sizeof_fmt(count_bytes)} format={im.format} size={im.size}" + ) + except: + traceback.print_exc() + + file_size = os.path.getsize(file_name) + debug and print() + debug and print("fb2 file size =", sizeof_fmt(file_size)) + debug and print( + f"total image size = {sizeof_fmt(total_image_size)} ({total_image_size / file_size * 100:.2f}%)" + ) + + +if __name__ == "__main__": + fb2_file_name = "../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2" + do(fb2_file_name) diff --git a/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_xml_etree.py b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_xml_etree.py index 0e9e1fbf7..867a8b221 100644 --- a/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_xml_etree.py +++ b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_xml_etree.py @@ -1,31 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт парсит файл формата fb2, вытаскивает из него картинки и сохраняет их в папке с таким же названием, как файл fb2.""" -import os import base64 import io +import os +import traceback from xml.etree import ElementTree as ET -from PIL import Image -import sys -sys.path.append('..') +# pip install humanize +from humanize import naturalsize as sizeof_fmt + +from PIL import Image -from common import sizeof_fmt, get_file_name_from_binary +from common import get_file_name_from_binary -def do(file_name, output_dir='output', debug=True): +def do(file_name, output_dir="output", debug=True) -> None: dir_fb2 = os.path.basename(file_name) dir_im = os.path.join(output_dir, dir_fb2) os.makedirs(dir_im, exist_ok=True) - debug and print(dir_im + ':') + debug and print(dir_im + ":") total_image_size = 0 number = 1 @@ -36,14 +38,14 @@ def do(file_name, output_dir='output', debug=True): for child in root: tag = child.tag if "}" in tag: - tag = tag[tag.index('}') + 1:] + tag = tag[tag.index("}") + 1 :] - if tag != 'binary': + if tag != "binary": continue try: - im_id = child.attrib['id'] - content_type = child.attrib['content-type'] + im_id = child.attrib["id"] + content_type = child.attrib["content-type"] im_file_name = get_file_name_from_binary(im_id, content_type) im_file_name = os.path.join(dir_im, im_file_name) @@ -53,28 +55,27 @@ def do(file_name, output_dir='output', debug=True): count_bytes = len(im_data) total_image_size += count_bytes - with open(im_file_name, mode='wb') as f: + with open(im_file_name, mode="wb") as f: f.write(im_data) im = Image.open(io.BytesIO(im_data)) - debug and print(' {}. {} {} format={} size={}'.format( - number, im_id, sizeof_fmt(count_bytes), im.format, im.size - )) + debug and print( + f" {number}. {im_id} {sizeof_fmt(count_bytes)} format={im.format} size={im.size}" + ) number += 1 except: - import traceback traceback.print_exc() file_size = os.path.getsize(file_name) debug and print() - debug and print('fb2 file size =', sizeof_fmt(file_size)) - debug and print('total image size = {} ({:.2f}%)'.format( - sizeof_fmt(total_image_size), total_image_size / file_size * 100 - )) + debug and print("fb2 file size =", sizeof_fmt(file_size)) + debug and print( + f"total image size = {sizeof_fmt(total_image_size)} ({total_image_size / file_size * 100:.2f}%)" + ) -if __name__ == '__main__': - fb2_file_name = '../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2' +if __name__ == "__main__": + fb2_file_name = "../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2" do(fb2_file_name) diff --git a/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_xml_etree_xpath.py b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_xml_etree_xpath.py new file mode 100644 index 000000000..ae0c4e5a1 --- /dev/null +++ b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_xml_etree_xpath.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +"""Скрипт парсит файл формата fb2, вытаскивает из него картинки и сохраняет их в папке с таким же названием, +как файл fb2.""" + + +import base64 +import io +import os +import traceback + +from xml.etree import ElementTree as ET + +# pip install humanize +from humanize import naturalsize as sizeof_fmt + +from PIL import Image + +from common import get_file_name_from_binary + + +def do(file_name, output_dir="output", debug=True) -> None: + dir_fb2 = os.path.basename(file_name) + dir_im = os.path.join(output_dir, dir_fb2) + os.makedirs(dir_im, exist_ok=True) + debug and print(dir_im + ":") + + total_image_size = 0 + number = 1 + + tree = ET.parse(file_name) + root = tree.getroot() + + for child in root.iterfind("./{http://www.gribuser.ru/xml/fictionbook/2.0}binary"): + try: + im_id = child.attrib["id"] + content_type = child.attrib["content-type"] + + im_file_name = get_file_name_from_binary(im_id, content_type) + im_file_name = os.path.join(dir_im, im_file_name) + + im_data = base64.b64decode(child.text.encode()) + + count_bytes = len(im_data) + total_image_size += count_bytes + + with open(im_file_name, mode="wb") as f: + f.write(im_data) + + im = Image.open(io.BytesIO(im_data)) + debug and print( + f" {number}. {im_id} {sizeof_fmt(count_bytes)} format={im.format} size={im.size}" + ) + + number += 1 + + except: + traceback.print_exc() + + file_size = os.path.getsize(file_name) + debug and print() + debug and print("fb2 file size =", sizeof_fmt(file_size)) + debug and print( + f"total image size = {sizeof_fmt(total_image_size)} ({total_image_size / file_size * 100:.2f}%)" + ) + + +if __name__ == "__main__": + fb2_file_name = "../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2" + do(fb2_file_name) diff --git a/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_xml_expat.py b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_xml_expat.py index aa4252c44..11740ce17 100644 --- a/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_xml_expat.py +++ b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_xml_expat.py @@ -1,60 +1,61 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт парсит файл формата fb2, вытаскивает из него картинки и сохраняет их в папке с таким же названием, как файл fb2.""" -import os import base64 import io - +import os +import traceback import xml.parsers.expat -from PIL import Image -import sys -sys.path.append('..') +# pip install humanize +from humanize import naturalsize as sizeof_fmt -from common import sizeof_fmt, get_file_name_from_binary +from PIL import Image + +from common import get_file_name_from_binary -def do(file_name, output_dir='output', debug=True): +def do(file_name, output_dir="output", debug=True) -> None: dir_fb2 = os.path.basename(file_name) dir_im = os.path.join(output_dir, dir_fb2) os.makedirs(dir_im, exist_ok=True) - debug and print(dir_im + ':') + debug and print(dir_im + ":") PARSE_DATA = { - 'last_start_tag': None, - 'last_tag_attrs': None, - 'last_tag_data': '', - 'total_image_size': 0, - 'number': 1, + "last_start_tag": None, + "last_tag_attrs": None, + "last_tag_data": "", + "total_image_size": 0, + "number": 1, } - def on_start_element(name, attrs): - PARSE_DATA['last_start_tag'] = name - PARSE_DATA['last_tag_attrs'] = attrs - PARSE_DATA['last_tag_data'] = '' + def on_start_element(name, attrs) -> None: + PARSE_DATA["last_start_tag"] = name + PARSE_DATA["last_tag_attrs"] = attrs + PARSE_DATA["last_tag_data"] = "" - def on_char_data(data): - if PARSE_DATA['last_start_tag'] != 'binary': + def on_char_data(data) -> None: + if PARSE_DATA["last_start_tag"] != "binary": return - PARSE_DATA['last_tag_data'] += data + PARSE_DATA["last_tag_data"] += data - def on_end_element(name): - if name != 'binary': + def on_end_element(name) -> None: + if name != "binary": return - data = PARSE_DATA['last_tag_data'] + data = PARSE_DATA["last_tag_data"] try: - im_id = PARSE_DATA['last_tag_attrs']['id'] - content_type = PARSE_DATA['last_tag_attrs']['content-type'] + im_id = PARSE_DATA["last_tag_attrs"]["id"] + content_type = PARSE_DATA["last_tag_attrs"]["content-type"] im_file_name = get_file_name_from_binary(im_id, content_type) im_file_name = os.path.join(dir_im, im_file_name) @@ -62,20 +63,25 @@ def on_end_element(name): im_data = base64.b64decode(data.encode()) count_bytes = len(im_data) - PARSE_DATA['total_image_size'] += count_bytes + PARSE_DATA["total_image_size"] += count_bytes - with open(im_file_name, mode='wb') as f: + with open(im_file_name, mode="wb") as f: f.write(im_data) im = Image.open(io.BytesIO(im_data)) - debug and print(' {}. {} {} format={} size={}'.format( - PARSE_DATA['number'], im_id, sizeof_fmt(count_bytes), im.format, im.size - )) - - PARSE_DATA['number'] += 1 + debug and print( + " {}. {} {} format={} size={}".format( + PARSE_DATA["number"], + im_id, + sizeof_fmt(count_bytes), + im.format, + im.size, + ) + ) + + PARSE_DATA["number"] += 1 except: - import traceback traceback.print_exc() p = xml.parsers.expat.ParserCreate() @@ -83,17 +89,20 @@ def on_end_element(name): p.CharacterDataHandler = on_char_data p.EndElementHandler = on_end_element - with open(file_name, 'rb') as fb2: + with open(file_name, "rb") as fb2: p.Parse(fb2.read(), 1) file_size = os.path.getsize(file_name) debug and print() - debug and print('fb2 file size =', sizeof_fmt(file_size)) - debug and print('total image size = {} ({:.2f}%)'.format( - sizeof_fmt(PARSE_DATA['total_image_size']), PARSE_DATA['total_image_size'] / file_size * 100 - )) + debug and print("fb2 file size =", sizeof_fmt(file_size)) + debug and print( + "total image size = {} ({:.2f}%)".format( + sizeof_fmt(PARSE_DATA["total_image_size"]), + PARSE_DATA["total_image_size"] / file_size * 100, + ) + ) -if __name__ == '__main__': - fb2_file_name = '../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2' +if __name__ == "__main__": + fb2_file_name = "../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2" do(fb2_file_name) diff --git a/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_xml_sax.py b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_xml_sax.py index 50cfdd592..9877c212b 100644 --- a/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_xml_sax.py +++ b/fb2__parsing/extract_pictures_from_fb2/fb2_pictures__using_xml_sax.py @@ -1,63 +1,64 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт парсит файл формата fb2, вытаскивает из него картинки и сохраняет их в папке с таким же названием, как файл fb2.""" -import os import base64 import io - +import os +import traceback import xml.sax -from PIL import Image -import sys -sys.path.append('..') +# pip install humanize +from humanize import naturalsize as sizeof_fmt -from common import sizeof_fmt, get_file_name_from_binary +from PIL import Image +from common import get_file_name_from_binary -def do(file_name, output_dir='output', debug=True): + +def do(file_name, output_dir="output", debug=True) -> None: dir_fb2 = os.path.basename(file_name) dir_im = os.path.join(output_dir, dir_fb2) os.makedirs(dir_im, exist_ok=True) - debug and print(dir_im + ':') + debug and print(dir_im + ":") # Analog: fb2_pictures__using_xml_expat.py PARSE_DATA = { - 'last_start_tag': None, - 'last_tag_attrs': None, - 'last_tag_data': '', - 'total_image_size': 0, - 'number': 1, + "last_start_tag": None, + "last_tag_attrs": None, + "last_tag_data": "", + "total_image_size": 0, + "number": 1, } class BinaryHandler(xml.sax.ContentHandler): - def startElement(self, name, attrs): - PARSE_DATA['last_start_tag'] = name - PARSE_DATA['last_tag_attrs'] = attrs - PARSE_DATA['last_tag_data'] = '' + def startElement(self, name, attrs) -> None: + PARSE_DATA["last_start_tag"] = name + PARSE_DATA["last_tag_attrs"] = attrs + PARSE_DATA["last_tag_data"] = "" - def characters(self, content): - if PARSE_DATA['last_start_tag'] != 'binary': + def characters(self, content) -> None: + if PARSE_DATA["last_start_tag"] != "binary": return - PARSE_DATA['last_tag_data'] += content + PARSE_DATA["last_tag_data"] += content - def endElement(self, name): - if name != 'binary': + def endElement(self, name) -> None: + if name != "binary": return - data = PARSE_DATA['last_tag_data'] + data = PARSE_DATA["last_tag_data"] try: - im_id = PARSE_DATA['last_tag_attrs']['id'] - content_type = PARSE_DATA['last_tag_attrs']['content-type'] + im_id = PARSE_DATA["last_tag_attrs"]["id"] + content_type = PARSE_DATA["last_tag_attrs"]["content-type"] im_file_name = get_file_name_from_binary(im_id, content_type) im_file_name = os.path.join(dir_im, im_file_name) @@ -65,20 +66,25 @@ def endElement(self, name): im_data = base64.b64decode(data.encode()) count_bytes = len(im_data) - PARSE_DATA['total_image_size'] += count_bytes + PARSE_DATA["total_image_size"] += count_bytes - with open(im_file_name, mode='wb') as f: + with open(im_file_name, mode="wb") as f: f.write(im_data) im = Image.open(io.BytesIO(im_data)) - debug and print(' {}. {} {} format={} size={}'.format( - PARSE_DATA['number'], im_id, sizeof_fmt(count_bytes), im.format, im.size - )) - - PARSE_DATA['number'] += 1 + debug and print( + " {}. {} {} format={} size={}".format( + PARSE_DATA["number"], + im_id, + sizeof_fmt(count_bytes), + im.format, + im.size, + ) + ) + + PARSE_DATA["number"] += 1 except: - import traceback traceback.print_exc() parser = xml.sax.make_parser() @@ -87,12 +93,17 @@ def endElement(self, name): file_size = os.path.getsize(file_name) debug and print() - debug and print('fb2 file size =', sizeof_fmt(file_size)) - debug and print('total image size = {} ({:.2f}%)'.format( - sizeof_fmt(PARSE_DATA['total_image_size']), PARSE_DATA['total_image_size'] / file_size * 100 - )) - - -if __name__ == '__main__': - fb2_file_name = '../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2' + debug and print("fb2 file size =", sizeof_fmt(file_size)) + debug and print( + "total image size = {} ({:.2f}%)".format( + sizeof_fmt(PARSE_DATA["total_image_size"]), + PARSE_DATA["total_image_size"] / file_size * 100, + ) + ) + + +if __name__ == "__main__": + fb2_file_name = ( + "../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2" + ) do(fb2_file_name) diff --git a/fb2__parsing/extract_pictures_from_fb2/time_test.py b/fb2__parsing/extract_pictures_from_fb2/time_test.py index 6c81453d4..8f1040d7b 100644 --- a/fb2__parsing/extract_pictures_from_fb2/time_test.py +++ b/fb2__parsing/extract_pictures_from_fb2/time_test.py @@ -1,32 +1,47 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +from timeit import timeit + from fb2_pictures__using_lxml import do as do_lxml +from fb2_pictures__using_lxml_iterwalk import do as do_lxml_iterwalk from fb2_pictures__using_bs4 import do as do_bs4 from fb2_pictures__using_xml_expat import do as do_xml_expat from fb2_pictures__using_xml_etree import do as do_xml_etree +from fb2_pictures__using_xml_etree_xpath import do as do_xml_etree_xpath from fb2_pictures__using_xml_sax import do as do_xml_sax from fb2_pictures__using_re import do as do_using_re +from fb2_pictures__using_str_find import do as do_using_str_find -file_name = '../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2' -count = 10 - -from timeit import timeit +file_name = "../input/Непутевый ученик в школе магии 1. Зачисление в школу (Часть 1).fb2" +count = 20 runs = [ - ('LXML', 'do_lxml'), - ('XML EXPAT', 'do_xml_expat'), - ('XML.ETREE', 'do_xml_etree'), - ('XML SAX', 'do_xml_sax'), - ('REGEXP', 'do_using_re'), - ('BS4', 'do_bs4'), + # ("LXML", "do_lxml"), + # ("LXML iterwalk", "do_lxml_iterwalk"), + # ("XML EXPAT", "do_xml_expat"), + # ("XML.ETREE", "do_xml_etree"), + # ("XML.ETREE xpath", "do_xml_etree_xpath"), + # ("XML SAX", "do_xml_sax"), + ("REGEXP", "do_using_re"), + ("STR FIND", "do_using_str_find"), + ("BS4", "do_bs4"), ] -runs_format = '{:%s} | {:.3f} secs' % (max(len(x[0]) for x in runs),) +runs_format = "{:%s} | {:.3f} secs" % (max(len(x[0]) for x in runs),) for name, stmt in runs: - timing = timeit(stmt + '(file_name, debug=False)', globals=globals(), number=count) + timing = timeit(stmt + "(file_name, debug=False)", globals=globals(), number=count) print(runs_format.format(name, timing)) +""" +LXML | 0.733 secs +XML EXPAT | 0.867 secs +XML.ETREE | 0.755 secs +XML SAX | 1.014 secs +REGEXP | 2.466 secs +STR FIND | 1.210 secs +BS4 | 2.166 secs +""" diff --git a/fb2__parsing/get_annotation.py b/fb2__parsing/get_annotation.py index b736071c3..5a7e3ebb5 100644 --- a/fb2__parsing/get_annotation.py +++ b/fb2__parsing/get_annotation.py @@ -1,31 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from bs4 import BeautifulSoup def get_annotation(root) -> str: - annotation_node = root.select_one('description > title-info > annotation') + annotation_node = root.select_one("description > title-info > annotation") if not annotation_node: - return '' + return "" return annotation_node.text.strip() -if __name__ == '__main__': +if __name__ == "__main__": import glob - for fb2_file_name in glob.glob('input/*.fb2'): + for fb2_file_name in glob.glob("input/*.fb2"): print(fb2_file_name) - with open(fb2_file_name, encoding='utf-8') as f: - root = BeautifulSoup(f, 'html.parser') + with open(fb2_file_name, encoding="utf-8") as f: + root = BeautifulSoup(f, "html.parser") annotation = get_annotation(root) print(repr(annotation)) print(annotation) - print('\n') + print("\n") diff --git a/fb2__parsing/get_authors.py b/fb2__parsing/get_authors.py index 561925e8f..c6df42034 100644 --- a/fb2__parsing/get_authors.py +++ b/fb2__parsing/get_authors.py @@ -1,27 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from bs4 import BeautifulSoup -import typing -def get_authors(root) -> typing.List[str]: +def get_authors(root) -> list[str]: # Пример тега: # # Сато # Цутому # - authors_nodes = root.select('description > title-info > author') + authors_nodes = root.select("description > title-info > author") authors = [] for author in authors_nodes: - first_name = author.select_one('first-name') - last_name = author.select_one('last-name') - middle_name = author.select_one('middle-name') + first_name = author.select_one("first-name") + last_name = author.select_one("last-name") + middle_name = author.select_one("middle-name") name = [] @@ -35,19 +34,19 @@ def get_authors(root) -> typing.List[str]: name.append(middle_name.text) # ['Виталий', 'Зыков'] -> 'Виталий Зыков' - name = ' '.join(name) + name = " ".join(name) authors.append(name) return authors -if __name__ == '__main__': +if __name__ == "__main__": import glob - for fb2_file_name in glob.glob('input/*.fb2'): - with open(fb2_file_name, encoding='utf-8') as f: - root = BeautifulSoup(f, 'html.parser') + for fb2_file_name in glob.glob("input/*.fb2"): + with open(fb2_file_name, encoding="utf-8") as f: + root = BeautifulSoup(f, "html.parser") authors = get_authors(root) print(authors) diff --git a/fb2__parsing/get_notes.py b/fb2__parsing/get_notes.py index 46ee63865..282ab4132 100644 --- a/fb2__parsing/get_notes.py +++ b/fb2__parsing/get_notes.py @@ -1,23 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from bs4 import BeautifulSoup -from typing import List, Tuple - from common import get_attribute_value_by_local_name -def get_note_links(root) -> List[Tuple['href', 'text']]: +def get_note_links(root) -> list[tuple[str, str]]: # в стиле Роберта Адама [1] note_link_list = root.select('a[type="note"]') items = [] for link in note_link_list: - href = get_attribute_value_by_local_name(link, 'href') + href = get_attribute_value_by_local_name(link, "href") text = link.text.strip() items.append((href, text)) @@ -25,7 +23,7 @@ def get_note_links(root) -> List[Tuple['href', 'text']]: return items -def get_notes(root) -> List[Tuple['id', 'title', 'text']]: +def get_notes(root) -> list[tuple[str, str, str]]: # Пример тега: # # @@ -42,7 +40,7 @@ def get_notes(root) -> List[Tuple['id', 'title', 'text']]: items = [] for note in notes_list: - note_id = note.attrs['id'] + note_id = note.attrs["id"] title = note.title.text.strip() @@ -57,19 +55,19 @@ def get_notes(root) -> List[Tuple['id', 'title', 'text']]: return items -if __name__ == '__main__': +if __name__ == "__main__": import glob - for fb2_file_name in glob.glob('input/*.fb2'): - with open(fb2_file_name, encoding='utf-8') as f: - root = BeautifulSoup(f, 'html.parser') + for fb2_file_name in glob.glob("input/*.fb2"): + with open(fb2_file_name, encoding="utf-8") as f: + root = BeautifulSoup(f, "html.parser") print(fb2_file_name) note_links = get_note_links(root) - print('note_links:', note_links) + print("note_links:", note_links) notes = get_notes(root) - print('notes:', notes) + print("notes:", notes) - print('\n') + print("\n") diff --git a/fb2__parsing/get_sections.py b/fb2__parsing/get_sections.py index 94c0a6c2c..fdf63a009 100644 --- a/fb2__parsing/get_sections.py +++ b/fb2__parsing/get_sections.py @@ -1,38 +1,36 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from bs4 import BeautifulSoup -import typing -from collections import OrderedDict -def get_sections_as_dict(root) -> typing.Dict[str, dict]: +def get_sections_as_dict(root) -> dict[str, dict]: # Рекурсивная функция поиска <section> - def _find_sections(root, root_dict: dict): - for section in root.find_all('section', recursive=False): + def _find_sections(root, root_dict: dict) -> None: + for section in root.find_all("section", recursive=False): title = section.title.text.strip() - children = OrderedDict() + children = dict() root_dict[title] = children _find_sections(section, children) # Первое <body> должно быть с содержанием, описываемое <section> - body = root.select_one('body') - section_by_children = OrderedDict() + body = root.select_one("body") + section_by_children = dict() _find_sections(body, section_by_children) return section_by_children -def get_sections_as_list(root) -> typing.List[typing.Tuple[str, list]]: +def get_sections_as_list(root) -> list[tuple[str, list]]: # Рекурсивная функция поиска <section> - def _find_sections(root, children_list: list): - for section in root.find_all('section', recursive=False): + def _find_sections(root, children_list: list) -> None: + for section in root.find_all("section", recursive=False): title = section.title.text.strip() children = [] @@ -41,7 +39,7 @@ def _find_sections(root, children_list: list): _find_sections(section, children) # Первое <body> должно быть с содержанием, описываемое <section> - body = root.select_one('body') + body = root.select_one("body") root_list = [] _find_sections(body, root_list) @@ -49,25 +47,24 @@ def _find_sections(root, children_list: list): return root_list -if __name__ == '__main__': +if __name__ == "__main__": import glob import json - def _print_sections(root: dict, level=1): + def _print_sections(root: dict, level=1) -> None: for title, children in root.items(): - text = '{}{}'.format(' ' * (level - 1), title.replace('\n', '. ')) + text = "{}{}".format(" " * (level - 1), title.replace("\n", ". ")) if children: - text += ':' + text += ":" print(text) _print_sections(children, level + 1) - - for fb2_file_name in glob.glob('input/*.fb2'): + for fb2_file_name in glob.glob("input/*.fb2"): print(fb2_file_name) - with open(fb2_file_name, encoding='utf-8') as f: - root = BeautifulSoup(f, 'html.parser') + with open(fb2_file_name, encoding="utf-8") as f: + root = BeautifulSoup(f, "html.parser") sections_d = get_sections_as_dict(root) print(sections_d) @@ -78,7 +75,7 @@ def _print_sections(root: dict, level=1): print(json.dumps(sections_l, indent=4, ensure_ascii=False)) print() - print('Содержание:') + print("Содержание:") _print_sections(sections_d, level=2) - print('\n') + print("\n") diff --git a/fb2__parsing/print_section_by_text_number_length.py b/fb2__parsing/print_section_by_text_number_length.py index 7207026e8..54a691d8e 100644 --- a/fb2__parsing/print_section_by_text_number_length.py +++ b/fb2__parsing/print_section_by_text_number_length.py @@ -1,20 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from bs4 import BeautifulSoup -from typing import Dict, Tuple -from collections import OrderedDict +from bs4 import BeautifulSoup, Tag -def get_sections_as_dict(root) -> Tuple[Dict[str, dict], Dict[str, 'Section']]: +def get_sections_as_dict(root) -> tuple[dict[str, dict], dict[str, Tag]]: # Рекурсивная функция поиска <section> - def _find_sections(root, root_dict: dict, title_by_section: dict): - for section in root.find_all('section', recursive=False): + def _find_sections(root, root_dict: dict, title_by_section: dict) -> None: + for section in root.find_all("section", recursive=False): title = section.title.text.strip() - children = OrderedDict() + children = dict() root_dict[title] = children title_by_section[title] = section @@ -22,23 +20,26 @@ def _find_sections(root, root_dict: dict, title_by_section: dict): _find_sections(section, children, title_by_section) # Первое <body> должно быть с содержанием, описываемое <section> - body = root.select_one('body') - section_by_children = OrderedDict() - title_by_section = OrderedDict() + body = root.select_one("body") + section_by_children = dict() + title_by_section = dict() _find_sections(body, section_by_children, title_by_section) return section_by_children, title_by_section -def get_section_by_text(section_by_children: Dict[str, dict], title_by_section: Dict[str, 'Section']) -> Dict[str, str]: - def _find_sections(section_by_text, section_by_children): +def get_section_by_text( + section_by_children: dict[str, dict], + title_by_section: dict[str, Tag], +) -> dict[str, str]: + def _find_sections(section_by_text, section_by_children) -> None: for title, children in section_by_children.items(): if children: _find_sections(section_by_text, children) continue - text = title.replace('\n', '. ') + text = title.replace("\n", ". ") section = title_by_section[title] # Удаление <title>. Тег title относится к section, точнее, оно явлется его названием, поэтому @@ -55,27 +56,32 @@ def _find_sections(section_by_text, section_by_children): return section_by_text -if __name__ == '__main__': +if __name__ == "__main__": import glob - def _print_sections(root: dict, section_by_text: dict, number_length_book_text, level=1): - def _find_section_lines(root, level, lines: list): + def _print_sections( + root: dict, + section_by_text: dict, + number_length_book_text, + level=1 + ) -> None: + def _find_section_lines(root, level, lines: list) -> None: for title, children in root.items(): - text = '{}{}'.format(' ' * (level - 1), title.replace('\n', '. ')) + text = "{}{}".format(" " * (level - 1), title.replace("\n", ". ")) if children: - text += ':' + text += ":" if title in section_by_text: len_num = len(section_by_text[title]) lines.append(( text, - '{} символов'.format(len_num), - '{:.2%}'.format(len_num / number_length_book_text) + f"{len_num} символов", + f"{len_num / number_length_book_text:.2%}", )) else: - lines.append((text, '', '')) + lines.append((text, "", "")) _find_section_lines(children, level + 1, lines) @@ -87,7 +93,7 @@ def _find_section_lines(root, level, lines: list): max_len_columns = [max(map(len, map(str, col))) for col in zip(*lines)] # Создание строки форматирования: [30, 14, 5] -> "{:<30} | {:<14} | {:<5}" - my_table_format = ' | '.join('{:<%s}' % max_len for max_len in max_len_columns) + my_table_format = " | ".join("{:<%s}" % max_len for max_len in max_len_columns) # Ручное формирование: "{:30} | {:14} | {}" # my_table_format = '{:%s} | {:%s} | {}' % (max(len(x[0]) for x in lines), max(len(x[1]) for x in lines)) @@ -95,18 +101,18 @@ def _find_section_lines(root, level, lines: list): for line in lines: print(my_table_format.format(*line)) - for fb2_file_name in glob.glob('input/*.fb2'): + for fb2_file_name in glob.glob("input/*.fb2"): print(fb2_file_name) - with open(fb2_file_name, encoding='utf-8') as f: - root = BeautifulSoup(f, 'html.parser') + with open(fb2_file_name, encoding="utf-8") as f: + root = BeautifulSoup(f, "html.parser") section_by_children, title_by_section = get_sections_as_dict(root) section_by_text = get_section_by_text(section_by_children, title_by_section) number_length_book_text = sum(len(text) for text in section_by_text.values()) - print('Всего символов в книге:', number_length_book_text) + print("Всего символов в книге:", number_length_book_text) _print_sections(section_by_children, section_by_text, number_length_book_text) - print('\n') + print("\n") diff --git a/ffmpeg_examples/many_files_with_subprocess.py b/ffmpeg_examples/many_files_with_subprocess.py index 85a93e947..85704c652 100644 --- a/ffmpeg_examples/many_files_with_subprocess.py +++ b/ffmpeg_examples/many_files_with_subprocess.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Запуск нескольких процессов ffmpeg и ожидание пока все они завершатся.""" @@ -9,17 +9,23 @@ # Чтобы убить все процессы ffmpeg в винде: Taskkill /F /IM ffmpeg.exe +import os +import time + +from subprocess import Popen, DEVNULL + + videos = [ - 'Горит от чатика - Dark Souls #1 176x144.3gp', - 'Горит от чатика - Dark Souls #1 320x180.3gp', - 'Горит от чатика - Dark Souls #1 640x360.mp4', - 'Горит от чатика - Dark Souls #1 640x360.webm', - 'Горит от чатика - Dark Souls #1 1280x720.mp4', + "Горит от чатика - Dark Souls #1 176x144.3gp", + "Горит от чатика - Dark Souls #1 320x180.3gp", + "Горит от чатика - Dark Souls #1 640x360.mp4", + "Горит от чатика - Dark Souls #1 640x360.webm", + "Горит от чатика - Dark Souls #1 1280x720.mp4", ] -DIRECTORY = 'extracted_images' -import os +DIRECTORY = "extracted_images" + if not os.path.exists(DIRECTORY): os.mkdir(DIRECTORY) @@ -28,11 +34,12 @@ process_list = list() for file_name in videos: - for ext in ['jpg', 'png']: - command = COMMAND_PATTERN.format(directory=DIRECTORY, file_name=file_name, ext=ext) + for ext in ["jpg", "png"]: + command = COMMAND_PATTERN.format( + directory=DIRECTORY, file_name=file_name, ext=ext + ) print(command) - from subprocess import Popen, DEVNULL process = Popen(command, stderr=DEVNULL, stdout=DEVNULL) process_list.append(process) @@ -49,5 +56,4 @@ print("Все процессы завершились") break - import time time.sleep(5) diff --git a/ffmpeg_examples/text_overlay_on_video/text_overlay_on_video.py b/ffmpeg_examples/text_overlay_on_video/text_overlay_on_video.py index 8494d9630..30198dfe0 100644 --- a/ffmpeg_examples/text_overlay_on_video/text_overlay_on_video.py +++ b/ffmpeg_examples/text_overlay_on_video/text_overlay_on_video.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -13,45 +13,45 @@ # http://ffmpeg.org/ffmpeg-filters.html#drawtext-1 +import os +from subprocess import Popen, PIPE + + command_pattern = ( - '''ffmpeg -i {in} ''' - '''-vf drawtext="fontfile={font}: text='{text}': ''' - '''fontcolor={text_color}: fontsize={fontsize}: x={x}: y={y}" ''' - '''-vb 20M {out} ''' + """ffmpeg -i {in} """ + """-vf drawtext="fontfile={font}: text='{text}': """ + """fontcolor={text_color}: fontsize={fontsize}: x={x}: y={y}" """ + """-vb 20M {out} """ ) params = { - 'in': 'BH_Logo.wmv', - 'out': 'BH_Logo_new.wmv', - - 'text': 'http://ffmpeg.org/ffmpeg-filters.html#drawtext-1', - 'font': 'FreeSerif.ttf', - 'fontsize': 30, - 'text_color': 'white', - + "in": "BH_Logo.wmv", + "out": "BH_Logo_new.wmv", + "text": "http://ffmpeg.org/ffmpeg-filters.html#drawtext-1", + "font": "FreeSerif.ttf", + "fontsize": 30, + "text_color": "white", # Рисование в правом нижнем угле с небольшим отступом от границ - 'x': 'w - text_w - 5', - 'y': 'h - text_h - 5', + "x": "w - text_w - 5", + "y": "h - text_h - 5", } # Экранирование ":" (двоеточие), т.к. оно является символом разделение параметров # команды фильтра -params['text'] = params['text'].replace(':', '\\:') +params["text"] = params["text"].replace(":", "\\:") command = command_pattern.format(**params) print(command) -import os -if os.path.exists(params['out']): - os.remove(params['out']) +if os.path.exists(params["out"]): + os.remove(params["out"]) -from subprocess import Popen, PIPE with Popen(command, stdout=PIPE, stderr=PIPE, universal_newlines=True) as process: out, err = process.communicate() print(out) - print('-' * 25) + print("-" * 25) print(err) -print('-' * 25) -print('{} size: {}'.format(params['in'], os.path.getsize(params['in']))) -print('{} size: {}'.format(params['out'], os.path.getsize(params['out']))) +print("-" * 25) +print("{} size: {}".format(params["in"], os.path.getsize(params["in"]))) +print("{} size: {}".format(params["out"], os.path.getsize(params["out"]))) diff --git a/file/append.py b/file/append.py index c9632be8e..76c047a82 100644 --- a/file/append.py +++ b/file/append.py @@ -1,19 +1,23 @@ -__author__ = 'ipetrash' +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" # https://docs.python.org/3.4/tutorial/inputoutput.html#reading-and-writing-files # http://pythonworld.ru/tipy-dannyx-v-python/fajly-rabota-s-fajlami.html -if __name__ == '__main__': - from datetime import datetime - # Открыть файл в режиме добавления записей - with open('foo.txt', mode='a') as f: - now_time = datetime.now().time().strftime('%H:%M:%S') - f.write(now_time + '\n') +from datetime import datetime + + +# Открыть файл в режиме добавления записей +with open("foo.txt", mode="a", encoding="utf-8") as f: + now_time = datetime.now().time().strftime("%H:%M:%S") + f.write(now_time + "\n") - # Открыть файл в режиме добавления записей - with open('foo.txt', mode='a') as f: - f.write('!!!' + '\n') - f.write('!!' + '\n') - f.write('!' + '\n') \ No newline at end of file +# Открыть файл в режиме добавления записей +with open("foo.txt", mode="a", encoding="utf-8") as f: + f.write("!!!" + "\n") + f.write("!!" + "\n") + f.write("!" + "\n") diff --git a/file/foo.py b/file/foo.py deleted file mode 100644 index 694a37004..000000000 --- a/file/foo.py +++ /dev/null @@ -1,7 +0,0 @@ -__author__ = 'ipetrash' - - -# TODO: больше примеров -# https://docs.python.org/3.4/tutorial/inputoutput.html#reading-and-writing-files -# http://pythonworld.ru/tipy-dannyx-v-python/fajly-rabota-s-fajlami.html - diff --git a/file/read.py b/file/read.py index 4d68e4f24..61ac67c1d 100644 --- a/file/read.py +++ b/file/read.py @@ -1,16 +1,20 @@ -__author__ = 'ipetrash' +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + # https://docs.python.org/3.4/tutorial/inputoutput.html#reading-and-writing-files # http://pythonworld.ru/tipy-dannyx-v-python/fajly-rabota-s-fajlami.html -if __name__ == '__main__': - # Открыть файл в режиме чтения - with open('foo.txt', mode='r') as f: - print(f.read()) - print() +# Открыть файл в режиме чтения +with open("foo.txt", mode="r", encoding="utf-8") as f: + print(f.read()) + +print() - # Открыть файл в режиме чтения и построчно считать файл - with open('foo.txt', mode='r') as f: - for r in f: - print(r, end='') \ No newline at end of file +# Открыть файл в режиме чтения и построчно считать файл +with open("foo.txt", mode="r", encoding="utf-8") as f: + for line in f: + print(line, end="") diff --git a/file/seek.py b/file/seek.py index f10150acb..c74bcbb19 100644 --- a/file/seek.py +++ b/file/seek.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -with open('input__seek.txt') as f: +with open("input__seek.txt", encoding="utf-8") as f: for i in range(3): - print('{}.'.format(i)) + print(f"{i}.") for line in f: - print(line.strip(), end=' ') + print(line.strip(), end=" ") - print('\n') + print("\n") f.seek(0) diff --git a/file/write.py b/file/write.py index 1a4bf890b..5c4fb7898 100644 --- a/file/write.py +++ b/file/write.py @@ -1,13 +1,16 @@ -__author__ = 'ipetrash' +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" # https://docs.python.org/3.4/tutorial/inputoutput.html#reading-and-writing-files # http://pythonworld.ru/tipy-dannyx-v-python/fajly-rabota-s-fajlami.html -if __name__ == '__main__': - # Открыть файл в режиме записи - with open('foo.txt', mode='w') as f: - f.write('123\n') - f.write('one two\n') - f.write('one two\n') - f.write('раз два\n') \ No newline at end of file + +# Открыть файл в режиме записи +with open("foo.txt", mode="w", encoding="utf-8") as f: + f.write("123\n") + f.write("one two\n") + f.write("one two\n") + f.write("раз два\n") diff --git a/file_tree_maker.py b/file_tree_maker.py index d0d2124e6..67fbab1cd 100644 --- a/file_tree_maker.py +++ b/file_tree_maker.py @@ -2,18 +2,18 @@ # -*- coding: utf-8 -*- -__author__ = 'legendmohe' +__author__ = "legendmohe" # CODE: http://stackoverflow.com/a/32656429/5909792 -import os import argparse +import os -class FileTreeMaker(object): - def _recurse(self, parent_path, file_list, prefix, output_buf, level): +class FileTreeMaker: + def _recurse(self, parent_path, file_list, prefix, output_buf, level) -> None: if len(file_list) == 0 or (self.max_level != -1 and self.max_level <= level): return else: @@ -33,7 +33,13 @@ def _recurse(self, parent_path, file_list, prefix, output_buf, level): tmp_prefix = prefix + "┃ " else: tmp_prefix = prefix + " " - self._recurse(full_path, os.listdir(full_path), tmp_prefix, output_buf, level + 1) + self._recurse( + full_path, + os.listdir(full_path), + tmp_prefix, + output_buf, + level + 1, + ) elif os.path.isfile(full_path): output_buf.append("%s%s%s" % (prefix, idc, sub_path)) @@ -52,7 +58,7 @@ def make(self, args): output_str = "\n".join(buf) if len(args.output) != 0: - with open(args.output, 'w') as of: + with open(args.output, "w") as of: of.write(output_str) return output_str @@ -61,8 +67,12 @@ def make(self, args): parser = argparse.ArgumentParser() parser.add_argument("-r", "--root", help="root of file tree", default=".") parser.add_argument("-o", "--output", help="output file name", default="") - parser.add_argument("-xf", "--exclude_folder", nargs='*', help="exclude folder", default=[]) - parser.add_argument("-xn", "--exclude_name", nargs='*', help="exclude name", default=[]) + parser.add_argument( + "-xf", "--exclude_folder", nargs="*", help="exclude folder", default=[] + ) + parser.add_argument( + "-xn", "--exclude_name", nargs="*", help="exclude name", default=[] + ) parser.add_argument("-m", "--max_level", help="max level", type=int, default=-1) args = parser.parse_args() print(FileTreeMaker().make(args)) diff --git a/find_groups_of_repeating_sequences__using_itertools_groupby.py b/find_groups_of_repeating_sequences__using_itertools_groupby.py index 4eb4d072d..8cae19e6a 100644 --- a/find_groups_of_repeating_sequences__using_itertools_groupby.py +++ b/find_groups_of_repeating_sequences__using_itertools_groupby.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from itertools import groupby def get_groups_seqs(text): - return [''.join(v) for _, v in groupby(text)] + return ["".join(v) for _, v in groupby(text)] -if __name__ == '__main__': - text = 'hhhrrrraaavvvvvvv' +if __name__ == "__main__": + text = "hhhrrrraaavvvvvvv" items = get_groups_seqs(text) print(items) # ['hhh', 'rrrr', 'aaa', 'vvvvvvv'] diff --git a/find_groups_of_repeating_sequences__using_re.py b/find_groups_of_repeating_sequences__using_re.py index 6e52cdbcf..ccb6dc6a6 100644 --- a/find_groups_of_repeating_sequences__using_re.py +++ b/find_groups_of_repeating_sequences__using_re.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re def get_groups_seqs(text): - return [m.group() for m in re.finditer(r'(.)\1+', text)] + return [m.group() for m in re.finditer(r"(.)\1+", text)] -if __name__ == '__main__': - text = 'hhhrrrraaavvvvvvv' +if __name__ == "__main__": + text = "hhhrrrraaavvvvvvv" items = get_groups_seqs(text) print(items) # ['hhh', 'rrrr', 'aaa', 'vvvvvvv'] diff --git a/find_most_common_substrings_in_a_string.py b/find_most_common_substrings_in_a_string.py index b55210788..161d66a0e 100644 --- a/find_most_common_substrings_in_a_string.py +++ b/find_most_common_substrings_in_a_string.py @@ -1,18 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -text = 'ACGTTGCATGTCGCATGATGCATGAGAGCT' +from collections import defaultdict + + +text = "ACGTTGCATGTCGCATGATGCATGAGAGCT" -from collections import defaultdict accumulator = defaultdict(int) for length in range(1, len(text) + 1): for start in range(len(text) - length): - sub_text = text[start: start + length] + sub_text = text[start : start + length] accumulator[sub_text] += 1 print(accumulator) diff --git a/find_most_common_substrings_in_a_string__with_fixed_subtring_length.py b/find_most_common_substrings_in_a_string__with_fixed_subtring_length.py index c674d08aa..e16a7220f 100644 --- a/find_most_common_substrings_in_a_string__with_fixed_subtring_length.py +++ b/find_most_common_substrings_in_a_string__with_fixed_subtring_length.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -text = 'ACGTTGCATGTCGCATGATGCATGAGAGCT' -key_len = 4 +from collections import defaultdict, Counter -from collections import defaultdict, Counter -accumulator = Counter(text[i: i + key_len] for i in range(len(text) - key_len + 1)) +text = "ACGTTGCATGTCGCATGATGCATGAGAGCT" +key_len = 4 +accumulator = Counter(text[i : i + key_len] for i in range(len(text) - key_len + 1)) print(accumulator) # {'A': 7, 'C': 6, 'G': 9, 'T': 7, max_items = defaultdict(list) diff --git a/find_most_common_substrings_in_a_string__with_minimal_substring_length.py b/find_most_common_substrings_in_a_string__with_minimal_substring_length.py index b239fd9d0..f9745b3ab 100644 --- a/find_most_common_substrings_in_a_string__with_minimal_substring_length.py +++ b/find_most_common_substrings_in_a_string__with_minimal_substring_length.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -text = 'ACGTTGCATGTCGCATGATGCATGAGAGCT' -min_len = 4 +from collections import defaultdict -from collections import defaultdict +text = "ACGTTGCATGTCGCATGATGCATGAGAGCT" +min_len = 4 + accumulator = defaultdict(int) for length in range(1, len(text) + 1): for start in range(len(text) - length): - sub_text = text[start: start + length] + sub_text = text[start : start + length] accumulator[sub_text] += 1 print(accumulator) diff --git a/firefox/api.py b/firefox/api.py index 27b2dc722..5e509a032 100644 --- a/firefox/api.py +++ b/firefox/api.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging @@ -9,60 +9,59 @@ from collections import Counter from pathlib import Path -from typing import List from jsonlz4_mozLz4 import mozlz4a def get_sessionstore(file_name_session: Path) -> dict: - with open(file_name_session, 'rb') as f: + with open(file_name_session, "rb") as f: return mozlz4a.loads_json(f) -def get_tab_urls_from_sessionstore(sessionstore: dict) -> List[str]: +def get_tab_urls_from_sessionstore(sessionstore: dict) -> list[str]: urls = [] - for w in sessionstore['windows']: - for t in w['tabs']: - i = t['index'] - 1 - tab_url = t['entries'][i]['url'] + for w in sessionstore["windows"]: + for t in w["tabs"]: + i = t["index"] - 1 + tab_url = t["entries"][i]["url"] urls.append(tab_url) return urls -def get_tab_urls(file_name_session: Path) -> List[str]: +def get_tab_urls(file_name_session: Path) -> list[str]: json_data = get_sessionstore(file_name_session) return get_tab_urls_from_sessionstore(json_data) def close_tabs( - file_name_session: Path, - urls: List[str], - log: logging.Logger, -): + file_name_session: Path, + urls: list[str], + log: logging.Logger, +) -> None: modified = False json_data = get_sessionstore(file_name_session) - for w in json_data['windows']: - for t in reversed(w['tabs']): - i = t['index'] - 1 - tab_url = t['entries'][i]['url'] + for w in json_data["windows"]: + for t in reversed(w["tabs"]): + i = t["index"] - 1 + tab_url = t["entries"][i]["url"] if tab_url in urls: - w['tabs'].remove(t) + w["tabs"].remove(t) - log.info(f'Removing tab: {tab_url}') + log.info(f"Removing tab: {tab_url}") modified = True if modified: - with open(file_name_session, 'wb') as f: + with open(file_name_session, "wb") as f: mozlz4a.dumps_json(f, json_data) def close_duplicate_tabs( - file_name_session: Path, - log: logging.Logger, -): + file_name_session: Path, + log: logging.Logger, +) -> None: json_data = get_sessionstore(file_name_session) urls = get_tab_urls_from_sessionstore(json_data) @@ -72,43 +71,43 @@ def close_duplicate_tabs( if count > 1 } - for w in json_data['windows']: - for t in reversed(w['tabs']): - i = t['index'] - 1 - tab_url = t['entries'][i]['url'] + for w in json_data["windows"]: + for t in reversed(w["tabs"]): + i = t["index"] - 1 + tab_url = t["entries"][i]["url"] if need_to_delete.get(tab_url, 0) > 0: - w['tabs'].remove(t) + w["tabs"].remove(t) need_to_delete[tab_url] -= 1 - log.info(f'Removed duplicate: {tab_url}') + log.info(f"Removed duplicate: {tab_url}") if need_to_delete: - with open(file_name_session, 'wb') as f: + with open(file_name_session, "wb") as f: mozlz4a.dumps_json(f, json_data) def get_bookmark_urls( file_name_places: Path -) -> List[str]: +) -> list[str]: connect = sqlite3.connect(file_name_places) - sql = 'SELECT p.url FROM moz_bookmarks b, moz_places p WHERE p.id = b.fk' + sql = "SELECT p.url FROM moz_bookmarks b, moz_places p WHERE p.id = b.fk" return [place_url for place_url, in connect.execute(sql)] def close_bookmarks( - file_name_places: Path, - urls: List[str], - log: logging.Logger, -): + file_name_places: Path, + urls: list[str], + log: logging.Logger, +) -> None: with sqlite3.connect(file_name_places) as connect: for url in urls: - sql = 'SELECT id FROM moz_bookmarks WHERE fk = (SELECT id FROM moz_places WHERE url = ?)' + sql = "SELECT id FROM moz_bookmarks WHERE fk = (SELECT id FROM moz_places WHERE url = ?)" result = connect.execute(sql, [url]).fetchone() if not result: continue log.info(f"Deleted bookmark for url: {url}") - sql = 'DELETE FROM moz_bookmarks WHERE id = ?' + sql = "DELETE FROM moz_bookmarks WHERE id = ?" bookmark_id = result[0] connect.execute(sql, [bookmark_id]) diff --git a/firefox/common.py b/firefox/common.py index 4dae7f62d..84f3b8578 100644 --- a/firefox/common.py +++ b/firefox/common.py @@ -1,41 +1,43 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging import sys - +from logging.handlers import RotatingFileHandler from pathlib import Path DIR = Path(__file__).resolve().parent -DIR_LOGS = DIR / 'logs' +DIR_LOGS = DIR / "logs" def get_logger(file_name: str, dir_name=DIR_LOGS): dir_name = Path(dir_name).resolve() dir_name.mkdir(parents=True, exist_ok=True) - file_name = dir_name / (Path(file_name).resolve().name + '.log') + file_name = dir_name / (Path(file_name).resolve().name + ".log") - log = logging.getLogger(__name__) + log = logging.getLogger(__file__) log.setLevel(logging.DEBUG) - formatter = logging.Formatter('[%(asctime)s] %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s') + formatter = logging.Formatter( + "[%(asctime)s] %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s" + ) - fh = logging.FileHandler(file_name, encoding='utf-8') + fh = RotatingFileHandler( + file_name, maxBytes=10_000_000, backupCount=5, encoding="utf-8" + ) fh.setLevel(logging.DEBUG) + fh.setFormatter(formatter) + log.addHandler(fh) ch = logging.StreamHandler(stream=sys.stdout) ch.setLevel(logging.DEBUG) - - fh.setFormatter(formatter) ch.setFormatter(formatter) - - log.addHandler(fh) log.addHandler(ch) return log diff --git a/firefox/delete_tabs_and_bookmarks/close_tabs_with_bookmarks.py b/firefox/delete_tabs_and_bookmarks/close_tabs_with_bookmarks.py new file mode 100644 index 000000000..d7f951e3c --- /dev/null +++ b/firefox/delete_tabs_and_bookmarks/close_tabs_with_bookmarks.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +from pathlib import Path + +DIR = Path(__file__).resolve().parent +sys.path.append(str(DIR.parent)) + +import api +from common import get_logger +from config import file_name_places, file_name_session + + +log = get_logger(__file__, DIR / "logs") + +log.info("Start") + +tab_urls = api.get_tab_urls(file_name_session) +log.info(f"Tab urls: {len(tab_urls)}") + +bookmark_urls = api.get_bookmark_urls(file_name_places) +log.info(f"Bookmark urls: {len(bookmark_urls)}") + +api.close_tabs(file_name_session, bookmark_urls, log) + +log.info("Finish") diff --git a/firefox/delete_tabs_and_bookmarks/config.py b/firefox/delete_tabs_and_bookmarks/config.py index 7674b7e1e..7abc59fe2 100644 --- a/firefox/delete_tabs_and_bookmarks/config.py +++ b/firefox/delete_tabs_and_bookmarks/config.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os.path from pathlib import Path -profile_name = 'ef941a74.dev-edition-default' +profile_name = "ef941a74.dev-edition-default" # Example: C:\Users\<user>\AppData\Roaming\Mozilla\Firefox\Profiles\<profile>\places.sqlite -dir_profiles = Path(os.path.expandvars(r'%AppData%\Mozilla\Firefox\Profiles')) +dir_profiles = Path(os.path.expandvars(r"%AppData%\Mozilla\Firefox\Profiles")) dir_profile = dir_profiles / profile_name -file_name_places = dir_profile / 'places.sqlite' -file_name_session = dir_profile / 'sessionstore.jsonlz4' +file_name_places = dir_profile / "places.sqlite" +file_name_session = dir_profile / "sessionstore.jsonlz4" diff --git a/firefox/delete_tabs_and_bookmarks/delete_duplicate_tabs.py b/firefox/delete_tabs_and_bookmarks/delete_duplicate_tabs.py index fa3122a7d..595a5efc9 100644 --- a/firefox/delete_tabs_and_bookmarks/delete_duplicate_tabs.py +++ b/firefox/delete_tabs_and_bookmarks/delete_duplicate_tabs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -15,9 +15,9 @@ from config import file_name_session -log = get_logger(__file__, DIR / 'logs') +log = get_logger(__file__, DIR / "logs") -log.info('Start') +log.info("Start") api.close_duplicate_tabs(file_name_session, log) -log.info('Finish') +log.info("Finish") diff --git a/firefox/delete_tabs_and_bookmarks/delete_tabs_and_bookmarks_with_404.py b/firefox/delete_tabs_and_bookmarks/delete_tabs_and_bookmarks_with_404.py index 4a7647315..e1c597acf 100644 --- a/firefox/delete_tabs_and_bookmarks/delete_tabs_and_bookmarks_with_404.py +++ b/firefox/delete_tabs_and_bookmarks/delete_tabs_and_bookmarks_with_404.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging @@ -20,40 +20,40 @@ from config import file_name_places, file_name_session -log = get_logger(__file__, DIR / 'logs') +log = get_logger(__file__, DIR / "logs") def is_404( - url: str, - log: logging.Logger, + url: str, + log: logging.Logger, ) -> bool: while True: try: rs = requests.get(url) return rs.status_code == 404 except Exception as e: - log.error(f'Error {str(e)!r} on {url}') + log.error(f"Error {str(e)!r} on {url}") return False # Skip url finally: time.sleep(2) -log.info('Start') +log.info("Start") tab_urls = api.get_tab_urls(file_name_session) -log.info(f'Tab urls: {len(tab_urls)}') +log.info(f"Tab urls: {len(tab_urls)}") bookmark_urls = api.get_bookmark_urls(file_name_places) -log.info(f'Bookmark urls: {len(bookmark_urls)}') +log.info(f"Bookmark urls: {len(bookmark_urls)}") urls = set(tab_urls + bookmark_urls) count = len(urls) -log.info(f'Urls to processing: {count}') +log.info(f"Urls to processing: {count}") for i, url in enumerate(urls, 1): # Every 10% if i % (count // 10) == 0: - log.info(f'Processed {i / count:.0%}') + log.info(f"Processed {i / count:.0%}") if not is_404(url, log): continue @@ -61,4 +61,4 @@ def is_404( api.close_tabs(file_name_session, [url], log) api.close_bookmarks(file_name_places, [url], log) -log.info('Finish') +log.info("Finish") diff --git a/firefox/delete_tabs_and_bookmarks/show_stats.py b/firefox/delete_tabs_and_bookmarks/show_stats.py new file mode 100644 index 000000000..4e6e7d404 --- /dev/null +++ b/firefox/delete_tabs_and_bookmarks/show_stats.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +from pathlib import Path + +DIR = Path(__file__).resolve().parent +sys.path.append(str(DIR.parent)) + +import api +from config import file_name_places, file_name_session + + +tab_urls = api.get_tab_urls(file_name_session) +print(f"Tab urls: {len(tab_urls)}") + +bookmark_urls = api.get_bookmark_urls(file_name_places) +print(f"Bookmark urls: {len(bookmark_urls)}") \ No newline at end of file diff --git a/firefox/jsonlz4_mozLz4/mozlz4a.py b/firefox/jsonlz4_mozLz4/mozlz4a.py index 9f5c9906f..bbee46ea8 100644 --- a/firefox/jsonlz4_mozLz4/mozlz4a.py +++ b/firefox/jsonlz4_mozLz4/mozlz4a.py @@ -79,8 +79,8 @@ def loads_json(file_obj: BinaryIO) -> dict: return json.loads(data) -def dumps_json(file_obj: BinaryIO, json_data: dict): - data = json.dumps(json_data).encode('utf-8') +def dumps_json(file_obj: BinaryIO, json_data: dict) -> None: + data = json.dumps(json_data).encode("utf-8") compressed = compress_data(data) file_obj.write(compressed) @@ -88,33 +88,37 @@ def dumps_json(file_obj: BinaryIO, json_data: dict): if __name__ == "__main__": from argparse import ArgumentParser + argparser = ArgumentParser(description="MozLz4a compression/decompression utility") argparser.add_argument( - "-d", "--decompress", "--uncompress", + "-d", + "--decompress", + "--uncompress", action="store_true", - help="Decompress the input file instead of compressing it." - ) - argparser.add_argument( - "in_file", - help="Path to input file." - ) - argparser.add_argument( - "out_file", - help="Path to output file." + help="Decompress the input file instead of compressing it.", ) + argparser.add_argument("in_file", help="Path to input file.") + argparser.add_argument("out_file", help="Path to output file.") parsed_args = argparser.parse_args() try: in_file = open(parsed_args.in_file, "rb") except IOError as e: - print("Could not open input file `%s' for reading: %s" % (parsed_args.in_file, e), file=sys.stderr) + print( + "Could not open input file `%s' for reading: %s" % (parsed_args.in_file, e), + file=sys.stderr, + ) sys.exit(2) - + try: out_file = open(parsed_args.out_file, "wb") except IOError as e: - print("Could not open output file `%s' for writing: %s" % (parsed_args.out_file, e), file=sys.stderr) + print( + "Could not open output file `%s' for writing: %s" + % (parsed_args.out_file, e), + file=sys.stderr, + ) sys.exit(3) try: @@ -123,13 +127,19 @@ def dumps_json(file_obj: BinaryIO, json_data: dict): else: data = compress(in_file) except Exception as e: - print("Could not compress/decompress file `%s': %s" % (parsed_args.in_file, e), file=sys.stderr) + print( + "Could not compress/decompress file `%s': %s" % (parsed_args.in_file, e), + file=sys.stderr, + ) sys.exit(4) try: out_file.write(data) except IOError as e: - print("Could not write to output file `%s': %s" % (parsed_args.out_file, e), file=sys.stderr) + print( + "Could not write to output file `%s': %s" % (parsed_args.out_file, e), + file=sys.stderr, + ) sys.exit(5) finally: out_file.close() diff --git a/firefox/jsonlz4_mozLz4/test/test.py b/firefox/jsonlz4_mozLz4/test/test.py index 57286dc27..2179a9864 100644 --- a/firefox/jsonlz4_mozLz4/test/test.py +++ b/firefox/jsonlz4_mozLz4/test/test.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import io @@ -18,16 +18,16 @@ import mozlz4a -FILE_TEST = DIR / 'recovery.jsonlz4' +FILE_TEST = DIR / "recovery.jsonlz4" class TestAll(unittest.TestCase): - def test_1_exists_file(self): + def test_1_exists_file(self) -> None: self.assertTrue(FILE_TEST.exists()) self.assertTrue(FILE_TEST.read_bytes()) - def test_decompress_compress(self): - with open(FILE_TEST, 'rb') as f: + def test_decompress_compress(self) -> None: + with open(FILE_TEST, "rb") as f: expected_data = mozlz4a.decompress(f) bytes_io = io.BytesIO(expected_data) @@ -38,16 +38,16 @@ def test_decompress_compress(self): self.assertEqual(expected_data, data) - def test_compress_decompress_data(self): - expected_data = str(uuid.uuid4()).encode('utf-8') + def test_compress_decompress_data(self) -> None: + expected_data = str(uuid.uuid4()).encode("utf-8") compressed_data = mozlz4a.compress_data(expected_data) data = mozlz4a.decompress_data(compressed_data) self.assertEqual(expected_data, data) - def test_json(self): - with open(FILE_TEST, 'rb') as f: + def test_json(self) -> None: + with open(FILE_TEST, "rb") as f: expected_json_data = mozlz4a.loads_json(f) bytes_io = io.BytesIO() @@ -59,5 +59,5 @@ def test_json(self): self.assertEqual(expected_json_data, json_data) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/firefox/parse_profile__places.sqlite/dump_dns-shop_products.py b/firefox/parse_profile__places.sqlite/dump_dns-shop_products.py index f356eb262..66c0e6ad5 100644 --- a/firefox/parse_profile__places.sqlite/dump_dns-shop_products.py +++ b/firefox/parse_profile__places.sqlite/dump_dns-shop_products.py @@ -1,25 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import json import re import sqlite3 -from collections import OrderedDict - def process_title(title: str) -> str: - if title.startswith('Купить '): - title = title[len('Купить '):] + if title.startswith("Купить "): + title = title[len("Купить ") :] - match = re.search('(.+?) – купить в интернет магазине DNS', title) + match = re.search("(.+?) – купить в интернет магазине DNS", title) if match: title = match.group(1) - match = re.search('(.+?) в интернет магазине DNS', title) + match = re.search("(.+?) в интернет магазине DNS", title) if match: title = match.group(1) @@ -29,35 +27,34 @@ def process_title(title: str) -> str: tracked_products = [] # C:\Users\<user>\AppData\Roaming\Mozilla\Firefox\Profiles\<profile>\places.sqlite -with sqlite3.connect('places.sqlite') as connect: +with sqlite3.connect("places.sqlite") as connect: sql = "select url, title from moz_places where title is not null and url like '%//www.dns-shop.ru/product/%/%/'" for url, title in connect.execute(sql): if not title: continue - if url.startswith('http://'): - url = 'https://' + url[len('http://'):] + if url.startswith("http://"): + url = "https://" + url[len("http://") :] - if not url.startswith('https://www.dns-shop.ru/product/'): + if not url.startswith("https://www.dns-shop.ru/product/"): continue - if not re.fullmatch(r'https://www\.dns-shop\.ru/product/\w+/[^/]+/', url): + if not re.fullmatch(r"https://www\.dns-shop\.ru/product/\w+/[^/]+/", url): continue title = process_title(title) print(url, title) - tracked_products.append( - OrderedDict([ - ('title', title), - ('url', url), - ]) - ) + tracked_products.append({ + "title": title, + "url": url, + }) -tracked_products.sort(key=lambda x: x['title']) +tracked_products.sort(key=lambda x: x["title"]) json.dump( tracked_products, - open('tracked_products.json', 'w', encoding='utf-8'), - indent=4, ensure_ascii=False + open("tracked_products.json", "w", encoding="utf-8"), + indent=4, + ensure_ascii=False, ) diff --git a/flask-paginate__example/app.py b/flask-paginate__example/app.py index 0ac4ef4a5..57ed5fd7c 100644 --- a/flask-paginate__example/app.py +++ b/flask-paginate__example/app.py @@ -1,131 +1,143 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from __future__ import unicode_literals + import sqlite3 + +import click + from flask import Flask, render_template, g, current_app from flask_paginate import Pagination, get_page_args -import click + click.disable_unicode_literals_warning = True app = Flask(__name__) -app.config.from_pyfile('app.cfg') +app.config.from_pyfile("app.cfg") @app.before_request -def before_request(): - g.conn = sqlite3.connect('test.db') +def before_request() -> None: + g.conn = sqlite3.connect("test.db") g.conn.row_factory = sqlite3.Row g.cur = g.conn.cursor() @app.teardown_request -def teardown(error): - if hasattr(g, 'conn'): +def teardown(error) -> None: + if hasattr(g, "conn"): g.conn.close() -@app.route('/') +@app.route("/") def index(): - g.cur.execute('select count(*) from users') + g.cur.execute("select count(*) from users") total = g.cur.fetchone()[0] page, per_page, offset = get_page_args() - sql = 'select name from users order by name limit {}, {}'\ - .format(offset, per_page) + sql = f"select name from users order by name limit {offset}, {per_page}" g.cur.execute(sql) users = g.cur.fetchall() - pagination = get_pagination(page=page, - per_page=per_page, - total=total, - record_name='users', - format_total=True, - format_number=True, - ) - return render_template('index.html', users=users, - page=page, - per_page=per_page, - pagination=pagination, - ) - - -@app.route('/users/', defaults={'page': 1}) -@app.route('/users', defaults={'page': 1}) -@app.route('/users/page/<int:page>/') -@app.route('/users/page/<int:page>') + pagination = get_pagination( + page=page, + per_page=per_page, + total=total, + record_name="users", + format_total=True, + format_number=True, + ) + return render_template( + "index.html", + users=users, + page=page, + per_page=per_page, + pagination=pagination, + ) + + +@app.route("/users/", defaults={"page": 1}) +@app.route("/users", defaults={"page": 1}) +@app.route("/users/page/<int:page>/") +@app.route("/users/page/<int:page>") def users(page): - g.cur.execute('select count(*) from users') + g.cur.execute("select count(*) from users") total = g.cur.fetchone()[0] page, per_page, offset = get_page_args() - sql = 'select name from users order by name limit {}, {}'\ - .format(offset, per_page) + sql = f"select name from users order by name limit {offset}, {per_page}" g.cur.execute(sql) users = g.cur.fetchall() - pagination = get_pagination(page=page, - per_page=per_page, - total=total, - record_name='users', - format_total=True, - format_number=True, - ) - return render_template('index.html', users=users, - page=page, - per_page=per_page, - pagination=pagination, - active_url='users-page-url', - ) - - -@app.route('/search/<name>/') -@app.route('/search/<name>') + pagination = get_pagination( + page=page, + per_page=per_page, + total=total, + record_name="users", + format_total=True, + format_number=True, + ) + return render_template( + "index.html", + users=users, + page=page, + per_page=per_page, + pagination=pagination, + active_url="users-page-url", + ) + + +@app.route("/search/<name>/") +@app.route("/search/<name>") def search(name): """The function is used to test multi values url.""" - sql = 'select count(*) from users where name like ?' - args = ('%{}%'.format(name), ) + sql = "select count(*) from users where name like ?" + args = (f"%{name}%",) g.cur.execute(sql, args) total = g.cur.fetchone()[0] page, per_page, offset = get_page_args() - sql = 'select * from users where name like ? limit {}, {}' + sql = "select * from users where name like ? limit {}, {}" g.cur.execute(sql.format(offset, per_page), args) users = g.cur.fetchall() - pagination = get_pagination(page=page, - per_page=per_page, - total=total, - record_name='users', - ) - return render_template('index.html', users=users, - page=page, - per_page=per_page, - pagination=pagination, - ) + pagination = get_pagination( + page=page, + per_page=per_page, + total=total, + record_name="users", + ) + return render_template( + "index.html", + users=users, + page=page, + per_page=per_page, + pagination=pagination, + ) def get_css_framework(): - return current_app.config.get('CSS_FRAMEWORK', 'bootstrap3') + return current_app.config.get("CSS_FRAMEWORK", "bootstrap3") def get_link_size(): - return current_app.config.get('LINK_SIZE', 'sm') + return current_app.config.get("LINK_SIZE", "sm") def show_single_page_or_not(): - return current_app.config.get('SHOW_SINGLE_PAGE', False) + return current_app.config.get("SHOW_SINGLE_PAGE", False) def get_pagination(**kwargs): - kwargs.setdefault('record_name', 'records') - return Pagination(css_framework=get_css_framework(), - link_size=get_link_size(), - show_single_page=show_single_page_or_not(), - **kwargs - ) + kwargs.setdefault("record_name", "records") + return Pagination( + css_framework=get_css_framework(), + link_size=get_link_size(), + show_single_page=show_single_page_or_not(), + **kwargs + ) @click.command() -@click.option('--port', '-p', default=5000, help='listening port') -def run(port): +@click.option("--port", "-p", default=5000, help="listening port") +def run(port) -> None: app.run(debug=True, port=port) -if __name__ == '__main__': + +if __name__ == "__main__": run() diff --git a/flask-paginate__example/sql.py b/flask-paginate__example/sql.py index 3a8a055bc..21fc60f4b 100644 --- a/flask-paginate__example/sql.py +++ b/flask-paginate__example/sql.py @@ -1,44 +1,45 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from __future__ import unicode_literals import sqlite3 import click + click.disable_unicode_literals_warning = True -sql = '''create table if not exists users( + +sql = """create table if not exists users( id integer primary key autoincrement, name varchar(30) ) -''' +""" @click.group() -def cli(): +def cli() -> None: pass -@cli.command(short_help='initialize database and tables') -def init_db(): - conn = sqlite3.connect('test.db') +@cli.command(short_help="initialize database and tables") +def init_db() -> None: + conn = sqlite3.connect("test.db") cur = conn.cursor() cur.execute(sql) conn.commit() conn.close() -@cli.command(short_help='fill records to database') -@click.option('--total', '-t', default=300, help='fill data for example') -def fill_data(total): - conn = sqlite3.connect('test.db') +@cli.command(short_help="fill records to database") +@click.option("--total", "-t", default=300, help="fill data for example") +def fill_data(total) -> None: + conn = sqlite3.connect("test.db") cur = conn.cursor() for i in range(total): - cur.execute('insert into users (name) values (?)', ['name' + str(i)]) + cur.execute("insert into users (name) values (?)", ["name" + str(i)]) conn.commit() conn.close() -if __name__ == '__main__': +if __name__ == "__main__": cli() diff --git a/flask__jquery_easyui__examples/datagrid__games/main.py b/flask__jquery_easyui__examples/datagrid__games/main.py index 3a45e7ed8..09bace2da 100644 --- a/flask__jquery_easyui__examples/datagrid__games/main.py +++ b/flask__jquery_easyui__examples/datagrid__games/main.py @@ -1,26 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://www.jeasyui.com/tutorial/index.php # SOURCE: http://www.jeasyui.com/tutorial/app/crud.php -from flask import Flask, render_template, jsonify, request -app = Flask(__name__, static_folder='../static') - import logging -logging.basicConfig(level=logging.DEBUG) +import sqlite3 -import datetime as DT +from datetime import datetime -import sqlite3 +from flask import Flask, render_template, jsonify, request + + +app = Flask(__name__, static_folder="../static") +logging.basicConfig(level=logging.DEBUG) def create_connect(fields_as_dict=False): - connect = sqlite3.connect('games.sqlite') + connect = sqlite3.connect("games.sqlite") if fields_as_dict: connect.row_factory = sqlite3.Row @@ -33,27 +34,27 @@ def index(): return render_template("index.html") -@app.route("/get_games", methods=['POST', 'GET']) +@app.route("/get_games", methods=["POST", "GET"]) def get_games(): with create_connect(fields_as_dict=True) as connect: - sql = ''' + sql = """ SELECT id, name, price, append_date FROM game ORDER BY name - ''' + """ items = list(map(dict, connect.execute(sql).fetchall())) return jsonify(items) -@app.route("/save_game", methods=['POST', 'GET']) +@app.route("/save_game", methods=["POST", "GET"]) def save_game(): try: - name = request.form['name'] - price = request.form['price'] + name = request.form["name"] + price = request.form["price"] # 2017-06-03 21:21:17 - append_date = DT.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + append_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") with create_connect() as connect: sql = "INSERT INTO Game (name, price, append_date) VALUES (?,?,?)" @@ -61,9 +62,9 @@ def save_game(): connect.execute(sql, (name, price, append_date)) except Exception as e: - return jsonify({'errorMsg': f'Some errors occured: "{e}"'}) + return jsonify({"errorMsg": f'Some errors occured: "{e}"'}) - return jsonify({'success': True}) + return jsonify({"success": True}) # return jsonify({ # 'id': rs.lastrowid, # 'name': name, @@ -72,42 +73,42 @@ def save_game(): # }) -@app.route("/update_game", methods=['POST', 'GET']) +@app.route("/update_game", methods=["POST", "GET"]) def update_game(): try: - id_ = request.args['id'] - name = request.form['name'] - price = request.form['price'] + id_ = request.args["id"] + name = request.form["name"] + price = request.form["price"] with create_connect() as connect: sql = "UPDATE Game SET name = ?, price = ? WHERE id = ?" connect.execute(sql, (name, price, id_)) except Exception as e: - return jsonify({'errorMsg': f'Some errors occured: "{e}"'}) + return jsonify({"errorMsg": f'Some errors occured: "{e}"'}) - return jsonify({'success': True}) + return jsonify({"success": True}) -@app.route("/delete_game", methods=['POST', 'GET']) +@app.route("/delete_game", methods=["POST", "GET"]) def delete_game(): try: - id_ = request.form['id'] + id_ = request.form["id"] with create_connect() as connect: sql = "DELETE FROM Game WHERE id = ?" connect.execute(sql, (id_,)) except Exception as e: - return jsonify({'errorMsg': f'Some errors occured: "{e}"'}) + return jsonify({"errorMsg": f'Some errors occured: "{e}"'}) - return jsonify({'success': True}) + return jsonify({"success": True}) # TODO: диалог редактирования кнопку Ок показывает активной только если данные изменены -if __name__ == '__main__': +if __name__ == "__main__": # app.debug = True # Localhost diff --git a/flask__jquery_easyui__examples/datalist__basic__and__group/main.py b/flask__jquery_easyui__examples/datalist__basic__and__group/main.py index 65302bad3..8b1d6098d 100644 --- a/flask__jquery_easyui__examples/datalist__basic__and__group/main.py +++ b/flask__jquery_easyui__examples/datalist__basic__and__group/main.py @@ -1,23 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://www.jeasyui.com/demo/main/index.php?plugin=DataList&theme=default # SOURCE: http://www.jeasyui.com/documentation/datalist.php +import logging from flask import Flask, render_template_string, jsonify -app = Flask(__name__, static_folder='../static') -import logging + +app = Flask(__name__, static_folder="../static") logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template_string("""\ + return render_template_string( + """\ <html> <head> <meta content='text/html; charset=UTF-8' http-equiv='Content-Type'/> @@ -144,7 +146,8 @@ def index(): </script> </body> </html> - """) + """ + ) @app.route("/datalist_data.json") @@ -162,11 +165,11 @@ def get_datalist_data(): {"text": "Logitech Gaming Keyboard G110", "group": "Keyboard"}, {"text": "Nikon COOLPIX L26 16.1 MP", "group": "Camera"}, {"text": "Canon PowerShot A1300", "group": "Camera"}, - {"text": "Canon PowerShot A2300", "group": "Camera"} + {"text": "Canon PowerShot A2300", "group": "Camera"}, ]) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__jquery_easyui__examples/layout__datagrid__games__two_horizontal/main.py b/flask__jquery_easyui__examples/layout__datagrid__games__two_horizontal/main.py index a741cab1c..78a7f63c3 100644 --- a/flask__jquery_easyui__examples/layout__datagrid__games__two_horizontal/main.py +++ b/flask__jquery_easyui__examples/layout__datagrid__games__two_horizontal/main.py @@ -1,24 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://www.jeasyui.com/tutorial/index.php # SOURCE: http://www.jeasyui.com/tutorial/app/crud.php +import logging +import sqlite3 + from flask import Flask, render_template, jsonify, request -app = Flask(__name__, static_folder='../static') -import logging -logging.basicConfig(level=logging.DEBUG) -import sqlite3 +app = Flask(__name__, static_folder="../static") +logging.basicConfig(level=logging.DEBUG) def create_connect(fields_as_dict=False): - connect = sqlite3.connect('../datagrid__games/games.sqlite') + connect = sqlite3.connect("../datagrid__games/games.sqlite") if fields_as_dict: connect.row_factory = sqlite3.Row @@ -31,23 +32,23 @@ def index(): return render_template("index.html") -@app.route("/get_games", methods=['POST', 'GET']) +@app.route("/get_games", methods=["POST", "GET"]) def get_games(): - kind = request.args['kind'] + kind = request.args["kind"] with create_connect(fields_as_dict=True) as connect: - sql = ''' + sql = """ SELECT id, name, price, append_date FROM game WHERE kind = ? ORDER BY name - ''' + """ items = list(map(dict, connect.execute(sql, (kind,)).fetchall())) return jsonify(items) -if __name__ == '__main__': +if __name__ == "__main__": # app.debug = True # Localhost diff --git a/flask__jquery_easyui__examples/set_locale/main.py b/flask__jquery_easyui__examples/set_locale/main.py index b16182e3a..64f9f7ff3 100644 --- a/flask__jquery_easyui__examples/set_locale/main.py +++ b/flask__jquery_easyui__examples/set_locale/main.py @@ -1,23 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://www.jeasyui.com/tutorial/index.php # SOURCE: http://www.jeasyui.com/tutorial/app/crud.php +import logging from flask import Flask, render_template_string -app = Flask(__name__, static_folder='../static') -import logging + +app = Flask(__name__, static_folder="../static") logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template_string("""\ + return render_template_string( + """\ <html> <head> <meta content='text/html; charset=UTF-8' http-equiv='Content-Type'/> @@ -45,10 +47,11 @@ def index(): <div class="easyui-calendar" style="width:250px;height:250px;"></div> </body> </html> - """) + """ + ) -if __name__ == '__main__': +if __name__ == "__main__": # app.debug = True # Localhost diff --git a/flask__jquery_easyui__examples/set_theme/main.py b/flask__jquery_easyui__examples/set_theme/main.py index a6b5661e9..df878dac3 100644 --- a/flask__jquery_easyui__examples/set_theme/main.py +++ b/flask__jquery_easyui__examples/set_theme/main.py @@ -1,22 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://www.jeasyui.com/tutorial/ +import logging from flask import Flask, render_template_string, request, jsonify -app = Flask(__name__, static_folder='../static') -import logging + +app = Flask(__name__, static_folder="../static") logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template_string("""\ + return render_template_string( + """\ <html> <head> <meta content='text/html; charset=UTF-8' http-equiv='Content-Type'/> @@ -52,21 +54,25 @@ def index(): </iframe> </body> </html> - """) + """ + ) @app.route("/get_html") def get_html(): - theme = request.args.get('theme', 'default') + theme = request.args.get("theme", "default") - return render_template_string("""\ + return render_template_string( + """\ <html> <head> <meta content='text/html; charset=UTF-8' http-equiv='Content-Type'/> <title>set_theme

{{ theme }}

- + @@ -97,26 +103,111 @@ def get_html(): - """, theme=theme) + """, + theme=theme, + ) @app.route("/datagrid_data1.json") def get_datalist_data(): - return jsonify({"total": 28, "rows": [ - {"productid": "FI-SW-01", "productname": "Koi", "unitcost": "10.00", "status": "P", "listprice": "36.50", "attr1": "Large", "itemid": "EST-1"}, - {"productid": "K9-DL-01", "productname": "Dalmation", "unitcost": "12.00", "status": "P", "listprice": "18.50", "attr1": "Spotted Adult Female", "itemid": "EST-10"}, - {"productid": "RP-SN-01", "productname": "Rattlesnake", "unitcost": "12.00", "status": "P", "listprice": "38.50", "attr1": "Venomless", "itemid": "EST-11"}, - {"productid": "RP-SN-01", "productname": "Rattlesnake", "unitcost": "12.00", "status": "P", "listprice": "26.50", "attr1": "Rattleless", "itemid": "EST-12"}, - {"productid": "RP-LI-02", "productname": "Iguana", "unitcost": "12.00", "status": "P", "listprice": "35.50", "attr1": "Green Adult", "itemid": "EST-13"}, - {"productid": "FL-DSH-01", "productname": "Manx", "unitcost": "12.00", "status": "P", "listprice": "158.50", "attr1": "Tailless", "itemid": "EST-14"}, - {"productid": "FL-DSH-01", "productname": "Manx", "unitcost": "12.00", "status": "P", "listprice": "83.50", "attr1": "With tail", "itemid": "EST-15"}, - {"productid": "FL-DLH-02", "productname": "Persian", "unitcost": "12.00", "status": "P", "listprice": "23.50", "attr1": "Adult Female", "itemid": "EST-16"}, - {"productid": "FL-DLH-02", "productname": "Persian", "unitcost": "12.00", "status": "P", "listprice": "89.50", "attr1": "Adult Male", "itemid": "EST-17"}, - {"productid": "AV-CB-01", "productname": "Amazon Parrot", "unitcost": "92.00", "status": "P", "listprice": "63.50", "attr1": "Adult Male", "itemid": "EST-18"} - ]}) - - -if __name__ == '__main__': + return jsonify({ + "total": 28, + "rows": [ + { + "productid": "FI-SW-01", + "productname": "Koi", + "unitcost": "10.00", + "status": "P", + "listprice": "36.50", + "attr1": "Large", + "itemid": "EST-1", + }, + { + "productid": "K9-DL-01", + "productname": "Dalmation", + "unitcost": "12.00", + "status": "P", + "listprice": "18.50", + "attr1": "Spotted Adult Female", + "itemid": "EST-10", + }, + { + "productid": "RP-SN-01", + "productname": "Rattlesnake", + "unitcost": "12.00", + "status": "P", + "listprice": "38.50", + "attr1": "Venomless", + "itemid": "EST-11", + }, + { + "productid": "RP-SN-01", + "productname": "Rattlesnake", + "unitcost": "12.00", + "status": "P", + "listprice": "26.50", + "attr1": "Rattleless", + "itemid": "EST-12", + }, + { + "productid": "RP-LI-02", + "productname": "Iguana", + "unitcost": "12.00", + "status": "P", + "listprice": "35.50", + "attr1": "Green Adult", + "itemid": "EST-13", + }, + { + "productid": "FL-DSH-01", + "productname": "Manx", + "unitcost": "12.00", + "status": "P", + "listprice": "158.50", + "attr1": "Tailless", + "itemid": "EST-14", + }, + { + "productid": "FL-DSH-01", + "productname": "Manx", + "unitcost": "12.00", + "status": "P", + "listprice": "83.50", + "attr1": "With tail", + "itemid": "EST-15", + }, + { + "productid": "FL-DLH-02", + "productname": "Persian", + "unitcost": "12.00", + "status": "P", + "listprice": "23.50", + "attr1": "Adult Female", + "itemid": "EST-16", + }, + { + "productid": "FL-DLH-02", + "productname": "Persian", + "unitcost": "12.00", + "status": "P", + "listprice": "89.50", + "attr1": "Adult Male", + "itemid": "EST-17", + }, + { + "productid": "AV-CB-01", + "productname": "Amazon Parrot", + "unitcost": "92.00", + "status": "P", + "listprice": "63.50", + "attr1": "Adult Male", + "itemid": "EST-18", + }, + ], + }) + + +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/3_ways_to_get_table/ajax__json/main.py b/flask__webservers/3_ways_to_get_table/ajax__json/main.py index 5a5b15d82..aeb481969 100644 --- a/flask__webservers/3_ways_to_get_table/ajax__json/main.py +++ b/flask__webservers/3_ways_to_get_table/ajax__json/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging @@ -10,7 +10,7 @@ from flask import Flask, render_template_string, jsonify # Для импорта common.py -sys.path.append('..') +sys.path.append("..") from common import generate_table @@ -21,7 +21,8 @@ @app.route("/") def index(): - return render_template_string("""\ + return render_template_string( + """\ @@ -95,7 +96,8 @@ def index(): - """) + """ + ) @app.route("/get_table") @@ -105,7 +107,7 @@ def get_table(): return jsonify(items) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/3_ways_to_get_table/common.py b/flask__webservers/3_ways_to_get_table/common.py index 745c7f49e..879f6dd02 100644 --- a/flask__webservers/3_ways_to_get_table/common.py +++ b/flask__webservers/3_ways_to_get_table/common.py @@ -1,13 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import List - - -def generate_table(size=20) -> List[List[str]]: +def generate_table(size=20) -> list[list[str]]: items = [[str(i) for i in range(size + 1)]] for i in range(1, size + 1): @@ -18,13 +15,13 @@ def generate_table(size=20) -> List[List[str]]: items.append(row) - items[0][0] = '' + items[0][0] = "" return items -if __name__ == '__main__': +if __name__ == "__main__": print(generate_table(5)) print(generate_table(2)) - assert generate_table(2) == [['', '1', '2'], ['1', '1', '2'], ['2', '2', '4']] + assert generate_table(2) == [["", "1", "2"], ["1", "1", "2"], ["2", "2", "4"]] diff --git a/flask__webservers/3_ways_to_get_table/html__script/main.py b/flask__webservers/3_ways_to_get_table/html__script/main.py index bc6739cc9..ade936069 100644 --- a/flask__webservers/3_ways_to_get_table/html__script/main.py +++ b/flask__webservers/3_ways_to_get_table/html__script/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import json @@ -11,7 +11,7 @@ from flask import Flask, render_template_string # Для импорта common.py -sys.path.append('..') +sys.path.append("..") from common import generate_table @@ -25,7 +25,8 @@ def index(): items = generate_table(10) items = json.dumps(items, ensure_ascii=False) - return render_template_string("""\ + return render_template_string( + """\ @@ -84,10 +85,12 @@ def index(): - """, items=items) + """, + items=items, + ) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/3_ways_to_get_table/html__tag__table/main.py b/flask__webservers/3_ways_to_get_table/html__tag__table/main.py index 503070cc2..2a572ff07 100644 --- a/flask__webservers/3_ways_to_get_table/html__tag__table/main.py +++ b/flask__webservers/3_ways_to_get_table/html__tag__table/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging @@ -10,7 +10,7 @@ from flask import Flask, render_template_string # Для импорта common.py -sys.path.append('..') +sys.path.append("..") from common import generate_table @@ -23,7 +23,8 @@ def index(): items = generate_table(10) - return render_template_string("""\ + return render_template_string( + """\ @@ -70,10 +71,12 @@ def index(): - """, items=items) + """, + items=items, + ) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/ajax_and_jquery/main.py b/flask__webservers/ajax_and_jquery/main.py index c460506fe..544ec2e8b 100644 --- a/flask__webservers/ajax_and_jquery/main.py +++ b/flask__webservers/ajax_and_jquery/main.py @@ -1,19 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging +from datetime import datetime + from flask import Flask, render_template_string, request, jsonify -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -78,10 +82,11 @@ def index():

-''') +""" + ) -@app.route("/post_method", methods=['POST']) +@app.route("/post_method", methods=["POST"]) def post_method(): data = request.get_json() print(data) @@ -91,7 +96,6 @@ def post_method(): @app.route("/get_method") def get_method(): - from datetime import datetime data = datetime.today() print(data) diff --git a/flask__webservers/ajax_recursive_ul_from_json/main.py b/flask__webservers/ajax_recursive_ul_from_json/main.py index 0d353a2f1..4d29c35a1 100644 --- a/flask__webservers/ajax_recursive_ul_from_json/main.py +++ b/flask__webservers/ajax_recursive_ul_from_json/main.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging - from flask import Flask, jsonify, render_template @@ -36,15 +35,15 @@ "children": [ { "title": "Потомок Потомка 1", - "url": "http://domain.com/url2/child1/child2/" + "url": "http://domain.com/url2/child1/child2/", }, { "title": "Потомок Потомка 2", - "url": "http://domain.com/url2/child1/child3/" - } - ] - } - ] + "url": "http://domain.com/url2/child1/child3/", + }, + ], + }, + ], }, { "title": "Родитель 3", @@ -53,9 +52,9 @@ ] -@app.route('/') +@app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") @app.route("/json/") @@ -63,7 +62,7 @@ def get_json(): return jsonify(DATA) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/bootstrap_4__examples/clickable_table/main.py b/flask__webservers/bootstrap_4__examples/clickable_table/main.py index 008429e7d..3b76e8f9e 100644 --- a/flask__webservers/bootstrap_4__examples/clickable_table/main.py +++ b/flask__webservers/bootstrap_4__examples/clickable_table/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/twbs/bootstrap @@ -13,17 +13,17 @@ from flask import Flask, render_template -app = Flask(__name__, static_folder='../_static') +app = Flask(__name__, static_folder="../_static") logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/bootstrap_4__examples/grid__layout/main.py b/flask__webservers/bootstrap_4__examples/grid__layout/main.py index f7dc24930..1fb24211a 100644 --- a/flask__webservers/bootstrap_4__examples/grid__layout/main.py +++ b/flask__webservers/bootstrap_4__examples/grid__layout/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/twbs/bootstrap @@ -13,17 +13,17 @@ from flask import Flask, render_template -app = Flask(__name__, static_folder='../_static') +app = Flask(__name__, static_folder="../_static") logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/bootstrap_4__examples/shadow/main.py b/flask__webservers/bootstrap_4__examples/shadow/main.py index 4d0bb2ea9..9304065bd 100644 --- a/flask__webservers/bootstrap_4__examples/shadow/main.py +++ b/flask__webservers/bootstrap_4__examples/shadow/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/twbs/bootstrap @@ -13,17 +13,17 @@ from flask import Flask, render_template -app = Flask(__name__, static_folder='../_static') +app = Flask(__name__, static_folder="../_static") logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/bootstrap_4__examples/table/main.py b/flask__webservers/bootstrap_4__examples/table/main.py index 008429e7d..3b76e8f9e 100644 --- a/flask__webservers/bootstrap_4__examples/table/main.py +++ b/flask__webservers/bootstrap_4__examples/table/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/twbs/bootstrap @@ -13,17 +13,17 @@ from flask import Flask, render_template -app = Flask(__name__, static_folder='../_static') +app = Flask(__name__, static_folder="../_static") logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/bootstrap_4__examples/tabs/main.py b/flask__webservers/bootstrap_4__examples/tabs/main.py index 385f08b12..554e3bb78 100644 --- a/flask__webservers/bootstrap_4__examples/tabs/main.py +++ b/flask__webservers/bootstrap_4__examples/tabs/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/twbs/bootstrap @@ -13,17 +13,17 @@ from flask import Flask, render_template -app = Flask(__name__, static_folder='../_static') +app = Flask(__name__, static_folder="../_static") logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/bootstrap_4__examples/tags__badges__game_genres_example/main.py b/flask__webservers/bootstrap_4__examples/tags__badges__game_genres_example/main.py index 3bccd06b2..214eecaf7 100644 --- a/flask__webservers/bootstrap_4__examples/tags__badges__game_genres_example/main.py +++ b/flask__webservers/bootstrap_4__examples/tags__badges__game_genres_example/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/twbs/bootstrap @@ -15,7 +15,7 @@ from flask import Flask, render_template -app = Flask(__name__, static_folder='../_static') +app = Flask(__name__, static_folder="../_static") logging.basicConfig(level=logging.DEBUG) @@ -80,10 +80,10 @@ def index(): ] columns = ["NAME", "URL", "GENRES"] - return render_template('index.html', columns=columns, items=items) + return render_template("index.html", columns=columns, items=items) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/bootstrap_4__toggle_switch__examples/main.py b/flask__webservers/bootstrap_4__toggle_switch__examples/main.py index 7db9c9ee2..ed308867d 100644 --- a/flask__webservers/bootstrap_4__toggle_switch__examples/main.py +++ b/flask__webservers/bootstrap_4__toggle_switch__examples/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/twbs/bootstrap @@ -20,10 +20,10 @@ @app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/caching_response.py b/flask__webservers/caching_response.py new file mode 100644 index 000000000..ac2b6128e --- /dev/null +++ b/flask__webservers/caching_response.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime + +# pip install flask==2.3.3 +from flask import Flask, render_template_string + +# pip install flask-caching=2.0.2 +from flask_caching import Cache + + +config = { + "DEBUG": True, # some Flask specific configs + "CACHE_TYPE": "SimpleCache", # Flask-Caching related configs + "CACHE_DEFAULT_TIMEOUT": 300, +} + +app = Flask(__name__) + +# tell Flask to use the above defined config +app.config.from_mapping(config) +cache = Cache(app) + + +@app.route("/") +@cache.cached(timeout=50) +def index(): + # SOURCE: https://stackoverflow.com/a/55050637/5909792 + return render_template_string( + """ + + + + + +
+

{{ text }}

+
+ + + """, + text=str(datetime.now()), + ) + + +if __name__ == "__main__": + # Localhost + # port=0 -- random free port + # app.run(port=0) + app.run(port=50000) + + # # Public IP + # app.run(host='0.0.0.0') diff --git a/flask__webservers/chart_js/radar_chart__DarkSouls__builds_stats/main.py b/flask__webservers/chart_js/radar_chart__DarkSouls__builds_stats/main.py index bb091f14a..4a32996f5 100644 --- a/flask__webservers/chart_js/radar_chart__DarkSouls__builds_stats/main.py +++ b/flask__webservers/chart_js/radar_chart__DarkSouls__builds_stats/main.py @@ -1,54 +1,63 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging from flask import Flask, render_template -app = Flask(__name__, static_folder='../static') +app = Flask(__name__, static_folder="../static") logging.basicConfig(level=logging.DEBUG) -title = 'chart_js/radar_chart__DarkSouls__builds_stats' -labels = ['Vitality', 'Attunement', 'Endurance', 'Strength', 'Dexterity', 'Resistance', 'Intelligence', 'Faith'] +title = "chart_js/radar_chart__DarkSouls__builds_stats" +labels = [ + "Vitality", + "Attunement", + "Endurance", + "Strength", + "Dexterity", + "Resistance", + "Intelligence", + "Faith", +] items = [ { "name": "Cult Cleric Regen", "url": "https://darksouls.wiki.fextralife.com/Cult+Cleric+Regen", "color": "rgb(255, 99, 132)", "stats": [95, 12, 30, 16, 13, 10, 9, 16], - "hidden": 'true', + "hidden": "true", }, { "name": "Holy Dragon Knightess", "url": "https://darksouls.wiki.fextralife.com/Holy+Dragon+Knightess", "color": "rgb(255, 159, 64)", "stats": [26, 14, 36, 16, 10, 12, 8, 60], - "hidden": 'false', + "hidden": "false", }, { "name": "Royal Tank", "url": "https://darksouls.wiki.fextralife.com/Royal+Tank", "color": "rgb(75, 192, 192)", "stats": [50, 8, 40, 46, 10, 11, 8, 10], - "hidden": 'false', + "hidden": "false", }, { "name": "Vergil Style (Wanderer Style)", "url": "https://darksouls.wiki.fextralife.com/Vergil+Style", "color": "rgb(54, 162, 235)", "stats": [31, 12, 40, 16, 40, 12, 24, 8], - "hidden": 'false', + "hidden": "false", }, { "name": "Vergil Style (Thief Style)", "url": "https://darksouls.wiki.fextralife.com/Vergil+Style", "color": "rgb(153, 102, 255)", "stats": [28, 12, 40, 16, 40, 10, 24, 11], - "hidden": 'false', + "hidden": "false", }, ] @@ -56,16 +65,14 @@ @app.route("/") def index(): return render_template( - 'index.html', + "index.html", title=title, - title_chart='DarkSouls builds stats', + title_chart="DarkSouls builds stats", labels=labels, items=items, ) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True - app.run( - port=5000 - ) + app.run(port=5000) diff --git a/flask__webservers/common.py b/flask__webservers/common.py deleted file mode 100644 index 7d2556fcd..000000000 --- a/flask__webservers/common.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import sys -from pathlib import Path - - -DIR = Path(__file__).resolve().parent - -sys.path.append(str(DIR.parent)) - -from human_byte_size import sizeof_fmt diff --git a/flask__webservers/cookies/main.py b/flask__webservers/cookies/main.py index a472c803d..e6ddcd2d3 100644 --- a/flask__webservers/cookies/main.py +++ b/flask__webservers/cookies/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging @@ -17,7 +17,7 @@ def get_cookies(): return jsonify(request.cookies) -@app.route("/set-cookies", methods=['POST']) +@app.route("/set-cookies", methods=["POST"]) def set_cookies(): rs = make_response(jsonify(dict(ok=True))) @@ -27,7 +27,7 @@ def set_cookies(): return rs -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True app.run( port=5001, diff --git a/flask__webservers/cookies/test.py b/flask__webservers/cookies/test.py index 138f970ac..5395d9602 100644 --- a/flask__webservers/cookies/test.py +++ b/flask__webservers/cookies/test.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import requests @@ -10,7 +10,7 @@ session = requests.Session() -rs = session.get('http://127.0.0.1:5001/get-cookies') +rs = session.get("http://127.0.0.1:5001/get-cookies") print(rs, rs.url) print(rs.headers) print(rs.cookies) @@ -24,7 +24,7 @@ print() -rs = session.post('http://127.0.0.1:5001/set-cookies', params=dict(a=123, b=3)) +rs = session.post("http://127.0.0.1:5001/set-cookies", params=dict(a=123, b=3)) print(rs, rs.url) print(rs.headers) print(rs.cookies) @@ -38,7 +38,7 @@ print() -rs = session.get('http://127.0.0.1:5001/get-cookies') +rs = session.get("http://127.0.0.1:5001/get-cookies") print(rs, rs.url) print(rs.headers) print(rs.cookies) diff --git a/flask__webservers/cors__hello_world.py b/flask__webservers/cors__hello_world.py new file mode 100644 index 000000000..f771cddde --- /dev/null +++ b/flask__webservers/cors__hello_world.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install flask==2.3.3 +from flask import Flask + +# pip install flask-cors==4.0.0 +from flask_cors import CORS + + +app = Flask(__name__) +CORS(app) + + +@app.route("/") +def index() -> str: + return "Hello World!" + + +if __name__ == "__main__": + app.debug = True + + # Localhost + # port=0 -- random free port + # app.run(port=0) + app.run(port=50000) + + # # Public IP + # app.run(host='0.0.0.0') diff --git a/flask__webservers/custom_error_page/main.py b/flask__webservers/custom_error_page/main.py new file mode 100644 index 000000000..64c67879b --- /dev/null +++ b/flask__webservers/custom_error_page/main.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://flask.palletsprojects.com/en/3.0.x/errorhandling/ + + +import logging + +from flask import Flask, render_template +from werkzeug.exceptions import HTTPException + + +app = Flask(__name__) +logging.basicConfig(level=logging.DEBUG) + + +@app.errorhandler(404) +def page_not_found(e): + # note that we set the 404 status explicitly + return render_template('errors/404.html'), 404 + + +@app.errorhandler(Exception) +def handle_exception(e): + # pass through HTTP errors + if isinstance(e, HTTPException): + return e + + # now you're handling non-HTTP exceptions only + return render_template("errors/500.html", e=e), 500 + + +@app.route("/") +def index(): + return render_template('index.html') + + +@app.route("/500") +def do_500() -> None: + 1/0 + + +if __name__ == "__main__": + app.run(port=5000) diff --git a/flask__webservers/custom_error_page/static/images/404.jpg b/flask__webservers/custom_error_page/static/images/404.jpg new file mode 100644 index 000000000..76a486e4a Binary files /dev/null and b/flask__webservers/custom_error_page/static/images/404.jpg differ diff --git a/flask__webservers/custom_error_page/static/images/500.jpg b/flask__webservers/custom_error_page/static/images/500.jpg new file mode 100644 index 000000000..f9edf60a6 Binary files /dev/null and b/flask__webservers/custom_error_page/static/images/500.jpg differ diff --git a/flask__webservers/custom_error_page/templates/base.html b/flask__webservers/custom_error_page/templates/base.html new file mode 100644 index 000000000..67f2c8af6 --- /dev/null +++ b/flask__webservers/custom_error_page/templates/base.html @@ -0,0 +1,11 @@ + + + + + + {% block title %}{% endblock %} + + + {% block content %}{% endblock %} + + diff --git a/flask__webservers/custom_error_page/templates/errors/404.html b/flask__webservers/custom_error_page/templates/errors/404.html new file mode 100644 index 000000000..40823bc07 --- /dev/null +++ b/flask__webservers/custom_error_page/templates/errors/404.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} + +{% block title %}Page Not Found{% endblock %} + +{% block content %} +

Page Not Found

+

What you were looking for is just not there.

+ +

go somewhere nice

+{% endblock %} diff --git a/flask__webservers/custom_error_page/templates/errors/500.html b/flask__webservers/custom_error_page/templates/errors/500.html new file mode 100644 index 000000000..01bd13206 --- /dev/null +++ b/flask__webservers/custom_error_page/templates/errors/500.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} + +{% block title %}Internal Server Error{% endblock %} + +{% block content %} +

Internal Server Error

+ +

Something unexpected happened... Please try again later

+

Error: {{ e }}

+{% endblock %} diff --git a/flask__webservers/custom_error_page/templates/index.html b/flask__webservers/custom_error_page/templates/index.html new file mode 100644 index 000000000..01cc057a1 --- /dev/null +++ b/flask__webservers/custom_error_page/templates/index.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} + +{% block title %}Start page{% endblock %} + +{% block content %} +

Start page

+

Go 404

+

Go 500

+{% endblock %} diff --git a/flask__webservers/filter-sort-paginate-on-server/items.json b/flask__webservers/filter-sort-paginate-on-server/items.json new file mode 100644 index 000000000..bc8789bd8 --- /dev/null +++ b/flask__webservers/filter-sort-paginate-on-server/items.json @@ -0,0 +1,302 @@ +[ + { + "id": 1, + "name": "Preston", + "description": "Magna commodo deserunt culpa anim anim exercitation reprehenderit officia occaecat consectetur nisi est ut. Tempor minim aute nostrud sunt do labore cillum. Consequat dolore do pariatur pariatur fugiat nulla anim exercitation culpa nisi qui dolor dolore magna.\r\n", + "command": "Autograte-prestonherrera@autograte.com" + }, + { + "id": 2, + "name": "Fay", + "description": "Elit laboris quis sint magna labore magna ullamco anim excepteur. Duis incididunt excepteur ut labore aliquip do. Aute sint do excepteur aliquip mollit proident fugiat sint. Mollit labore ex esse consectetur incididunt commodo in. Occaecat commodo eiusmod ad enim eiusmod consectetur anim cupidatat do duis est elit. Nisi occaecat est dolor dolor officia anim irure excepteur magna aute mollit.\r\n", + "command": "Liquidoc-fayherrera@liquidoc.com" + }, + { + "id": 3, + "name": "Dena", + "description": "Deserunt consequat Lorem adipisicing duis minim. Irure pariatur occaecat voluptate consectetur pariatur sint commodo excepteur incididunt esse amet id. Elit commodo nulla enim aliqua mollit ullamco duis anim consequat. Culpa sit ex nostrud fugiat reprehenderit fugiat. Incididunt enim ullamco esse velit consectetur. Esse cillum anim ullamco occaecat commodo enim aute laboris.\r\n", + "command": "Lingoage-denaherrera@lingoage.com" + }, + { + "id": 4, + "name": "Jacobs", + "description": "Voluptate anim amet dolor deserunt nostrud cillum magna velit enim qui. Dolore magna aliquip nisi eu exercitation eu eu Lorem. Ea occaecat culpa sit qui cupidatat veniam. Exercitation minim minim anim ad nulla veniam et velit. Nostrud laborum labore culpa do ipsum et cupidatat. Cupidatat officia adipisicing sunt aliquip id nisi deserunt.\r\n", + "command": "Imant-jacobsherrera@imant.com" + }, + { + "id": 5, + "name": "Richards", + "description": "Et eu laboris aute ipsum. Cupidatat consectetur dolor sint elit. Et quis laborum deserunt deserunt do cillum ipsum proident sunt laboris enim veniam. Sint id fugiat eu quis est dolore ut minim duis. Labore voluptate nulla adipisicing culpa proident.\r\n", + "command": "Octocore-richardsherrera@octocore.com" + }, + { + "id": 6, + "name": "Connie", + "description": "Officia culpa culpa proident fugiat dolor esse irure incididunt officia occaecat mollit amet. Ea exercitation reprehenderit proident laboris sint aute eiusmod voluptate. Mollit cupidatat exercitation laboris nulla sunt non ex.\r\n", + "command": "Circum-connieherrera@circum.com" + }, + { + "id": 7, + "name": "Lilia", + "description": "Aliquip sunt non do et nisi incididunt minim et. Nisi culpa elit in exercitation velit dolor cupidatat veniam consectetur mollit aliqua culpa. Ea irure commodo et cillum id. Sint pariatur eu cupidatat occaecat tempor excepteur do eiusmod sint.\r\n", + "command": "Petigems-liliaherrera@petigems.com" + }, + { + "id": 8, + "name": "Serena", + "description": "Lorem aliqua ad ut ut. Minim dolore incididunt sit enim incididunt consectetur. Ad cillum non sit sunt elit quis reprehenderit qui deserunt. Exercitation voluptate consectetur ullamco Lorem ipsum ut pariatur ea velit anim ex elit. Cupidatat proident reprehenderit ad aute in anim anim veniam ullamco mollit qui.\r\n", + "command": "Slumberia-serenaherrera@slumberia.com" + }, + { + "id": 9, + "name": "Cherie", + "description": "Labore incididunt mollit exercitation deserunt nostrud sint qui eu. Quis laboris labore ipsum cupidatat ea. Laboris consectetur ea Lorem deserunt tempor reprehenderit aliquip ipsum nostrud ex nisi veniam.\r\n", + "command": "Futurize-cherieherrera@futurize.com" + }, + { + "id": 10, + "name": "Lindsey", + "description": "Excepteur sit pariatur cupidatat veniam ullamco quis magna eu voluptate labore eiusmod magna tempor. Sint do sit in cillum id et ullamco aliquip nostrud occaecat. Consequat amet in nostrud adipisicing magna laborum duis labore fugiat culpa ut laborum. Non aliqua ea ex non. Nostrud quis ullamco nisi Lorem reprehenderit proident culpa fugiat.\r\n", + "command": "Aquoavo-lindseyherrera@aquoavo.com" + }, + { + "id": 11, + "name": "Nicole", + "description": "In est magna laboris velit sint ea. Quis tempor deserunt quis voluptate in proident adipisicing dolor exercitation laborum. Sit consequat reprehenderit cillum ut non ex aute sint. Lorem ea eu fugiat adipisicing nostrud cillum duis dolore incididunt nostrud nisi veniam aliqua occaecat.\r\n", + "command": "Strozen-nicoleherrera@strozen.com" + }, + { + "id": 12, + "name": "Joyner", + "description": "Dolore enim sunt adipisicing et reprehenderit. Incididunt laborum elit commodo duis sint Lorem fugiat dolore in cupidatat sunt commodo. Adipisicing do officia eiusmod adipisicing amet eu reprehenderit non incididunt excepteur ipsum sit. Et duis irure pariatur tempor laborum ad minim id enim fugiat exercitation aute mollit eu. Sint velit ipsum sint pariatur dolor aliquip sint irure adipisicing eu. Deserunt non eu dolor mollit irure elit consectetur voluptate sint ut dolore cillum do ullamco. Magna amet cillum est culpa aliquip occaecat eiusmod non est laborum non officia aliqua non.\r\n", + "command": "Rubadub-joynerherrera@rubadub.com" + }, + { + "id": 13, + "name": "Isabel", + "description": "Ad culpa esse mollit quis laborum deserunt consequat ad aliqua qui irure reprehenderit est mollit. Irure mollit cillum nostrud non. Ullamco laboris deserunt est et. Irure consectetur pariatur laboris in sunt aliqua ex reprehenderit duis laboris aliquip deserunt laborum. Pariatur ex veniam nulla ipsum amet excepteur qui dolor commodo. Incididunt aliqua nostrud et dolore nisi cupidatat dolore eu minim deserunt aute velit. Duis occaecat et duis commodo ex magna officia pariatur consectetur exercitation irure ullamco proident consectetur.\r\n", + "command": "Assurity-isabelherrera@assurity.com" + }, + { + "id": 14, + "name": "Jayne", + "description": "Ad cupidatat adipisicing occaecat ea magna mollit ex esse nostrud velit officia. Aliqua commodo enim commodo adipisicing cillum est in quis. Mollit sint ea adipisicing enim ea labore et. Cillum aliquip dolore elit aute sit sint incididunt exercitation dolore voluptate ut. Aliqua nostrud consectetur minim reprehenderit eu aliquip elit nulla consectetur nisi reprehenderit ullamco. Nostrud consectetur sunt in nostrud nostrud consectetur officia aliqua esse Lorem.\r\n", + "command": "Playce-jayneherrera@playce.com" + }, + { + "id": 15, + "name": "Tonia", + "description": "Eiusmod ex elit ullamco exercitation consectetur enim aliquip id qui dolor. Occaecat id et sit aute deserunt duis eiusmod mollit labore adipisicing. Voluptate consectetur dolore deserunt elit Lorem. Reprehenderit ex esse proident anim sunt qui pariatur in sit culpa.\r\n", + "command": "Fossiel-toniaherrera@fossiel.com" + }, + { + "id": 16, + "name": "Teresa", + "description": "Duis nostrud fugiat in duis laboris non irure quis. Non tempor reprehenderit eu incididunt occaecat exercitation incididunt excepteur deserunt cillum nulla deserunt elit sunt. Minim occaecat amet nostrud dolore Lorem labore incididunt tempor proident. Fugiat reprehenderit culpa quis pariatur eiusmod eu do aute eu sunt.\r\n", + "command": "Zosis-teresaherrera@zosis.com" + }, + { + "id": 17, + "name": "Nelda", + "description": "Nulla labore eiusmod aute aute mollit laborum mollit aliquip. Non qui id incididunt enim qui minim cupidatat aute proident est in nostrud. Id consequat eiusmod officia ex veniam elit occaecat veniam commodo eiusmod ullamco tempor. Pariatur exercitation sint sint anim nulla. Deserunt dolore enim cupidatat exercitation mollit elit magna magna Lorem mollit.\r\n", + "command": "Uneeq-neldaherrera@uneeq.com" + }, + { + "id": 18, + "name": "Farley", + "description": "Qui sit veniam nostrud consequat deserunt fugiat laboris occaecat. Laboris nisi sit velit velit aliqua ea labore amet sunt ullamco cillum. Laborum Lorem exercitation laboris consequat exercitation occaecat cupidatat ex elit officia aliqua deserunt culpa ea.\r\n", + "command": "Shopabout-farleyherrera@shopabout.com" + }, + { + "id": 19, + "name": "Porter", + "description": "Ipsum anim pariatur eiusmod sint sunt labore. Reprehenderit adipisicing laborum occaecat anim labore laboris mollit eiusmod minim et. Nisi dolor in dolor est nostrud. Commodo qui laborum ipsum occaecat esse est. Veniam velit mollit aute qui.\r\n", + "command": "Artiq-porterherrera@artiq.com" + }, + { + "id": 20, + "name": "Teri", + "description": "Lorem aute nostrud ipsum excepteur ipsum Lorem nulla eiusmod do. Eiusmod tempor nostrud commodo enim consectetur qui ut irure excepteur do duis reprehenderit amet tempor. Et in laboris exercitation anim sunt voluptate est cillum ullamco mollit reprehenderit anim ut.\r\n", + "command": "Omnigog-teriherrera@omnigog.com" + }, + { + "id": 21, + "name": "Miranda", + "description": "Ut dolore fugiat ex anim minim amet magna commodo mollit dolor excepteur reprehenderit laboris dolor. Amet consectetur anim reprehenderit do. Consectetur eu sit mollit culpa elit irure. Id exercitation ea nostrud cillum nostrud fugiat fugiat pariatur nulla culpa labore. Pariatur exercitation minim laboris cillum esse ullamco ut occaecat est. Excepteur officia officia officia consectetur tempor esse do. Nisi sit eiusmod ex anim ullamco non labore culpa.\r\n", + "command": "Comverges-mirandaherrera@comverges.com" + }, + { + "id": 22, + "name": "Hutchinson", + "description": "Et cupidatat aliquip culpa qui. Labore in sunt ut deserunt irure exercitation amet cupidatat aliqua nostrud velit sit aliquip ea. Ad aute consequat eiusmod non irure voluptate incididunt officia ipsum aliqua anim occaecat irure. Officia quis consectetur ea pariatur incididunt voluptate occaecat. Velit exercitation id mollit minim proident nostrud mollit adipisicing sint dolore.\r\n", + "command": "Zolavo-hutchinsonherrera@zolavo.com" + }, + { + "id": 23, + "name": "Woods", + "description": "Sunt sint laborum enim irure officia. Occaecat dolor velit esse adipisicing reprehenderit culpa laboris aute deserunt laboris exercitation. Aliqua elit ipsum reprehenderit laboris eu ad. Dolore mollit ullamco aute dolore dolore mollit dolore cillum incididunt aliquip qui tempor. Veniam ut labore in magna ullamco reprehenderit proident labore ea fugiat Lorem exercitation. Esse anim sint duis id eiusmod non eiusmod Lorem et. Pariatur sit cupidatat aliquip id reprehenderit minim nisi enim ipsum dolor cillum.\r\n", + "command": "Magnina-woodsherrera@magnina.com" + }, + { + "id": 24, + "name": "Minnie", + "description": "Velit elit cupidatat culpa culpa in commodo labore excepteur eu. Irure et dolore nulla adipisicing. Magna consectetur nisi do est veniam fugiat.\r\n", + "command": "Grupoli-minnieherrera@grupoli.com" + }, + { + "id": 25, + "name": "Faulkner", + "description": "Sunt aliquip quis quis consequat mollit aliqua laboris qui elit id duis et. Cupidatat officia excepteur anim non deserunt velit. Deserunt voluptate ex velit officia sit dolore minim tempor et. Et in dolor laboris dolore. Elit exercitation aliqua amet sint ut et cillum tempor proident incididunt sit. Dolore elit nisi proident anim exercitation tempor quis. Enim Lorem in laboris ipsum proident pariatur consectetur.\r\n", + "command": "Plasmosis-faulknerherrera@plasmosis.com" + }, + { + "id": 26, + "name": "Maude", + "description": "Consectetur esse Lorem id officia reprehenderit quis. Labore magna nisi deserunt sint officia cillum. Incididunt aliqua veniam officia incididunt elit mollit aliqua qui reprehenderit cillum anim. Qui tempor eu ex anim tempor amet minim dolore elit ipsum Lorem quis cupidatat. Reprehenderit velit quis aliquip Lorem incididunt.\r\n", + "command": "Jamnation-maudeherrera@jamnation.com" + }, + { + "id": 27, + "name": "Roberson", + "description": "Mollit sint aliqua veniam velit consectetur consequat. Ex fugiat labore reprehenderit in cupidatat nostrud adipisicing. Do sit nulla do nisi voluptate nostrud nostrud magna laboris.\r\n", + "command": "Kyagoro-robersonherrera@kyagoro.ru" + }, + { + "id": 28, + "name": "Debra", + "description": "Ex sit laboris duis velit culpa cupidatat nisi consectetur reprehenderit quis. Amet ea dolore irure nulla proident deserunt sint sint adipisicing pariatur sint elit. Non commodo aute Lorem cupidatat sit labore excepteur velit nostrud ut ea. Pariatur non qui sunt mollit amet veniam deserunt ullamco laborum est sint. Et minim fugiat irure dolor aliqua ut proident amet adipisicing culpa minim.\r\n", + "command": "Magneato-debraherrera@magneato.ru" + }, + { + "id": 29, + "name": "Pacheco", + "description": "Pariatur excepteur proident anim cillum dolore consectetur ipsum deserunt cupidatat cillum pariatur sunt officia laborum. Dolor voluptate culpa et elit pariatur ea. Voluptate velit ex commodo sunt exercitation ea nostrud quis elit est velit amet. Laboris est magna incididunt non sit minim culpa anim non proident occaecat irure veniam non. Ut quis laboris aliquip do tempor dolore ipsum reprehenderit deserunt deserunt. Do labore consectetur eu quis excepteur nulla anim consectetur ut dolore elit.\r\n", + "command": "Keengen-pachecoherrera@keengen.com" + }, + { + "id": 30, + "name": "Wyatt", + "description": "Commodo ex sint et amet tempor veniam culpa pariatur Lorem proident ut sint. Commodo anim occaecat anim in minim occaecat velit. Anim id id ea ex aliqua deserunt minim duis tempor ullamco voluptate esse. Duis mollit esse minim excepteur et consequat. Laborum voluptate culpa deserunt anim et nulla enim qui anim laborum anim exercitation. In dolor elit culpa velit ullamco aute anim Lorem excepteur esse magna Lorem exercitation.\r\n", + "command": "Newcube-wyattherrera@newcube.com" + }, + { + "id": 31, + "name": "Laurel", + "description": "Laborum exercitation aliqua occaecat labore. In velit ea fugiat elit eu quis sint duis elit officia irure enim proident tempor. Minim do quis mollit elit. Ipsum tempor laborum exercitation mollit ut qui tempor laboris magna fugiat qui sit aliqua quis. Elit velit aliquip ipsum esse quis anim pariatur anim irure minim. Veniam laborum id commodo et mollit Lorem aliqua laborum ipsum magna. Occaecat amet fugiat proident in.\r\n", + "command": "Escenta-laurelherrera@escenta.org" + }, + { + "id": 32, + "name": "Hopper", + "description": "Do consequat magna in quis quis. Veniam esse tempor velit ex irure. Minim reprehenderit excepteur sint mollit sit. Incididunt nisi velit minim ad anim nisi incididunt mollit culpa proident.\r\n", + "command": "Portalis-hopperherrera@portalis.com" + }, + { + "id": 33, + "name": "Guzman", + "description": "Lorem in ipsum elit eiusmod. Nostrud officia veniam voluptate culpa anim eu ex qui duis do quis ea quis aliquip. Do dolore aliqua qui enim sunt do exercitation non non esse nostrud eu. Aliqua enim consectetur sunt minim non. Eu anim sunt ipsum id aliquip labore laboris exercitation. Commodo cupidatat exercitation occaecat ea anim sint pariatur incididunt velit minim eu.\r\n", + "command": "Jimbies-guzmanherrera@jimbies.ru" + }, + { + "id": 34, + "name": "Amber", + "description": "Nostrud reprehenderit culpa veniam eu ipsum et id nostrud. Deserunt do aliquip exercitation enim ullamco incididunt fugiat in nostrud exercitation anim. Dolor cillum nisi nostrud ipsum reprehenderit excepteur quis adipisicing mollit voluptate consequat ea occaecat. Minim mollit non in fugiat magna voluptate.\r\n", + "command": "Zaggles-amberherrera@zaggles.ru" + }, + { + "id": 35, + "name": "Ruthie", + "description": "Deserunt nostrud dolor labore laboris. Elit pariatur non cillum minim veniam. Nisi incididunt cillum in consectetur enim nulla ea dolore pariatur. Velit magna in ex exercitation voluptate eiusmod velit eiusmod nisi in elit nisi cillum. Non do labore aute eiusmod ut et nulla commodo.\r\n", + "command": "Boilicon-ruthieherrera@boilicon.org" + }, + { + "id": 36, + "name": "Billie", + "description": "Consectetur minim pariatur duis dolor cupidatat laboris occaecat nisi veniam reprehenderit laborum veniam et. Officia ullamco fugiat proident cupidatat fugiat ut culpa minim sint. Elit eiusmod id labore voluptate sit tempor ea cupidatat aute voluptate anim. Do laboris dolor elit duis nulla aliquip incididunt amet duis magna dolore esse. Eiusmod eiusmod nisi enim aute aliquip do ea laborum duis voluptate nostrud deserunt officia.\r\n", + "command": "Geekko-billieherrera@geekko.com" + }, + { + "id": 37, + "name": "Lindsay", + "description": "Ea laboris exercitation tempor amet mollit in ea excepteur ut excepteur quis. Sit ipsum ipsum ullamco aliquip anim cillum veniam. Do incididunt excepteur duis ullamco aute quis elit minim ipsum.\r\n", + "command": "Rocklogic-lindsayherrera@rocklogic.org" + }, + { + "id": 38, + "name": "Mercer", + "description": "Mollit labore aliqua tempor pariatur veniam duis enim laborum. Ipsum pariatur ut do ullamco velit ea nisi adipisicing ullamco est ea elit laboris pariatur. Ex consequat do deserunt pariatur mollit ut ipsum incididunt aute sit proident reprehenderit eu.\r\n", + "command": "Satiance-mercerherrera@satiance.org" + }, + { + "id": 39, + "name": "Benson", + "description": "Proident est aliqua eu nisi tempor ut veniam irure. Sunt sit ex enim elit consequat eu officia cillum occaecat esse. Lorem id magna et veniam et ullamco adipisicing nostrud irure Lorem. Velit proident occaecat eiusmod ullamco nisi commodo. Amet velit ex commodo qui id exercitation amet fugiat reprehenderit velit magna consequat quis. Laborum minim exercitation esse veniam ad do qui duis mollit adipisicing voluptate nostrud cupidatat cupidatat. Reprehenderit ipsum ex nostrud do ex sit excepteur mollit aute.\r\n", + "command": "Lyria-bensonherrera@lyria.com" + }, + { + "id": 40, + "name": "Melisa", + "description": "Mollit esse sunt ullamco excepteur cillum non cupidatat laborum consequat enim ea. Amet quis incididunt cupidatat incididunt excepteur voluptate sit. Culpa proident laboris occaecat fugiat labore id eu aute magna ea irure commodo laborum voluptate. Ut excepteur esse sint culpa reprehenderit sit. Officia tempor elit dolore ex sint culpa. Nostrud ipsum et dolore aliquip magna laboris aliquip commodo qui consequat in commodo amet. Velit proident non esse reprehenderit excepteur consectetur eu irure.\r\n", + "command": "Genmy-melisaherrera@genmy.com" + }, + { + "id": 41, + "name": "Amber", + "description": "Et tempor cupidatat consequat veniam minim labore quis duis incididunt quis anim. Ut incididunt fugiat pariatur ipsum occaecat dolore ea aute id nisi commodo. Amet eiusmod proident dolore Lorem. Ut consequat officia sint nostrud est in occaecat esse.\r\n", + "command": "Asimiline-ellenherrera@asimiline.org" + }, + { + "id": 42, + "name": "Jocelyn", + "description": "Est anim mollit aliqua qui est cillum. Est voluptate eu laboris ea adipisicing occaecat nostrud aliquip exercitation cillum deserunt sit cillum. Cillum sunt fugiat sit veniam dolore velit minim cillum occaecat ut ex aliqua ipsum.\r\n", + "command": "Insurety-jocelynherrera@insurety.org" + }, + { + "id": 43, + "name": "Mccullough", + "description": "Ullamco aliqua et id et minim. Est amet deserunt adipisicing cupidatat ipsum quis tempor. Irure deserunt amet aliquip officia id. Quis deserunt dolore aute quis aliqua irure voluptate ex qui ad. Sint fugiat do nisi sunt ad laborum officia reprehenderit. Non commodo velit amet enim mollit occaecat tempor quis eiusmod ipsum ex commodo sunt.\r\n", + "command": "Straloy-mcculloughherrera@straloy.com" + }, + { + "id": 44, + "name": "Lisa", + "description": "Nostrud fugiat in commodo ex tempor. Veniam excepteur mollit eiusmod ut labore quis sint qui nisi. Voluptate elit labore nostrud est pariatur do amet commodo consectetur.\r\n", + "command": "Flyboyz-lisaherrera@flyboyz.com" + }, + { + "id": 45, + "name": "Abigail", + "description": "Deserunt nisi irure ex culpa ut eu commodo tempor proident qui. Magna nostrud amet qui ipsum et ipsum. Est cillum ullamco voluptate labore laborum quis ut et consectetur reprehenderit sint. Sint nostrud fugiat consectetur mollit nisi excepteur veniam excepteur tempor adipisicing dolor amet. Proident commodo exercitation dolore reprehenderit quis quis in aliquip est veniam sit quis ipsum nisi. Laborum ut pariatur in enim cupidatat enim deserunt adipisicing irure ut tempor non consequat aute. Ea cupidatat excepteur sunt eiusmod.\r\n", + "command": "Overfork-abigailherrera@overfork.com" + }, + { + "id": 46, + "name": "Estrada", + "description": "Aute qui nisi adipisicing qui ad laborum sit duis deserunt sint in consequat. Dolor irure mollit eu nostrud officia. Sit amet irure ea nisi in mollit amet ad laborum ea nulla ex tempor. Voluptate labore est exercitation proident ullamco magna magna pariatur. Fugiat enim eu officia ut ullamco quis officia proident commodo. Laborum incididunt consectetur eiusmod nulla qui. Anim commodo eiusmod sunt fugiat ea amet ex et in pariatur mollit mollit.\r\n", + "command": "Typhonica-estradaherrera@typhonica.com" + }, + { + "id": 47, + "name": "Amber", + "description": "Aute quis cillum id nostrud labore duis aliquip duis commodo officia irure amet Lorem. Esse voluptate amet cillum adipisicing amet esse nulla deserunt. Aliquip eu quis pariatur reprehenderit et occaecat excepteur ut do consequat consequat reprehenderit et. Aliqua minim exercitation consectetur laboris enim minim ex voluptate quis. Laboris pariatur dolore eiusmod Lorem nulla tempor reprehenderit quis nisi enim aute. Non velit sit pariatur officia minim laboris minim irure sunt irure ea ex anim culpa.\r\n", + "command": "Vinch-susanaherrera@vinch.com" + }, + { + "id": 48, + "name": "Joseph", + "description": "Elit et exercitation et officia ad mollit consectetur id laborum consectetur adipisicing sint quis. Aliqua deserunt eiusmod voluptate ex. Ipsum nulla aliquip occaecat in minim do in adipisicing sunt in minim.\r\n", + "command": "Jumpstack-josephherrera@jumpstack.ru" + }, + { + "id": 49, + "name": "Johnnie", + "description": "Aliqua Lorem in ea ea duis. Consectetur ex laborum non nulla enim magna elit qui exercitation commodo consectetur. Consequat irure mollit veniam consectetur tempor enim aliquip amet nulla aliquip excepteur reprehenderit. Reprehenderit excepteur eu nulla cillum excepteur nulla velit consequat anim. Amet ut aute et ut aliqua. Consequat nostrud duis in eiusmod quis consectetur ad nostrud nostrud do exercitation occaecat dolore qui. Tempor sint ad do consequat.\r\n", + "command": "Biohab-johnnieherrera@biohab.ru" + }, + { + "id": 50, + "name": "Amber", + "description": "Aliquip aliqua nostrud veniam est. Laborum ut et nostrud proident qui proident. Esse quis amet in culpa sint tempor proident consectetur velit incididunt reprehenderit. Eu sint exercitation qui do ullamco.\r\n", + "command": "Luxuria-estesherrera@luxuria.ru" + } +] \ No newline at end of file diff --git a/flask__webservers/filter-sort-paginate-on-server/main.py b/flask__webservers/filter-sort-paginate-on-server/main.py new file mode 100644 index 000000000..314282a7a --- /dev/null +++ b/flask__webservers/filter-sort-paginate-on-server/main.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# NOTE: https://datatables.net/manual/server-side + + +import json +import re +from functools import cmp_to_key +from pathlib import Path +from typing import Any + +from flask import Flask, render_template, jsonify, Response, request + +# pip install querystring-parser==1.2.4 +from querystring_parser import parser + + +def cmp(a: Any, b: Any) -> int: + return (a > b) - (a < b) + + +ITEMS = json.load(open("items.json", encoding="utf-8")) + + +app = Flask(__name__) + + +@app.route("/") +def index() -> str: + return render_template( + "index.html", + title=Path(__file__).resolve().parent.name, + ) + + +@app.route("/api/get_items") +def api_get_items() -> Response: + args: dict[str, Any] = parser.parse(request.query_string) + + search_value: str = args["search"]["value"] + has_search_regex: str = args["search"]["regex"] == "true" + + # TODO: Фильтрация может быть по отдельным столбцам в поле search + # { + # 0: {'data': 'id', 'name': 'id', 'searchable': 'true', 'orderable': 'true', 'search': {'value': '', 'regex': 'false'}}, + # 1: {'data': 'name', 'name': 'name', 'searchable': 'true', 'orderable': 'true', 'search': {'value': '', 'regex': 'false'}}, + # 2: {'data': 'description', 'name': 'description', 'searchable': 'true', 'orderable': 'true', 'search': {'value': '', 'regex': 'false'}}, + # 3: {'data': 'command', 'name': 'command', 'searchable': 'true', 'orderable': 'true', 'search': {'value': '', 'regex': 'false'}} + # } + # columns: dict[int, dict[str, str]] = args["columns"] + print("columns:", args["columns"]) + + filtered_items: list[dict[str, Any]] = [] + for item in ITEMS: + if search_value: + values: list[str] = [str(x).lower() for x in item.values()] + if not any( + ( + re.search(search_value, value) + if has_search_regex + else search_value.lower() in value + ) + for value in values + ): + continue + + filtered_items.append(item) + + number_of_filtered = len(filtered_items) + + order: dict[int, dict[str, str]] = args.get("order", dict()) + print("order:", order) + if order: + def cmp2(item1: dict[str, str], item2: dict[str, str]) -> int: + # NOTE: https://stackoverflow.com/a/62381089/5909792 + total_result: int | None = None + for order_column in order.values(): + name: str = order_column["name"] + result = cmp(item1[name], item2[name]) + if order_column["dir"] == "desc": + result = -result + + if total_result is None: + total_result = result + else: + total_result = total_result or result + + return total_result + + filtered_items.sort(key=cmp_to_key(cmp2)) + + start: int = int(args["start"]) + length: int = int(args["length"]) + if length > 0: + filtered_items = filtered_items[start : start + length] + + return jsonify( + { + "draw": int(args["draw"]), + "recordsTotal": len(ITEMS), + "recordsFiltered": number_of_filtered, + "data": filtered_items, + } + ) + + +if __name__ == "__main__": + app.run() diff --git a/flask__webservers/filter-sort-paginate-on-server/static/index.js b/flask__webservers/filter-sort-paginate-on-server/static/index.js new file mode 100644 index 000000000..498ec7edf --- /dev/null +++ b/flask__webservers/filter-sort-paginate-on-server/static/index.js @@ -0,0 +1,22 @@ +$(function() { + new DataTable('table', { + ajax: 'api/get_items', + rowId: 'id', + serverSide: true, + processing: true, + lengthMenu: [ + [5, 10, 25, 50, -1], + ["5 records", "10 records", "25 records", "50 records", "All records"] + ], + columns: [ + { name: 'id', data: 'id', title: 'Id', }, + { name: 'name', data: 'name', title: 'Name' }, + { name: 'description', data: 'description', title: 'Description', }, + { name: 'command', data: 'command', title: 'Command', } + ], + order: [ + // Сортировка по убыванию id + [0, "desc"], + ], + }); +}); diff --git a/flask__webservers/filter-sort-paginate-on-server/templates/index.html b/flask__webservers/filter-sort-paginate-on-server/templates/index.html new file mode 100644 index 000000000..911c791f4 --- /dev/null +++ b/flask__webservers/filter-sort-paginate-on-server/templates/index.html @@ -0,0 +1,18 @@ + + + + + + + {{ title }} + + + + + + +
+ + + + diff --git a/flask__webservers/flask_debugtoolbar__examples/app.py b/flask__webservers/flask_debugtoolbar__examples/app.py index 5e3790aa8..85a51fbea 100644 --- a/flask__webservers/flask_debugtoolbar__examples/app.py +++ b/flask__webservers/flask_debugtoolbar__examples/app.py @@ -6,47 +6,47 @@ app = Flask(__name__) -app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = True -#app.config['DEBUG_TB_PANELS'] = ( +app.config["DEBUG_TB_INTERCEPT_REDIRECTS"] = True +# app.config['DEBUG_TB_PANELS'] = ( # 'flask_debugtoolbar.panels.headers.HeaderDebugPanel', # 'flask_debugtoolbar.panels.logger.LoggingPanel', # 'flask_debugtoolbar.panels.timer.TimerDebugPanel', -#) -#app.config['DEBUG_TB_HOSTS'] = ('127.0.0.1', '::1' ) -app.config['SECRET_KEY'] = 'asd' +# ) +# app.config['DEBUG_TB_HOSTS'] = ('127.0.0.1', '::1' ) +app.config["SECRET_KEY"] = "asd" # TODO: This can be removed once flask_sqlalchemy 3.0 ships -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db' +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///test.db" db = SQLAlchemy(app) class ExampleModel(db.Model): - __tablename__ = 'examples' + __tablename__ = "examples" value = db.Column(db.String(100), primary_key=True) @app.before_first_request -def setup(): +def setup() -> None: db.create_all() -@app.route('/') +@app.route("/") def index(): app.logger.info("Hello there") ExampleModel.query.get(1) - return render_template('index.html') + return render_template("index.html") -@app.route('/redirect') +@app.route("/redirect") def redirect_example(): - response = redirect(url_for('index')) - response.set_cookie('test_cookie', '1') + response = redirect(url_for("index")) + response.set_cookie("test_cookie", "1") return response -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True toolbar = DebugToolbarExtension(app) @@ -54,9 +54,7 @@ def redirect_example(): # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000 - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/flask_debugtoolbar__examples/hello_world.py b/flask__webservers/flask_debugtoolbar__examples/hello_world.py index ad793d1fa..d6846fb15 100644 --- a/flask__webservers/flask_debugtoolbar__examples/hello_world.py +++ b/flask__webservers/flask_debugtoolbar__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging @@ -9,19 +9,20 @@ from flask import Flask from flask_debugtoolbar import DebugToolbarExtension + app = Flask(__name__) # set a 'SECRET_KEY' to enable the Flask session cookies -app.config['SECRET_KEY'] = '' +app.config["SECRET_KEY"] = "" @app.route("/") -def index(): +def index() -> str: # NOTE: Need tab body: "Could not insert debug toolbar. tag not found in response." return "Hello World!" -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True if app.debug: @@ -32,9 +33,7 @@ def index(): # Localhost # port=0 -- random free port # app.run(port=0) - app.run( - port=5000 - ) + app.run(port=5000) # # Public IP # app.run(host='0.0.0.0') diff --git a/flask__webservers/generate_qrcode/main.py b/flask__webservers/generate_qrcode/main.py index 9b8e78a03..01e022eb4 100644 --- a/flask__webservers/generate_qrcode/main.py +++ b/flask__webservers/generate_qrcode/main.py @@ -1,23 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import hashlib import logging import os -from flask import Flask, request, jsonify, render_template_string, send_from_directory, url_for, redirect +from flask import ( + Flask, + request, + jsonify, + render_template_string, + send_from_directory, + url_for, + redirect, +) # pip install qrcode import qrcode -UPLOAD_FOLDER = 'images' +UPLOAD_FOLDER = "images" app = Flask(__name__) -app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER os.makedirs(UPLOAD_FOLDER, exist_ok=True) @@ -26,7 +34,8 @@ @app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -89,37 +98,41 @@ def index(): -''') +""" + ) -@app.route("/generate_qrcode", methods=['GET', 'POST']) +@app.route("/generate_qrcode", methods=["GET", "POST"]) def generate_qrcode(): - print('request.args:', request.args) - print('request.form:', request.form) + print("request.args:", request.args) + print("request.form:", request.form) print() - text = request.args.get('text', None) + text = request.args.get("text", None) if not text: - text = request.form.get('text') + text = request.form.get("text") - download = 'download' in request.args - is_redirect = 'redirect' in request.args - as_image = 'as_image' in request.args + download = "download" in request.args + is_redirect = "redirect" in request.args + as_image = "as_image" in request.args if not text: - return render_template_string("""\ + return render_template_string( + """\ Example:
{{ example_uri }}
{{ example_uri }}&download
{{ example_uri }}&redirect
{{ example_uri }}&as_image - """, example_uri='/generate_qrcode?text=Hello World!') + """, + example_uri="/generate_qrcode?text=Hello World!", + ) algo = hashlib.md5(text.encode()) hash_data = algo.hexdigest() - file_name = hash_data + '.png' - upload_folder = app.config['UPLOAD_FOLDER'] + file_name = hash_data + ".png" + upload_folder = app.config["UPLOAD_FOLDER"] abs_file_name = os.path.join(upload_folder, file_name) if not os.path.exists(abs_file_name): @@ -143,14 +156,16 @@ def generate_qrcode(): # Вернется json c ссылкой на картинку return jsonify({ # url_for составляет путь для функции images, которая возвращает картинку с сервера - 'file_name': uri, + "file_name": uri, }) -@app.route('/' + UPLOAD_FOLDER + '/') +@app.route("/" + UPLOAD_FOLDER + "/") def images(file_name): - download = 'download' in request.args - return send_from_directory(app.config['UPLOAD_FOLDER'], file_name, as_attachment=download) + download = "download" in request.args + return send_from_directory( + app.config["UPLOAD_FOLDER"], file_name, as_attachment=download + ) if __name__ == "__main__": diff --git a/flask__webservers/generate_table/main.py b/flask__webservers/generate_table/main.py index c6c693046..3c8f6c18c 100644 --- a/flask__webservers/generate_table/main.py +++ b/flask__webservers/generate_table/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging @@ -26,9 +26,10 @@ def index(): items.append(row) - items[0][0] = '' + items[0][0] = "" - return render_template_string("""\ + return render_template_string( + """\ @@ -74,10 +75,12 @@ def index(): - """, items=items) + """, + items=items, + ) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/get_URL__parameter_argument_query.py b/flask__webservers/get_URL__parameter_argument_query.py index f659b31eb..1145a004d 100644 --- a/flask__webservers/get_URL__parameter_argument_query.py +++ b/flask__webservers/get_URL__parameter_argument_query.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, request -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") -def index(): +def index() -> str: return """
@@ -47,13 +48,13 @@ def print_args(): print(args) text = '' - text += '' - text += '
URL ARGUMENTS:' + text += "URL ARGUMENTS:" for k, v in args.items(): - text += f'
{k}{v}
' + text += f"{k}{v}" + text += "" return text -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/flask__webservers/get_exif_info/main.py b/flask__webservers/get_exif_info/main.py index e0f8533af..702ecf392 100644 --- a/flask__webservers/get_exif_info/main.py +++ b/flask__webservers/get_exif_info/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import base64 @@ -17,17 +17,17 @@ def get_exif_tags(file_object_or_file_name, as_category=True): if type(file_object_or_file_name) == str: # Open image file for reading (binary mode) - file_object_or_file_name = open(file_object_or_file_name, mode='rb') + file_object_or_file_name = open(file_object_or_file_name, mode="rb") # Return Exif tags tags = exifread.process_file(file_object_or_file_name) tags_by_value = dict() if not tags: - print('Not tags') + print("Not tags") return tags_by_value - print('Tags ({}):'.format(len(tags))) + print(f"Tags ({len(tags)}):") for tag, value in tags.items(): # Process value @@ -36,9 +36,9 @@ def get_exif_tags(file_object_or_file_name, as_category=True): try: # If last 2 items equals [0, 0] if value.values[-2:] == [0, 0]: - value = bytes(value.values[:-2]).decode('utf-16') + value = bytes(value.values[:-2]).decode("utf-16") else: - value = bytes(value.values).decode('utf-16') + value = bytes(value.values).decode("utf-16") except: value = str(value.values) @@ -52,16 +52,16 @@ def get_exif_tags(file_object_or_file_name, as_category=True): if type(value) == bytes: value = base64.b64encode(value).decode() - print(' "{}": {}'.format(tag, value)) + print(f' "{tag}": {value}') if not as_category: tags_by_value[tag] = value else: # Fill categories_by_tag - if ' ' in tag: - ''.split() - category, sub_tag = tag.split(' ', maxsplit=1) + if " " in tag: + "".split() + category, sub_tag = tag.split(" ", maxsplit=1) if category not in tags_by_value: tags_by_value[category] = dict() @@ -81,9 +81,10 @@ def get_exif_tags(file_object_or_file_name, as_category=True): logging.basicConfig(level=logging.DEBUG) -@app.route('/') +@app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -215,23 +216,24 @@ def index(): -''') +""" + ) -@app.route("/get_exif", methods=['POST']) +@app.route("/get_exif", methods=["POST"]) def get_exif(): print(request.files) # check if the post request has the file part - if 'file' not in request.files: - return redirect('/') + if "file" not in request.files: + return redirect("/") - file = request.files['file'] + file = request.files["file"] categories_by_tag = get_exif_tags(file.stream) return jsonify(categories_by_tag) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/get_size_upload_file/main.py b/flask__webservers/get_size_upload_file/main.py index 40f2504db..761086f62 100644 --- a/flask__webservers/get_size_upload_file/main.py +++ b/flask__webservers/get_size_upload_file/main.py @@ -1,16 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging -import sys from flask import Flask, request, redirect, render_template_string, jsonify -sys.path.append('..') -from common import sizeof_fmt +# pip install humanize +from humanize import naturalsize as sizeof_fmt app = Flask(__name__) @@ -18,9 +17,10 @@ logging.basicConfig(level=logging.DEBUG) -@app.route('/') +@app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -87,28 +87,29 @@ def index(): -''') +""" + ) -@app.route('/get_file_size', methods=['POST']) +@app.route("/get_file_size", methods=["POST"]) def get_file_size(): print(request.files) # check if the post request has the file part - if 'file' not in request.files: - return redirect('/') + if "file" not in request.files: + return redirect("/") length = 0 - file = request.files['file'] + file = request.files["file"] if file: data = file.stream.read() length = len(data) - return jsonify({'length': length, 'length_human': sizeof_fmt(length)}) + return jsonify({"length": length, "length_human": sizeof_fmt(length)}) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/get_size_upload_file_with_progress/main.py b/flask__webservers/get_size_upload_file_with_progress/main.py index 882d398c4..35ae0b78b 100644 --- a/flask__webservers/get_size_upload_file_with_progress/main.py +++ b/flask__webservers/get_size_upload_file_with_progress/main.py @@ -1,16 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging -import sys from flask import Flask, request, redirect, render_template_string, jsonify -sys.path.append('..') -from common import sizeof_fmt +# pip install humanize +from humanize import naturalsize as sizeof_fmt app = Flask(__name__) @@ -18,9 +17,10 @@ logging.basicConfig(level=logging.DEBUG) -@app.route('/') +@app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -118,28 +118,29 @@ def index(): -''') +""" + ) -@app.route('/get_file_size', methods=['POST']) +@app.route("/get_file_size", methods=["POST"]) def get_file_size(): print(request.files) # check if the post request has the file part - if 'file' not in request.files: - return redirect('/') + if "file" not in request.files: + return redirect("/") length = 0 - file = request.files['file'] + file = request.files["file"] if file: data = file.stream.read() length = len(data) - return jsonify({'length': length, 'length_human': sizeof_fmt(length)}) + return jsonify({"length": length, "length_human": sizeof_fmt(length)}) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/get_upload_and_image_process/commands.py b/flask__webservers/get_upload_and_image_process/commands.py index c25dacbbf..ae059f2f0 100644 --- a/flask__webservers/get_upload_and_image_process/commands.py +++ b/flask__webservers/get_upload_and_image_process/commands.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PIL import Image, ImageOps, ImageFilter @@ -10,12 +10,12 @@ # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/4f6d5b013d218cfe5b964af17a2540e2e49adca0/pil_example/invert/main.py def invert(image): - if image.mode == 'RGBA': + if image.mode == "RGBA": r, g, b, a = image.split() - rgb_image = Image.merge('RGB', (r, g, b)) + rgb_image = Image.merge("RGB", (r, g, b)) inverted_image = ImageOps.invert(rgb_image) r2, g2, b2 = inverted_image.split() - return Image.merge('RGBA', (r2, g2, b2, a)) + return Image.merge("RGBA", (r2, g2, b2, a)) else: return ImageOps.invert(image) @@ -34,8 +34,14 @@ def invert_gray(img): def pixelate(image, pixel_size=9, draw_margin=True): margin_color = (0, 0, 0) - image = image.resize((image.size[0] // pixel_size, image.size[1] // pixel_size), Image.NEAREST) - image = image.resize((image.size[0] * pixel_size, image.size[1] * pixel_size), Image.NEAREST) + image = image.resize( + (image.size[0] // pixel_size, image.size[1] // pixel_size), + Image.NEAREST + ) + image = image.resize( + (image.size[0] * pixel_size, image.size[1] * pixel_size), + Image.NEAREST + ) pixel = image.load() # Draw black margin between pixels @@ -43,15 +49,15 @@ def pixelate(image, pixel_size=9, draw_margin=True): for i in range(0, image.size[0], pixel_size): for j in range(0, image.size[1], pixel_size): for r in range(pixel_size): - pixel[i+r, j] = margin_color - pixel[i, j+r] = margin_color + pixel[i + r, j] = margin_color + pixel[i, j + r] = margin_color return image def jackal_jpg(img): data_io = io.BytesIO() - img.save(data_io, format='JPEG', quality=1) + img.save(data_io, format="JPEG", quality=1) return Image.open(data_io) diff --git a/flask__webservers/get_upload_and_image_process/main.py b/flask__webservers/get_upload_and_image_process/main.py index de1e025b6..6c7b48f64 100644 --- a/flask__webservers/get_upload_and_image_process/main.py +++ b/flask__webservers/get_upload_and_image_process/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import base64 @@ -18,28 +18,28 @@ COMMANDS = { - 'invert': invert, - 'gray': gray, - 'invert_gray': invert_gray, - 'pixelate': pixelate, - 'pixelate16': lambda img: pixelate(img, 16), - 'pixelate32': lambda img: pixelate(img, 32), - 'pixelate48': lambda img: pixelate(img, 48), - 'jackal_jpg': jackal_jpg, - 'thumbnail32': lambda img: thumbnail(img, (32, 32)), - 'thumbnail64': lambda img: thumbnail(img, (64, 64)), - 'thumbnail128': lambda img: thumbnail(img, (128, 129)), - 'blur': blur, - 'blur5': lambda img: blur(img, 5) + "invert": invert, + "gray": gray, + "invert_gray": invert_gray, + "pixelate": pixelate, + "pixelate16": lambda img: pixelate(img, 16), + "pixelate32": lambda img: pixelate(img, 32), + "pixelate48": lambda img: pixelate(img, 48), + "jackal_jpg": jackal_jpg, + "thumbnail32": lambda img: thumbnail(img, (32, 32)), + "thumbnail64": lambda img: thumbnail(img, (64, 64)), + "thumbnail128": lambda img: thumbnail(img, (128, 129)), + "blur": blur, + "blur5": lambda img: blur(img, 5), } # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/master/img_to_base64_html/main.py -def img_to_base64_html(file_name__or__bytes__or__file_object): +def img_to_base64_html(file_name__or__bytes__or__file_object) -> str: arg = file_name__or__bytes__or__file_object if type(arg) == str: - with open(arg, mode='rb') as f: + with open(arg, mode="rb") as f: img_bytes = f.read() elif type(arg) == bytes: @@ -51,10 +51,10 @@ def img_to_base64_html(file_name__or__bytes__or__file_object): bytes_io = io.BytesIO(img_bytes) img = Image.open(bytes_io) - img_base64 = base64.b64encode(img_bytes).decode('utf-8') + img_base64 = base64.b64encode(img_bytes).decode("utf-8") # print(img_base64) - return 'data:image/{};base64,'.format(img.format.lower()) + img_base64 + return f"data:image/{img.format.lower()};base64,{img_base64}" app = Flask(__name__) @@ -64,27 +64,28 @@ def img_to_base64_html(file_name__or__bytes__or__file_object): # ensure that independent of the hash seed of the dictionary the return value will be consistent to not trash external # HTTP caches. You can override the default behavior by changing this variable. This is not recommended but might give # you a performance improvement on the cost of cacheability. -app.config['JSON_SORT_KEYS'] = False +app.config["JSON_SORT_KEYS"] = False logging.basicConfig(level=logging.DEBUG) -LAST_IMAGE = 'last_image.jpg' +LAST_IMAGE = "last_image.jpg" -def save_last_image(file_data): - with open(LAST_IMAGE, 'wb') as f: +def save_last_image(file_data) -> None: + with open(LAST_IMAGE, "wb") as f: f.write(file_data) -def load_last_image(): - with open(LAST_IMAGE, 'rb') as f: +def load_last_image() -> bytes: + with open(LAST_IMAGE, "rb") as f: return f.read() -@app.route('/') +@app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -279,70 +280,72 @@ def index(): -''', commands=COMMANDS) +""", + commands=COMMANDS, + ) -@app.route("/upload_file", methods=['POST']) +@app.route("/upload_file", methods=["POST"]) def upload_file(): print(request.files) # check if the post request has the file part - if 'file' not in request.files: - return redirect('/') + if "file" not in request.files: + return redirect("/") - file = request.files['file'] + file = request.files["file"] file_data = file.stream.read() save_last_image(file_data) return jsonify({ - 'img_original': img_to_base64_html(file_data), - 'img_process': None, + "img_original": img_to_base64_html(file_data), + "img_process": None, }) -@app.route("/upload_url", methods=['POST']) +@app.route("/upload_url", methods=["POST"]) def upload_url(): print(request.form) - if 'url' not in request.form: - return redirect('/') + if "url" not in request.form: + return redirect("/") - url = request.form['url'] + url = request.form["url"] file_data = requests.get(url).content save_last_image(file_data) return jsonify({ - 'img_original': img_to_base64_html(file_data), - 'img_process': None, + "img_original": img_to_base64_html(file_data), + "img_process": None, }) -@app.route("/image_process", methods=['POST']) +@app.route("/image_process", methods=["POST"]) def image_process(): print(request.form) - command = request.form['command'] + command = request.form["command"] img_original = load_last_image() data_io = io.BytesIO(img_original) - img = Image.open(data_io).convert('RGB') + img = Image.open(data_io).convert("RGB") # Получение и вызов функции result = COMMANDS[command](img) data_io = io.BytesIO() - result.save(data_io, 'JPEG') + result.save(data_io, "JPEG") img_process = data_io.getvalue() return jsonify({ - 'img_original': img_to_base64_html(img_original), - 'img_process': img_to_base64_html(img_process), + "img_original": img_to_base64_html(img_original), + "img_process": img_to_base64_html(img_process), }) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/get_upload_image_info/main.py b/flask__webservers/get_upload_image_info/main.py index 7897c4c38..b1a2edacb 100644 --- a/flask__webservers/get_upload_image_info/main.py +++ b/flask__webservers/get_upload_image_info/main.py @@ -1,14 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import base64 import io import json import logging -import sys # pip install exifread import exifread @@ -16,17 +15,18 @@ import requests from flask import Flask, jsonify, render_template_string, redirect, request -from PIL import Image -sys.path.append('..') -from common import sizeof_fmt +# pip install humanize +from humanize import naturalsize as sizeof_fmt + +from PIL import Image # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/master/print_exif/main.py def get_exif_tags(file_object_or_file_name, as_category=True): if type(file_object_or_file_name) == str: # Open image file for reading (binary mode) - file_object_or_file_name = open(file_object_or_file_name, mode='rb') + file_object_or_file_name = open(file_object_or_file_name, mode="rb") # Return Exif tags tags = exifread.process_file(file_object_or_file_name) @@ -45,9 +45,9 @@ def get_exif_tags(file_object_or_file_name, as_category=True): try: # If last 2 items equals [0, 0] if value.values[-2:] == [0, 0]: - value = bytes(value.values[:-2]).decode('utf-16') + value = bytes(value.values[:-2]).decode("utf-16") else: - value = bytes(value.values).decode('utf-16') + value = bytes(value.values).decode("utf-16") except: value = str(value.values) @@ -68,8 +68,8 @@ def get_exif_tags(file_object_or_file_name, as_category=True): else: # Fill categories_by_tag - if ' ' in tag: - category, sub_tag = tag.split(' ', maxsplit=1) + if " " in tag: + category, sub_tag = tag.split(" ", maxsplit=1) if category not in tags_by_value: tags_by_value[category] = dict() @@ -85,11 +85,11 @@ def get_exif_tags(file_object_or_file_name, as_category=True): # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/master/img_to_base64_html/main.py -def img_to_base64_html(file_name__or__bytes__or__file_object): +def img_to_base64_html(file_name__or__bytes__or__file_object) -> str: arg = file_name__or__bytes__or__file_object if type(arg) == str: - with open(arg, mode='rb') as f: + with open(arg, mode="rb") as f: img_bytes = f.read() elif type(arg) == bytes: @@ -101,10 +101,10 @@ def img_to_base64_html(file_name__or__bytes__or__file_object): bytes_io = io.BytesIO(img_bytes) img = Image.open(bytes_io) - img_base64 = base64.b64encode(img_bytes).decode('utf-8') + img_base64 = base64.b64encode(img_bytes).decode("utf-8") # print(img_base64) - return 'data:image/{};base64,'.format(img.format.lower()) + img_base64 + return f"data:image/{img.format.lower()};base64,{img_base64}" # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/84651cfefaee768851170ec4ba7d025bbaae622d/get_image_info/main.py#L84 @@ -114,7 +114,7 @@ def get_image_info(file_name__or__bytes__or__bytes_io, pretty_json_str=False): # File name if type_data == str: - with open(data, mode='rb') as f: + with open(data, mode="rb") as f: data = f.read() if type(data) == bytes: @@ -126,23 +126,30 @@ def get_image_info(file_name__or__bytes__or__bytes_io, pretty_json_str=False): img = Image.open(data) info = dict() - info['length'] = dict() - info['length']['value'] = length - info['length']['text'] = sizeof_fmt(length) - - info['format'] = img.format - info['mode'] = img.mode - info['channels'] = len(img.getbands()) - info['bit_color'] = { - '1': 1, 'L': 8, 'P': 8, 'RGB': 24, 'RGBA': 32, - 'CMYK': 32, 'YCbCr': 24, 'I': 32, 'F': 32 + info["length"] = dict() + info["length"]["value"] = length + info["length"]["text"] = sizeof_fmt(length) + + info["format"] = img.format + info["mode"] = img.mode + info["channels"] = len(img.getbands()) + info["bit_color"] = { + "1": 1, + "L": 8, + "P": 8, + "RGB": 24, + "RGBA": 32, + "CMYK": 32, + "YCbCr": 24, + "I": 32, + "F": 32, }[img.mode] - info['size'] = dict() - info['size']['width'] = img.width - info['size']['height'] = img.height + info["size"] = dict() + info["size"]["width"] = img.width + info["size"]["height"] = img.height - info['exif'] = exif + info["exif"] = exif if pretty_json_str: info = json.dumps(info, indent=4, ensure_ascii=False) @@ -157,14 +164,15 @@ def get_image_info(file_name__or__bytes__or__bytes_io, pretty_json_str=False): # ensure that independent of the hash seed of the dictionary the return value will be consistent to not trash external # HTTP caches. You can override the default behavior by changing this variable. This is not recommended but might give # you a performance improvement on the cost of cacheability. -app.config['JSON_SORT_KEYS'] = False +app.config["JSON_SORT_KEYS"] = False logging.basicConfig(level=logging.DEBUG) -@app.route('/') +@app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -356,43 +364,44 @@ def index(): -''') +""" + ) -@app.route("/get_info_from_file", methods=['POST']) +@app.route("/get_info_from_file", methods=["POST"]) def get_info_from_file(): print(request.files) # check if the post request has the file part - if 'file' not in request.files: - return redirect('/') + if "file" not in request.files: + return redirect("/") - file = request.files['file'] + file = request.files["file"] file_data = file.stream.read() info = get_image_info(file_data) - info['img_base64'] = img_to_base64_html(file_data) + info["img_base64"] = img_to_base64_html(file_data) return jsonify(info) -@app.route("/get_info_from_url", methods=['POST']) +@app.route("/get_info_from_url", methods=["POST"]) def get_info_from_url(): print(request.form) - if 'url' not in request.form: - return redirect('/') + if "url" not in request.form: + return redirect("/") - url = request.form['url'] + url = request.form["url"] file_data = requests.get(url).content info = get_image_info(file_data) - info['img_base64'] = img_to_base64_html(file_data) + info["img_base64"] = img_to_base64_html(file_data) return jsonify(info) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/hash__md5_sha1_etc/main.py b/flask__webservers/hash__md5_sha1_etc/main.py index b97386b77..f13aaa1ba 100644 --- a/flask__webservers/hash__md5_sha1_etc/main.py +++ b/flask__webservers/hash__md5_sha1_etc/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import hashlib @@ -15,14 +15,17 @@ app = Flask(__name__) -SUPPORTED_HASH_ALGOS = set(list(hashlib.algorithms_guaranteed) + ['md4']) -SUPPORTED_HASH_ALGOS = [algo for algo in SUPPORTED_HASH_ALGOS if not algo.lower().startswith('shake')] +SUPPORTED_HASH_ALGOS = set(list(hashlib.algorithms_guaranteed) + ["md4"]) +SUPPORTED_HASH_ALGOS = [ + algo for algo in SUPPORTED_HASH_ALGOS if not algo.lower().startswith("shake") +] SUPPORTED_HASH_ALGOS = sorted(SUPPORTED_HASH_ALGOS) @app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\ @@ -131,46 +134,51 @@ def index(): -''', algos=SUPPORTED_HASH_ALGOS) +""", + algos=SUPPORTED_HASH_ALGOS, + ) -@app.route("/do_hash", methods=['GET', 'POST']) +@app.route("/do_hash", methods=["GET", "POST"]) def do_hash(): - print('request.args:', request.args) - print('request.form:', request.form) - print('request.files:', request.files) + print("request.args:", request.args) + print("request.form:", request.form) + print("request.files:", request.files) print() - text = request.args.get('text') + text = request.args.get("text") if not text: - text = request.form.get('text') + text = request.form.get("text") - file = request.files.get('file') + file = request.files.get("file") - algo = request.args.get('hash') + algo = request.args.get("hash") if not algo: - algo = request.form.get('hash') + algo = request.form.get("hash") if not ((text or file) and algo): - return render_template_string("""\ + return render_template_string( + """\ Example:
{{ example_uri }}&hash=md5
{{ example_uri }}&hash=sha1
{{ example_uri }}&hash=sha512 - """, example_uri='/do_hash?text=Hello World!') + """, + example_uri="/do_hash?text=Hello World!", + ) result = { - 'result': None, - 'hash': algo, - 'error': None, + "result": None, + "hash": algo, + "error": None, } if algo not in SUPPORTED_HASH_ALGOS: - result['error'] = f'Unsupported algorithm {algo!r}' + result["error"] = f"Unsupported algorithm {algo!r}" else: if file: digest = hashlib.new(algo) - for chunk in iter(lambda: file.stream.read(128 * digest.block_size), b''): + for chunk in iter(lambda: file.stream.read(128 * digest.block_size), b""): digest.update(chunk) else: @@ -178,7 +186,7 @@ def do_hash(): hex_digest = digest.hexdigest() - result['result'] = hex_digest + result["result"] = hex_digest # Вернется json c результатом хеширования return jsonify(result) diff --git a/flask__webservers/hello_world.py b/flask__webservers/hello_world.py index a6418ba30..90f9c84e3 100644 --- a/flask__webservers/hello_world.py +++ b/flask__webservers/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging @@ -14,11 +14,11 @@ @app.route("/") -def index(): +def index() -> str: return "Hello World!" -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/hello_world_with_compress.py b/flask__webservers/hello_world_with_compress.py new file mode 100644 index 000000000..4c7185f22 --- /dev/null +++ b/flask__webservers/hello_world_with_compress.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging + +from flask import Flask + +# pip install flask-compress +from flask_compress import Compress + + +app = Flask(__name__) +Compress(app) + +logging.basicConfig(level=logging.DEBUG) + + +@app.route("/") +def index(): + return "Hello World!" * 100 + + +if __name__ == "__main__": + app.debug = True + + # Localhost + # port=0 -- random free port + # app.run(port=0) + app.run(port=5001) + + # # Public IP + # app.run(host='0.0.0.0') diff --git a/flask__webservers/json_as_response.py b/flask__webservers/json_as_response.py index a521639e3..a744bf9e3 100644 --- a/flask__webservers/json_as_response.py +++ b/flask__webservers/json_as_response.py @@ -1,24 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging - from flask import Flask, jsonify -app = Flask(__name__) + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) DATA = { - 'text': "Hello World!", - 'ok': True, - 'items': [ - 'Hello', - 'World!', - ] + "text": "Hello World!", + "ok": True, + "items": [ + "Hello", + "World!", + ], } @@ -32,5 +32,5 @@ def index_v2(): return DATA -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/flask__webservers/logging__examples/log-config.yaml b/flask__webservers/logging__examples/log-config.yaml new file mode 100644 index 000000000..b427173a0 --- /dev/null +++ b/flask__webservers/logging__examples/log-config.yaml @@ -0,0 +1,27 @@ +version: 1 + +formatters: + default: + format: "[%(asctime)s] %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s" + +handlers: + console: + class: "logging.StreamHandler" + formatter: "default" + stream: "ext://sys.stdout" + + web-server-file: + class: "logging.handlers.RotatingFileHandler" + formatter: "default" + filename: "main_new.log" + encoding: "utf-8" + backupCount: 5 + maxBytes: 10000000 + delay: true + +loggers: + web-server: &web-server + handlers: ["console", "web-server-file"] + level: "DEBUG" + + werkzeug: *web-server diff --git a/flask__webservers/logging__examples/main_new.py b/flask__webservers/logging__examples/main_new.py new file mode 100644 index 000000000..8000e0e63 --- /dev/null +++ b/flask__webservers/logging__examples/main_new.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging.config +from pathlib import Path +from typing import Any + +# pip install PyYAML +import yaml + +from flask import Flask + + +DIR: Path = Path(__file__).resolve().parent + +DIR_LOGS: Path = DIR / "logs" +DIR_LOGS.mkdir(parents=True, exist_ok=True) + +CONFIG_LOG_FILE_NAME: Path = DIR / "log-config.yaml" + + +LOGGING: dict[str, Any] = yaml.safe_load( + CONFIG_LOG_FILE_NAME.read_text("utf-8") +) +for handler in LOGGING["handlers"].values(): + try: + handler["filename"] = DIR_LOGS / handler["filename"] + except KeyError: + pass + +logging.config.dictConfig(LOGGING) + +log = logging.getLogger("web-server") + + +app = Flask(__name__) +app.logger = log + + +@app.route("/") +def index() -> str: + log.debug("call index") + return "Hello World!" + + +if __name__ == "__main__": + app.debug = True + + app.run(host="0.0.0.0", port=5000) diff --git a/flask__webservers/logging__examples/main_old.py b/flask__webservers/logging__examples/main_old.py new file mode 100644 index 000000000..e5bdab429 --- /dev/null +++ b/flask__webservers/logging__examples/main_old.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +import sys +from logging.handlers import RotatingFileHandler +from pathlib import Path + +from flask import Flask + + +DIR_LOGS: Path = Path(__file__).resolve().parent / "logs" +DIR_LOGS.mkdir(parents=True, exist_ok=True) + + +app = Flask(__name__) + +formatter = logging.Formatter( + "[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s" +) + +file_handler = RotatingFileHandler( + DIR_LOGS / "main_old.log", maxBytes=10_000_000, backupCount=5, encoding="utf-8" +) +file_handler.setFormatter(formatter) + +stream_handler = logging.StreamHandler(stream=sys.stdout) +stream_handler.setFormatter(formatter) + +log: logging.Logger = app.logger +log.handlers.clear() +log.setLevel(logging.DEBUG) +log.addHandler(file_handler) +log.addHandler(stream_handler) + +log_werkzeug = logging.getLogger("werkzeug") +log_werkzeug.setLevel(logging.DEBUG) +log_werkzeug.addHandler(file_handler) +log_werkzeug.addHandler(stream_handler) + + +@app.route("/") +def index() -> str: + log.debug("call index") + return "Hello World!" + + +if __name__ == "__main__": + app.debug = True + + app.run(host="0.0.0.0", port=5000) diff --git a/flask__webservers/logging__remove_date_from_werkzeug_logs/log-config.yaml b/flask__webservers/logging__remove_date_from_werkzeug_logs/log-config.yaml new file mode 100644 index 000000000..90c089ccd --- /dev/null +++ b/flask__webservers/logging__remove_date_from_werkzeug_logs/log-config.yaml @@ -0,0 +1,30 @@ +version: 1 + +formatters: + default: + format: "[%(asctime)s] %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s" + +handlers: + console: + class: "logging.StreamHandler" + formatter: "default" + stream: "ext://sys.stdout" + + web-server-file: + class: "logging.handlers.RotatingFileHandler" + formatter: "default" + filename: "main_new.log" + encoding: "utf-8" + backupCount: 5 + maxBytes: 10000000 + delay: true + +filters: + filter_remove_date_from_werkzeug_logs: + (): utils.FilterRemoveDateFromWerkzeugLogs + +loggers: + werkzeug: + handlers: ["console", "web-server-file"] + level: "DEBUG" + filters: ["filter_remove_date_from_werkzeug_logs"] diff --git a/flask__webservers/logging__remove_date_from_werkzeug_logs/main_new.py b/flask__webservers/logging__remove_date_from_werkzeug_logs/main_new.py new file mode 100644 index 000000000..6c2e83aec --- /dev/null +++ b/flask__webservers/logging__remove_date_from_werkzeug_logs/main_new.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging.config +from pathlib import Path +from typing import Any + +# pip install PyYAML +import yaml + +from flask import Flask + + +DIR: Path = Path(__file__).resolve().parent + +DIR_LOGS: Path = DIR / "logs" +DIR_LOGS.mkdir(parents=True, exist_ok=True) + +CONFIG_LOG_FILE_NAME: Path = DIR / "log-config.yaml" + + +LOGGING: dict[str, Any] = yaml.safe_load( + CONFIG_LOG_FILE_NAME.read_text("utf-8") +) +for handler in LOGGING["handlers"].values(): + try: + handler["filename"] = DIR_LOGS / handler["filename"] + except KeyError: + pass + +logging.config.dictConfig(LOGGING) + +log = logging.getLogger("werkzeug") + + +app = Flask(__name__) +app.logger = log + + +@app.route("/") +def index() -> str: + log.debug("call index") + return "Hello World!" + + +if __name__ == "__main__": + app.debug = True + + app.run(host="0.0.0.0", port=5000) diff --git a/flask__webservers/logging__remove_date_from_werkzeug_logs/main_old.py b/flask__webservers/logging__remove_date_from_werkzeug_logs/main_old.py new file mode 100644 index 000000000..7b7c003bb --- /dev/null +++ b/flask__webservers/logging__remove_date_from_werkzeug_logs/main_old.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +import sys + +from logging.handlers import RotatingFileHandler +from pathlib import Path + +from flask import Flask + +from utils import FilterRemoveDateFromWerkzeugLogs + + +DIR_LOGS: Path = Path(__file__).resolve().parent / "logs" +DIR_LOGS.mkdir(parents=True, exist_ok=True) + + +app = Flask(__name__) + +formatter = logging.Formatter( + "[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s" +) + +file_handler = RotatingFileHandler( + DIR_LOGS / "main_old.log", maxBytes=10_000_000, backupCount=5, encoding="utf-8" +) +file_handler.setFormatter(formatter) + +stream_handler = logging.StreamHandler(stream=sys.stdout) +stream_handler.setFormatter(formatter) + +log: logging.Logger = app.logger +log.handlers.clear() +log.setLevel(logging.DEBUG) +log.addHandler(file_handler) +log.addHandler(stream_handler) + +log_werkzeug = logging.getLogger("werkzeug") +log_werkzeug.setLevel(logging.DEBUG) +log_werkzeug.addHandler(file_handler) +log_werkzeug.addHandler(stream_handler) +log_werkzeug.addFilter(FilterRemoveDateFromWerkzeugLogs()) + + +@app.route("/") +def index() -> str: + log.debug("call index") + return "Hello World!" + + +if __name__ == "__main__": + app.debug = True + + app.run(host="0.0.0.0", port=5000) diff --git a/flask__webservers/logging__remove_date_from_werkzeug_logs/utils.py b/flask__webservers/logging__remove_date_from_werkzeug_logs/utils.py new file mode 100644 index 000000000..531a2deba --- /dev/null +++ b/flask__webservers/logging__remove_date_from_werkzeug_logs/utils.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +import re + + +# NOTE: Fix https://github.com/pallets/werkzeug/blob/72b2e48e7d44927b1b7d6b2f940d0691230de893/src/werkzeug/serving.py#L425C38-L425C44 +class FilterRemoveDateFromWerkzeugLogs(logging.Filter): + # '192.168.0.102 - - [30/Jun/2024 01:14:03] "%s" %s %s' -> '192.168.0.102 - "%s" %s %s' + pattern: re.Pattern = re.compile(r' - - \[.+?] "') + + def filter(self, record: logging.LogRecord) -> bool: + record.msg = self.pattern.sub(' - "', record.msg) + return True diff --git a/flask__webservers/post_data.py b/flask__webservers/post_data.py index d2ece5c21..6d22577a5 100644 --- a/flask__webservers/post_data.py +++ b/flask__webservers/post_data.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, request -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") -def index(): +def index() -> str: return """
@@ -38,13 +39,13 @@ def index(): """ -@app.route("/print_data", methods=['POST']) +@app.route("/print_data", methods=["POST"]) def print_data(): data = request.data - print('data:', data) + print("data:", data) return data -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/flask__webservers/post_data__as_form.py b/flask__webservers/post_data__as_form.py index 2791a8c43..ab732dd71 100644 --- a/flask__webservers/post_data__as_form.py +++ b/flask__webservers/post_data__as_form.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from flask import Flask, request, jsonify -app = Flask(__name__) - import logging +from flask import Flask, request + + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") -def index(): +def index() -> str: return """

@@ -46,16 +47,16 @@ def index(): """ -@app.route("/print_data", methods=['POST']) +@app.route("/print_data", methods=["POST"]) def print_data(): data = request.data - print('data:', data) + print("data:", data) form = request.form - print('form:', form) + print("form:", form) - return form['name'] + " " + form['surname'] + return form["name"] + " " + form["surname"] -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/flask__webservers/post_data__as_json.py b/flask__webservers/post_data__as_json.py index 9666b60aa..3c44aa550 100644 --- a/flask__webservers/post_data__as_json.py +++ b/flask__webservers/post_data__as_json.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, request -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") -def index(): +def index() -> str: return """
@@ -42,16 +43,16 @@ def index(): """ -@app.route("/print_data", methods=['POST']) +@app.route("/print_data", methods=["POST"]) def print_data(): data = request.data - print('data:', data) + print("data:", data) json = request.json - print('json:', json) + print("json:", json) return data -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/flask__webservers/print_hex_post_data/main.py b/flask__webservers/print_hex_post_data/main.py index 0a571f9f3..e8b699b97 100644 --- a/flask__webservers/print_hex_post_data/main.py +++ b/flask__webservers/print_hex_post_data/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import binascii @@ -15,15 +15,15 @@ logging.basicConfig(level=logging.DEBUG) -@app.route("/", methods=['POST']) -def index(): +@app.route("/", methods=["POST"]) +def index() -> str: data = request.data print(binascii.hexlify(data), data) return "Ok" -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True app.run(port=33333) diff --git a/flask__webservers/redirect_to_/main.py b/flask__webservers/redirect_to_/main.py index b315d5013..1e5e8dec1 100644 --- a/flask__webservers/redirect_to_/main.py +++ b/flask__webservers/redirect_to_/main.py @@ -1,40 +1,44 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, redirect, request, render_template_string -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return render_template_string('''\ + return render_template_string( + """\
{{ example }}

- ''', example='/redirect?url=http://bash.im/') + """, + example="/redirect?url=http://bash.im/", + ) -@app.route('/redirect') +@app.route("/redirect") def redirect_to(): # From url arguments - url = request.args.get('url', None) + url = request.args.get("url", None) if not url: # From form arguments - url = request.form.get('url', '/') + url = request.form.get("url", "/") return redirect(url) -if __name__ == '__main__': +if __name__ == "__main__": # Localhost app.run(port=5001) diff --git a/flask__webservers/redirect_to_google/main.py b/flask__webservers/redirect_to_google/main.py index 7f7751ac2..93b33e734 100644 --- a/flask__webservers/redirect_to_google/main.py +++ b/flask__webservers/redirect_to_google/main.py @@ -1,22 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, redirect -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - return redirect('https://google.ru') + return redirect("https://google.ru") -if __name__ == '__main__': +if __name__ == "__main__": # Localhost app.run(port=5001) diff --git a/flask__webservers/run_bat_from_ajax/main.py b/flask__webservers/run_bat_from_ajax/main.py index 146078588..9055ec144 100644 --- a/flask__webservers/run_bat_from_ajax/main.py +++ b/flask__webservers/run_bat_from_ajax/main.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging import os import subprocess + from pathlib import Path from flask import Flask, jsonify, render_template DIR = Path(__file__).resolve().parent -DIR_BAT_SCRIPTS = DIR / 'bat_scripts' +DIR_BAT_SCRIPTS = DIR / "bat_scripts" app = Flask(__name__) @@ -21,28 +22,31 @@ logging.basicConfig(level=logging.DEBUG) -@app.route('/') +@app.route("/") def index(): - return render_template('index.html', scripts=[f.name for f in DIR_BAT_SCRIPTS.glob('*.bat')]) + return render_template( + "index.html", + scripts=[f.name for f in DIR_BAT_SCRIPTS.glob("*.bat")], + ) -@app.route("/os_startfile/", methods=['POST']) +@app.route("/os_startfile/", methods=["POST"]) def on_os_startfile(script_name: str): os.startfile(DIR_BAT_SCRIPTS / script_name) - return jsonify({'ok': True}) + return jsonify({"ok": True}) -@app.route("/subprocess/", methods=['POST']) +@app.route("/subprocess/", methods=["POST"]) def on_subprocess(script_name: str): result = subprocess.check_output( DIR_BAT_SCRIPTS / script_name, universal_newlines=True, timeout=1 ) print(result) os.startfile(DIR_BAT_SCRIPTS / script_name) - return jsonify({'ok': True, 'result': result}) + return jsonify({"ok": True, "result": result}) -if __name__ == '__main__': +if __name__ == "__main__": app.debug = True # Localhost diff --git a/flask__webservers/run_with_random_port/main.py b/flask__webservers/run_with_random_port/main.py index 5e3244a97..f9a8d2d53 100644 --- a/flask__webservers/run_with_random_port/main.py +++ b/flask__webservers/run_with_random_port/main.py @@ -5,22 +5,23 @@ # SOURCE: https://stackoverflow.com/a/5089963/5909792 -from flask import Flask, request import socket +from flask import Flask, request + app = Flask(__name__) -@app.route('/') -def hello(): - return 'Hello, world! running on %s' % request.host +@app.route("/") +def hello() -> str: + return "Hello, world! running on %s" % request.host -if __name__ == '__main__': +if __name__ == "__main__": sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.bind(('localhost', 0)) + sock.bind(("localhost", 0)) port = sock.getsockname()[1] - print('sock.getsockname:', sock.getsockname()) + print("sock.getsockname:", sock.getsockname()) sock.close() app.run(port=port) diff --git a/flask__webservers/server__handle_stdin__input.py b/flask__webservers/server__handle_stdin__input.py index 414428fb7..288ff43e8 100644 --- a/flask__webservers/server__handle_stdin__input.py +++ b/flask__webservers/server__handle_stdin__input.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging @@ -20,9 +20,9 @@ text = "Hello World!" -def go(): +def go() -> None: time.sleep(2) - print('\n') + print("\n") global text @@ -35,7 +35,7 @@ def index(): return text -if __name__ == '__main__': +if __name__ == "__main__": thread = Thread(target=go) thread.start() diff --git a/flask__webservers/server_with_additional_command_thread/main.py b/flask__webservers/server_with_additional_command_thread/main.py index 12d4ccac4..fcb917e94 100644 --- a/flask__webservers/server_with_additional_command_thread/main.py +++ b/flask__webservers/server_with_additional_command_thread/main.py @@ -1,13 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging +import time + +from urllib.parse import urljoin + +import requests + +from bs4 import BeautifulSoup from flask import Flask, request, redirect -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @@ -17,55 +25,50 @@ @app.route("/") def index(): - return redirect('/img_search?text=Котята') + return redirect("/img_search?text=Котята") @app.route("/img_search") def img_search(): - text = request.args.get('text') - print('text:', text) + text = request.args.get("text") + print("text:", text) global EXECUTE_COMMAND, COMMAND_TEXT COMMAND_TEXT = text EXECUTE_COMMAND = False - return 'text: ' + text - + return "text: " + text -def loop_command_function(): - import time +def loop_command_function() -> None: while True: global EXECUTE_COMMAND, COMMAND_TEXT if not EXECUTE_COMMAND and COMMAND_TEXT: print(COMMAND_TEXT) - url = 'http://yandex.ru/images/search?text=' + COMMAND_TEXT + url = "http://yandex.ru/images/search?text=" + COMMAND_TEXT - import requests rs = requests.get(url) print(rs) - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'lxml') - - from urllib.parse import urljoin + root = BeautifulSoup(rs.content, "lxml") img_list = [] - for img in root.select('img.serp-item__thumb'): - url_img = urljoin(url, img['src']) + for img in root.select("img.serp-item__thumb"): + url_img = urljoin(url, img["src"]) img_list.append(url_img) - print('img_list[{}]: {}'.format(len(img_list), img_list)) + print(f"img_list[{len(img_list)}]: {img_list}") EXECUTE_COMMAND = True time.sleep(1) -if __name__ == '__main__': +if __name__ == "__main__": from threading import Thread + thread = Thread(target=loop_command_function) thread.start() diff --git a/flask__webservers/server_with_window_notification/main.py b/flask__webservers/server_with_window_notification/main.py index 866cb270f..c7a16d49f 100644 --- a/flask__webservers/server_with_window_notification/main.py +++ b/flask__webservers/server_with_window_notification/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging @@ -13,7 +13,9 @@ from flask import Flask, request, redirect ROOT = Path(__file__).resolve().parent.parent.parent -sys.path.append(str(ROOT / 'winapi__windows__ctypes/windows__toast_balloontip_notifications')) +sys.path.append( + str(ROOT / "winapi__windows__ctypes/windows__toast_balloontip_notifications") +) from run_notify import run_in_thread @@ -21,29 +23,29 @@ logging.basicConfig(level=logging.DEBUG) -def show(text): +def show(text) -> None: title = str(threading.current_thread()) run_in_thread(title, text, duration=20) @app.route("/") def index(): - return redirect('/show_notification?text=О, уведомление пришло!') + return redirect("/show_notification?text=О, уведомление пришло!") @app.route("/show_notification") -def show_notification(): - text = request.args.get('text') - print('text:', text) +def show_notification() -> str: + text = request.args.get("text") + print("text:", text) # Run function in new thread thread = threading.Thread(target=show, args=(text,)) thread.start() - return 'Ok' + return "Ok" -if __name__ == '__main__': +if __name__ == "__main__": # Localhost app.run( port=5000, diff --git a/flask__webservers/set_response_headers.py b/flask__webservers/set_response_headers.py index 5038d328e..edc84839f 100644 --- a/flask__webservers/set_response_headers.py +++ b/flask__webservers/set_response_headers.py @@ -1,23 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, Response -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): rs = Response("Hello World!") - rs.headers['User-Agent'] = "FooBar" + rs.headers["User-Agent"] = "FooBar" return rs -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/flask__webservers/show_and_download_from_readmanga/main.py b/flask__webservers/show_and_download_from_readmanga/main.py index 8d8d0eb6d..7b8a2a63c 100644 --- a/flask__webservers/show_and_download_from_readmanga/main.py +++ b/flask__webservers/show_and_download_from_readmanga/main.py @@ -1,24 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging +import os +import sys + from flask import Flask, render_template_string, request, redirect -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) # Добавление пути основной папки репозитория, чтобы импортировать модуль download_volume_readmanga -import os dir = os.path.dirname(__file__) dir = os.path.dirname(dir) dir = os.path.dirname(dir) - -import sys sys.path.append(dir) - from download_volume_readmanga import get_url_images, save_urls_to_zip @@ -27,7 +27,7 @@

Например: {0}

""" -DEFAULT_URL_MANGA = 'https://readmanga.live/one_punch_man__A1bc88e/vol1/1' +DEFAULT_URL_MANGA = "https://readmanga.live/one_punch_man__A1bc88e/vol1/1" # TODO: FIXED HTTP STATUS "402 Payment Required" on @@ -35,15 +35,16 @@ @app.route("/") def index(): if not request.args: - return NOT_ARGS_HTML.format(f'/?url={DEFAULT_URL_MANGA}') + return NOT_ARGS_HTML.format(f"/?url={DEFAULT_URL_MANGA}") - url = request.args.get('url') - print('Url manga:', url) + url = request.args.get("url") + print("Url manga:", url) images_urls = get_url_images(url) - print('Urls images:', images_urls) + print("Urls images:", images_urls) - return render_template_string('''\ + return render_template_string( + """\ Показать и скачать из readmanga @@ -61,32 +62,37 @@ def index(): - ''', url=url, images_urls=images_urls) + """, + url=url, + images_urls=images_urls, + ) @app.route("/export") def export(): if not request.args: - return NOT_ARGS_HTML.format(f'/export?{DEFAULT_URL_MANGA}') + return NOT_ARGS_HTML.format(f"/export?{DEFAULT_URL_MANGA}") - url = request.args.get('url') - print('Url manga:', url) + url = request.args.get("url") + print("Url manga:", url) - file_name = os.path.basename(url) + '.zip' - static_file_name = os.path.join('static', file_name) + file_name = os.path.basename(url) + ".zip" + static_file_name = os.path.join("static", file_name) # Если архива с главой нет, качаем ее if not os.path.exists(static_file_name): - print('Архива "{}" нет, качаем и создаем.'.format(static_file_name)) + print(f'Архива "{static_file_name}" нет, качаем и создаем.') images_urls = get_url_images(url) - print('Urls images:', images_urls) + print("Urls images:", images_urls) save_urls_to_zip(static_file_name, images_urls) - print('Сохранено в архиве:', file_name) + print("Сохранено в архиве:", file_name) # Перенаправляем к url с архивом - relative_url = render_template_string("{{ url_for('static', filename='%s') }}" % (file_name, )) + relative_url = render_template_string( + "{{ url_for('static', filename='%s') }}" % (file_name,) + ) return redirect(relative_url) diff --git a/flask__webservers/show_client_ip_API.py b/flask__webservers/show_client_ip_API.py index 182aac0c2..422c6a93a 100644 --- a/flask__webservers/show_client_ip_API.py +++ b/flask__webservers/show_client_ip_API.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging @@ -14,28 +14,25 @@ logging.basicConfig(level=logging.DEBUG) -@app.route('/') +@app.route("/") def index(): return request.remote_addr -@app.route('/json') +@app.route("/json") def get_json(): return { - 'ip': request.remote_addr, + "ip": request.remote_addr, } -@app.route('/xml') +@app.route("/xml") def get_xml(): - root = ET.Element('ip') + root = ET.Element("ip") root.text = request.remote_addr xml_bytes = ET.tostring(root, encoding="utf-8", xml_declaration=True) - return Response(xml_bytes, mimetype='text/xml') + return Response(xml_bytes, mimetype="text/xml") -if __name__ == '__main__': - app.run( - host='0.0.0.0', - port=5000 - ) +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) diff --git a/flask__webservers/show_last_bash_quotes/main.py b/flask__webservers/show_last_bash_quotes/main.py index 3663c4fee..3c91e6018 100644 --- a/flask__webservers/show_last_bash_quotes/main.py +++ b/flask__webservers/show_last_bash_quotes/main.py @@ -1,37 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging import re +import traceback + +import requests + +from bs4 import BeautifulSoup +from flask import Flask, render_template_string def today_quotes(soup): # Узнаем количество сегодняшних цитат - tag = soup.find(attrs=dict(id='stats')) + tag = soup.find(attrs=dict(id="stats")) - match = re.search('сегодня (\d+),', tag.text) + match = re.search(r"сегодня (\d+),", tag.text) return int(match.group(1)) -from flask import Flask, render_template_string app = Flask(__name__) - -import logging logging.basicConfig(level=logging.DEBUG) -import requests -from bs4 import BeautifulSoup - -import traceback - - @app.route("/") def index(): try: - rs = requests.get('http://bash.im') + rs = requests.get("http://bash.im") soup = BeautifulSoup(rs.text, "lxml") number = today_quotes(soup) @@ -48,7 +46,8 @@ def index(): print(i, text) quotes.append(text) - return render_template_string('''\ + return render_template_string( + """\ Новые за день цитаты bash.im @@ -68,11 +67,14 @@ def index(): {% endif %} - ''', number=number, item_list=quotes) + """, + number=number, + item_list=quotes, + ) except BaseException as e: - print('Error: {}\n\n{}'.format(e, traceback.format_exc())) - return 'Error: ' + str(e) + print(f"Error: {e}\n\n{traceback.format_exc()}") + return "Error: " + str(e) if __name__ == "__main__": diff --git a/flask__webservers/show_lunch_menu/main.py b/flask__webservers/show_lunch_menu/main.py index 8f5ce497a..cb2de5d3c 100644 --- a/flask__webservers/show_lunch_menu/main.py +++ b/flask__webservers/show_lunch_menu/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Чтение из docx файла таблицы меню и отображении таблицы на веб странице. @@ -10,17 +10,18 @@ """ +import logging +import re + +from docx import Document from flask import Flask, render_template_string -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) # Регулярка для поиска последовательностей пробелов: от двух подряд и более -import re -multi_space_pattern = re.compile(r'[ ]{2,}') - -from docx import Document +multi_space_pattern = re.compile(r"[ ]{2,}") def get_rows_lunch_menu(): @@ -31,16 +32,19 @@ def get_rows_lunch_menu(): for table in document.tables: # Перебор начинается со второй строки, потому что, первая строка таблицы -- это строка "Обеденное меню" for row in table.rows[1:]: - name, weight, price = [multi_space_pattern.sub(' ', i.text.strip()) for i in row.cells] + name, weight, price = [ + multi_space_pattern.sub(" ", i.text.strip()) + for i in row.cells + ] if name == weight == price or (not weight or not price): name = name.title() logging.debug(name) - rows.append((name, )) + rows.append((name,)) continue rows.append((name, weight, price)) - logging.debug('{} {} {}'.format(name, weight, price)) + logging.debug(f"{name} {weight} {price}") # Таблицы в меню дублируются break @@ -52,7 +56,8 @@ def get_rows_lunch_menu(): def index(): rows = get_rows_lunch_menu() - return render_template_string('''\ + return render_template_string( + """\ Обеденное меню @@ -82,10 +87,12 @@ def index(): - ''', rows=rows) + """, + rows=rows, + ) -if __name__ == '__main__': +if __name__ == "__main__": # Localhost app.run(port=5001) diff --git a/flask__webservers/show_lunch_menu_from_email/config.py b/flask__webservers/show_lunch_menu_from_email/config.py index 747621f50..fb26fa83a 100644 --- a/flask__webservers/show_lunch_menu_from_email/config.py +++ b/flask__webservers/show_lunch_menu_from_email/config.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Например: @mail.com diff --git a/flask__webservers/show_lunch_menu_from_email/main.py b/flask__webservers/show_lunch_menu_from_email/main.py index 4e8e8057b..6ebcd5323 100644 --- a/flask__webservers/show_lunch_menu_from_email/main.py +++ b/flask__webservers/show_lunch_menu_from_email/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -14,21 +14,30 @@ """ +import email +import imaplib import os -import traceback +import re import sys +import traceback +from datetime import datetime, date + +# pip install python-docx +from docx import Document +from flask import Flask, render_template_string, request import config + + if config.debug: import logging logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) def get_current_lunch_file_name(): - from datetime import date - filename = date.today().strftime('%d.%m.%y') + '.docx' - return os.path.join('lunch menu', filename) + filename = date.today().strftime("%d.%m.%y") + ".docx" + return os.path.join("lunch menu", filename) def save_attachment(msg): @@ -43,22 +52,22 @@ def save_attachment(msg): os.makedirs(os.path.dirname(file_name), exist_ok=True) for part in msg.walk(): - if part.get_content_maintype() == 'multipart': + if part.get_content_maintype() == "multipart": continue - if part.get('Content-Disposition') is None: + if part.get("Content-Disposition") is None: continue if not os.path.exists(file_name): - with open(file_name, 'wb') as fp: + with open(file_name, "wb") as fp: fp.write(part.get_payload(decode=True)) else: - logging.debug('Lunch menu file exist: %s', file_name) + logging.debug("Lunch menu file exist: %s", file_name) return file_name -def add_lunch_email_info(msg, file_name): +def add_lunch_email_info(msg, file_name) -> None: """ Функция для добавления в поле comments docx информации о письме. @@ -68,24 +77,20 @@ def add_lunch_email_info(msg, file_name): """ try: - import email - # Получаем заголовок письма - data, charset = email.header.decode_header(msg['Subject'])[0] - subject = data.decode(charset).strip().replace('\t', ' ') + data, charset = email.header.decode_header(msg["Subject"])[0] + subject = data.decode(charset).strip().replace("\t", " ") logging.debug('Subject: "%s".', subject) # Получаем дату получения (отправления??) письма - from datetime import datetime - date_tuple = email.utils.parsedate_tz(msg['Date']) + date_tuple = email.utils.parsedate_tz(msg["Date"]) email_date = datetime.fromtimestamp(email.utils.mktime_tz(date_tuple)) email_date = email_date.strftime(config.header_date_format) logging.debug("Date: %s.", email_date) # Открываем документ и переписываем поле comments, добавляя в него свою информацию - from docx import Document document = Document(file_name) - document.core_properties.comments = subject + '\t' + email_date + document.core_properties.comments = subject + "\t" + email_date document.save(file_name) except BaseException as e: @@ -103,26 +108,24 @@ def get_last_lunch_menu(): # Проверка на то, что меню уже скачано file_name = get_current_lunch_file_name() if os.path.exists(file_name): - logging.debug('Lunch menu file already exist: %s', file_name) + logging.debug("Lunch menu file already exist: %s", file_name) return file_name - logging.debug('Check last email.') + logging.debug("Check last email.") - import imaplib connect = imaplib.IMAP4(config.smtp_server) connect.login(config.username, config.password) connect.select() - logging.debug('Search emails from %s.', config.lunch_email) + logging.debug("Search emails from %s.", config.lunch_email) - typ, msgnums = connect.search(None, '(HEADER From {})'.format(config.lunch_email)) + typ, msgnums = connect.search(None, f"(HEADER From {config.lunch_email})") last_id = msgnums[0].split()[-1] - typ, data = connect.fetch(last_id, '(RFC822)') + typ, data = connect.fetch(last_id, "(RFC822)") - import email msg = email.message_from_bytes(data[0][1]) - logging.debug('Save lunch file name: %s.', file_name) + logging.debug("Save lunch file name: %s.", file_name) file_name = save_attachment(msg) add_lunch_email_info(msg, file_name) @@ -132,12 +135,10 @@ def get_last_lunch_menu(): return file_name -from flask import Flask, render_template_string, request app = Flask(__name__) # Регулярка для поиска последовательностей пробелов: от двух подряд и более -import re -multi_space_pattern = re.compile(r'[ ]{2,}') +multi_space_pattern = re.compile(r"[ ]{2,}") def get_info_lunch_menu(): @@ -147,14 +148,14 @@ def get_info_lunch_menu(): if file_name is None: return rows - logging.debug('Read lunch file name: %s.', file_name) - from docx import Document + logging.debug("Read lunch file name: %s.", file_name) + document = Document(file_name) - subject, date = '', '' + subject, date = "", "" comments = document.core_properties.comments if comments: - parts = comments.split('\t', 2) + parts = comments.split("\t", 2) if len(parts) == 2: subject, date = parts @@ -164,16 +165,18 @@ def get_info_lunch_menu(): for table in document.tables: # Перебор начинается со второй строки, потому что, первая строка таблицы -- это строка "Обеденное меню" for row in table.rows[1:]: - name, weight, price = [multi_space_pattern.sub(' ', i.text.strip()) for i in row.cells] + name, weight, price = [ + multi_space_pattern.sub(" ", i.text.strip()) for i in row.cells + ] if name == weight == price or (not weight or not price): name = name.title() logging.debug(name) - rows.append((name, )) + rows.append((name,)) continue rows.append((name, weight, price)) - logging.debug('{} {} {}'.format(name, weight, price)) + logging.debug(f"{name} {weight} {price}") # Таблицы в меню дублируются break @@ -183,16 +186,17 @@ def get_info_lunch_menu(): @app.route("/") def index(): - logging.debug('/index from %s.', request.remote_addr) + logging.debug("/index from %s.", request.remote_addr) try: rows, subject, date = get_info_lunch_menu() except BaseException as e: logging.error(e) logging.error(traceback.format_exc()) - return 'Error: "{}".'.format(e), 500 + return f'Error: "{e}".', 500 - return render_template_string('''\ + return render_template_string( + """\ {{ subject }} @@ -222,10 +226,14 @@ def index(): - ''', rows=rows, subject=subject, date=date) + """, + rows=rows, + subject=subject, + date=date, + ) -if __name__ == '__main__': +if __name__ == "__main__": # Localhost app.run(port=5001) diff --git a/flask__webservers/show_my_ip/main.py b/flask__webservers/show_my_ip/main.py index 991ea4a38..8ff891a59 100644 --- a/flask__webservers/show_my_ip/main.py +++ b/flask__webservers/show_my_ip/main.py @@ -1,24 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, request -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") -def index(): - return """Your IPv4 Address Is: {}""".format(request.remote_addr) +def index() -> str: + return f"""Your IPv4 Address Is: {request.remote_addr}""" -if __name__ == '__main__': +if __name__ == "__main__": # # Localhost # app.run(port=5001) # Public IP - app.run(host='0.0.0.0') + app.run(host="0.0.0.0") diff --git a/flask__webservers/show_random_abusive/main.py b/flask__webservers/show_random_abusive/main.py index 9c2410772..5947db507 100644 --- a/flask__webservers/show_random_abusive/main.py +++ b/flask__webservers/show_random_abusive/main.py @@ -1,40 +1,41 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging +import os +import sys + from flask import Flask, render_template_string, request, redirect -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) # Добавление пути основной папки репозитория, чтобы импортировать модуль random_abusive -import os dir = os.path.dirname(__file__) dir = os.path.dirname(dir) dir = os.path.dirname(dir) - -import sys sys.path.append(dir) - import random_abusive @app.route("/") def index(): if not request.args: - return redirect('/?number=10&chain=2') + return redirect("/?number=10&chain=2") - print('query_string:', request.query_string) - number = int(request.args['number']) - chain = int(request.args['chain']) + print("query_string:", request.query_string) + number = int(request.args["number"]) + chain = int(request.args["chain"]) words = random_abusive.get_words(number, chain) - print('words:', words) + print("words:", words) - return render_template_string('''\ + return render_template_string( + """\ Рандомные матерные слова @@ -44,7 +45,9 @@ def index(): - ''', words=words) + """, + words=words, + ) if __name__ == "__main__": diff --git a/flask__webservers/show_screenshot/main.py b/flask__webservers/show_screenshot/main.py index 58b8f0918..29f52c27d 100644 --- a/flask__webservers/show_screenshot/main.py +++ b/flask__webservers/show_screenshot/main.py @@ -1,30 +1,34 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from flask import Flask, render_template_string -app = Flask(__name__) - import logging -logging.basicConfig(level=logging.DEBUG) + +from flask import Flask, render_template_string import pyscreenshot as ImageGrab +app = Flask(__name__) +logging.basicConfig(level=logging.DEBUG) + + @app.route("/") def index(): - im = ImageGrab.grab() - im.save('static/screenshot.png') + img = ImageGrab.grab() + img.save("static/screenshot.png") - return render_template_string('''\ + return render_template_string( + """\ Show screenshot

- ''') + """ + ) if __name__ == "__main__": diff --git a/flask__webservers/show_your_user_agent.py b/flask__webservers/show_your_user_agent.py index 96aa46a87..addb8a263 100644 --- a/flask__webservers/show_your_user_agent.py +++ b/flask__webservers/show_your_user_agent.py @@ -1,23 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import logging from flask import Flask, request -app = Flask(__name__) -import logging + +app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) @app.route("/") def index(): - user_agent = request.headers['User-Agent'] + user_agent = request.headers["User-Agent"] print(user_agent) return user_agent -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/flask__webservers/simple_echo.py b/flask__webservers/simple_echo.py index e0a234438..5bd7bf429 100644 --- a/flask__webservers/simple_echo.py +++ b/flask__webservers/simple_echo.py @@ -1,25 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from flask import Flask, request, Response + + app = Flask(__name__) -@app.route('/', methods=['GET', 'POST']) +@app.route("/", methods=["GET", "POST"]) def index(): - print('-' * 20) - print('Method:', request.method) - print('Args:', dict(request.args)) - print('Data:', request.data) - print('Form:', request.form) - print('Headers:', dict(request.headers)) - print('-' * 20) + print("-" * 20) + print("Method:", request.method) + print("Args:", dict(request.args)) + print("Data:", request.data) + print("Form:", request.form) + print("Headers:", dict(request.headers)) + print("-" * 20) return Response( response=request.data, - content_type=request.headers.get('Content-Type') + content_type=request.headers.get("Content-Type"), ) diff --git a/flask__webservers/simple_upload_server/main.py b/flask__webservers/simple_upload_server/main.py index 82d65ec3c..2f5faf9aa 100644 --- a/flask__webservers/simple_upload_server/main.py +++ b/flask__webservers/simple_upload_server/main.py @@ -1,24 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from flask import Flask, request + + app = Flask(__name__) -@app.route("/", methods=['POST']) -def index(): - file = request.files['file'] +@app.route("/", methods=["POST"]) +def index() -> str: + file = request.files["file"] # file.save(file.filename) # OR: - file.save('upload_file.txt') + file.save("upload_file.txt") - return 'ok' + return "ok" -if __name__ == '__main__': +if __name__ == "__main__": app.run( port=6000, ) diff --git a/flask__webservers/simple_upload_server/test.py b/flask__webservers/simple_upload_server/test.py index 0bacceb12..f4584925a 100644 --- a/flask__webservers/simple_upload_server/test.py +++ b/flask__webservers/simple_upload_server/test.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Отправка файла на сервер import requests -rs = requests.post('http://localhost:6000', files={'file': open('disk_info.txt', 'rb')}) + + +rs = requests.post("http://localhost:6000", files={"file": open("disk_info.txt", "rb")}) print(rs) diff --git a/flask__webservers/upload_and_download_files/main.py b/flask__webservers/upload_and_download_files/main.py index bab1d66b9..f92fabaf2 100644 --- a/flask__webservers/upload_and_download_files/main.py +++ b/flask__webservers/upload_and_download_files/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os @@ -10,41 +10,41 @@ from werkzeug.utils import secure_filename -UPLOAD_FOLDER = 'uploads' +UPLOAD_FOLDER = "uploads" UPLOAD_FOLDER = os.path.abspath(UPLOAD_FOLDER) app = Flask(__name__) -app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER +app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER os.makedirs(UPLOAD_FOLDER, exist_ok=True) -@app.route('/', methods=['GET', 'POST']) +@app.route("/", methods=["GET", "POST"]) def upload_file(): - if request.method == 'POST': + if request.method == "POST": # check if the post request has the file part - if 'file' not in request.files: - flash('No file part') + if "file" not in request.files: + flash("No file part") return redirect(request.url) - file = request.files['file'] + file = request.files["file"] # if user does not select file, browser also # submit a empty part without filename - if file.filename == '': - flash('No selected file') + if file.filename == "": + flash("No selected file") return redirect(request.url) if file: # Функция secure_filename не дружит с не ascii-символами, поэтому # файлы русскими словами не называть filename = secure_filename(file.filename) - file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) + file.save(os.path.join(app.config["UPLOAD_FOLDER"], filename)) # Функция url_for('uploaded_file', filename=filename) возвращает строку вида: /uploads/ - return redirect(url_for('uploaded_file', filename=filename)) + return redirect(url_for("uploaded_file", filename=filename)) - return ''' + return """ Upload new File

Upload new File

@@ -52,17 +52,17 @@ def upload_file():

- ''' + """ # Пример обработчика, возвращающий файлы из папки app.config['UPLOAD_FOLDER'] для путей uploads и files. # т.е. не нужно давать специальное название, чтобы получить файл в flask # @app.route('/uploads/') -@app.route('/files/') +@app.route("/files/") def uploaded_file(filename): - return send_from_directory(app.config['UPLOAD_FOLDER'], filename) + return send_from_directory(app.config["UPLOAD_FOLDER"], filename) -if __name__ == '__main__': +if __name__ == "__main__": # Localhost app.run() diff --git a/flask__webservers/upload_and_download_files/test_download_from_script.py b/flask__webservers/upload_and_download_files/test_download_from_script.py index 5529ad71e..c52b6b141 100644 --- a/flask__webservers/upload_and_download_files/test_download_from_script.py +++ b/flask__webservers/upload_and_download_files/test_download_from_script.py @@ -1,34 +1,37 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os -import sys -sys.path.append('..') -from common import sizeof_fmt +# pip install humanize +from humanize import naturalsize as sizeof_fmt -def progress(count, block_size, total_size): +def progress(count, block_size, total_size) -> None: percent = count * block_size * 100.0 / total_size - print(f"Download: {sizeof_fmt(count * block_size)}/{sizeof_fmt(total_size)}({percent:.1f}%)" + ' ' * 20, end='\r') + print( + f"Download: {sizeof_fmt(count * block_size)}/{sizeof_fmt(total_size)}({percent:.1f}%)" + + " " * 20, + end="\r", + ) -def create_test_file(): - file_name = 'uploads/bigfile' +def create_test_file() -> None: + file_name = "uploads/bigfile" if os.path.exists(file_name): return # Создание больших файлов для теста - with open(file_name, 'wb') as f: + with open(file_name, "wb") as f: for i in range(1024 * 1024 * 600): - f.write(b'0') + f.write(b"0") -if __name__ == '__main__': +if __name__ == "__main__": create_test_file() from urllib.request import urlretrieve - urlretrieve('http://127.0.0.1:5000/files/bigfile', "file", reporthook=progress) + urlretrieve("http://127.0.0.1:5000/files/bigfile", "file", reporthook=progress) diff --git a/flask__webservers/url_shortener/config.py b/flask__webservers/url_shortener/config.py new file mode 100644 index 000000000..f23e91138 --- /dev/null +++ b/flask__webservers/url_shortener/config.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + + +# Текущая папка, где находится скрипт +DIR = Path(__file__).resolve().parent + +# Создание папки для базы данных +DB_DIR_NAME = DIR / "database" +DB_DIR_NAME.mkdir(parents=True, exist_ok=True) + +# Путь к файлу базы данных +DB_FILE_NAME = str(DB_DIR_NAME / "database.sqlite") + +# Максимальная длина идентификаторов ссылок +LENGTH_URL_ID: int = 5 diff --git a/flask__webservers/url_shortener/db.py b/flask__webservers/url_shortener/db.py new file mode 100644 index 000000000..2b8c3dc08 --- /dev/null +++ b/flask__webservers/url_shortener/db.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time + +from typing import Type, Optional, Iterable, TypeVar +from uuid import uuid4 + +# pip install peewee +from peewee import Model, TextField, ForeignKeyField, CharField +from playhouse.sqliteq import SqliteQueueDatabase + +from config import DB_FILE_NAME, LENGTH_URL_ID +from shorten import shorten + + +def generate_link_id(length: int = LENGTH_URL_ID) -> str: + # TODO: Возможна ситуация, когда length будет больше размера UUID + return uuid4().hex[:length] + + +class NotDefinedParameterException(Exception): + def __init__(self, parameter_name: str) -> None: + self.parameter_name = parameter_name + text = f'Parameter "{self.parameter_name}" must be defined!' + + super().__init__(text) + + +# This working with multithreading +# SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqliteq +db = SqliteQueueDatabase( + DB_FILE_NAME, + pragmas={ + "foreign_keys": 1, + "journal_mode": "wal", # WAL-mode + "cache_size": -1024 * 64, # 64MB page-cache + }, + use_gevent=False, # Use the standard library "threading" module. + autostart=True, + queue_max_size=64, # Max. # of pending writes that can accumulate. + results_timeout=5.0, # Max. time to wait for query to be executed. +) + + +ChildModel = TypeVar("ChildModel", bound="BaseModel") + + +class BaseModel(Model): + """ + Базовая модель классов-таблиц + """ + + class Meta: + database = db + + def get_new(self) -> ChildModel: + return type(self).get(self._pk_expr()) + + @classmethod + def get_first(cls) -> ChildModel: + return cls.select().first() + + @classmethod + def get_last(cls) -> ChildModel: + return cls.select().order_by(cls.id.desc()).first() + + @classmethod + def get_inherited_models(cls) -> list[Type["BaseModel"]]: + return sorted(cls.__subclasses__(), key=lambda x: x.__name__) + + @classmethod + def print_count_of_tables(cls) -> None: + items = [] + for sub_cls in cls.get_inherited_models(): + name = sub_cls.__name__ + count = sub_cls.select().count() + items.append(f"{name}: {count}") + + print(", ".join(items)) + + @classmethod + def count(cls, filters: Iterable = None) -> int: + query = cls.select() + if filters: + query = query.filter(*filters) + return query.count() + + def __str__(self) -> str: + fields = [] + for k, field in self._meta.fields.items(): + v = getattr(self, k) + + if isinstance(field, (TextField, CharField)): + if v: + v = repr(shorten(v)) + + elif isinstance(field, ForeignKeyField): + k = f"{k}_id" + if v: + v = v.id + + fields.append(f"{k}={v}") + + return self.__class__.__name__ + "(" + ", ".join(fields) + ")" + + +class Link(BaseModel): + link_id = TextField(primary_key=True) + link_url = TextField(unique=True) + + @classmethod + def get_by_link_url(cls, link_url: str) -> Optional["Link"]: + if not link_url or not link_url.strip(): + raise NotDefinedParameterException(parameter_name="link_url") + + return cls.get_or_none(cls.link_url == link_url) + + @classmethod + def get_by_link_id(cls, link_id: str) -> Optional["Link"]: + if not link_id or not link_id.strip(): + raise NotDefinedParameterException(parameter_name="link_id") + + return cls.get_or_none(cls.link_id == link_id) + + @classmethod + def add(cls, link_url: str) -> "Link": + obj = cls.get_by_link_url(link_url) + if not obj: + # Убеждаемся, что link_id будет уникальным + while True: # TODO: Возможна ситуация, когда свободные закончатся + link_id = generate_link_id() + if not cls.get_by_link_id(link_id): + break + + obj = cls.create( + link_id=link_id, + link_url=link_url, + ) + + return obj + + +db.connect() +db.create_tables(BaseModel.get_inherited_models()) + +# Задержка в 50мс, чтобы дать время на запуск SqliteQueueDatabase и создание таблиц +# Т.к. в SqliteQueueDatabase запросы на чтение выполняются сразу, а на запись попадают в очередь +time.sleep(0.050) + +if __name__ == "__main__": + BaseModel.print_count_of_tables() + # Link: 1 + print() + + link = Link.add(link_url="https://example.com") + assert Link.select().count() + assert link == link.get_by_link_url(link_url=link.link_url) + assert link == link.get_by_link_id(link_id=link.link_id) diff --git a/flask__webservers/url_shortener/main.py b/flask__webservers/url_shortener/main.py new file mode 100644 index 000000000..a048af41f --- /dev/null +++ b/flask__webservers/url_shortener/main.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from urllib.parse import urlparse, urljoin + +from flask import Flask, request, render_template, redirect, abort, jsonify + +import db + + +app = Flask(__name__) + + +def server_url() -> str: + parse = urlparse(request.url) + return f"{parse.scheme}://{parse.netloc}/" + + +@app.route("/", defaults={"link_id": ""}) +@app.route("/") +def index(link_id: str): + if link_id: + link = db.Link.get_by_link_id(link_id) + if not link: + abort(404) + + url = link.link_url + return redirect(url) + + return render_template( + "index.html", + title=Path(__file__).parent.resolve().stem, + ) + + +@app.route("/add", methods=["POST"]) +def add(): + result = { + "url": "", + } + if "url" in request.form: + link_url = request.form["url"] + link = db.Link.add(link_url) + result["url"] = urljoin(server_url(), link.link_id) + + return jsonify(result) + + +if __name__ == "__main__": + # # Localhost + # app.run(port=5001) + + # Public IP + app.run(host="0.0.0.0") diff --git a/flask__webservers/url_shortener/static/bootstrap-4.4.1/bootstrap.bundle.js b/flask__webservers/url_shortener/static/bootstrap-4.4.1/bootstrap.bundle.js new file mode 100644 index 000000000..534452210 --- /dev/null +++ b/flask__webservers/url_shortener/static/bootstrap-4.4.1/bootstrap.bundle.js @@ -0,0 +1,7134 @@ +/*! + * Bootstrap v4.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('jquery')) : + typeof define === 'function' && define.amd ? define(['exports', 'jquery'], factory) : + (global = global || self, factory(global.bootstrap = {}, global.jQuery)); +}(this, (function (exports, $) { 'use strict'; + + $ = $ && $.hasOwnProperty('default') ? $['default'] : $; + + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; + } + + function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; + } + + function ownKeys(object, enumerableOnly) { + var keys = Object.keys(object); + + if (Object.getOwnPropertySymbols) { + var symbols = Object.getOwnPropertySymbols(object); + if (enumerableOnly) symbols = symbols.filter(function (sym) { + return Object.getOwnPropertyDescriptor(object, sym).enumerable; + }); + keys.push.apply(keys, symbols); + } + + return keys; + } + + function _objectSpread2(target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i] != null ? arguments[i] : {}; + + if (i % 2) { + ownKeys(Object(source), true).forEach(function (key) { + _defineProperty(target, key, source[key]); + }); + } else if (Object.getOwnPropertyDescriptors) { + Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + } else { + ownKeys(Object(source)).forEach(function (key) { + Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); + }); + } + } + + return target; + } + + function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + subClass.__proto__ = superClass; + } + + /** + * -------------------------------------------------------------------------- + * Bootstrap (v4.4.1): util.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + /** + * ------------------------------------------------------------------------ + * Private TransitionEnd Helpers + * ------------------------------------------------------------------------ + */ + + var TRANSITION_END = 'transitionend'; + var MAX_UID = 1000000; + var MILLISECONDS_MULTIPLIER = 1000; // Shoutout AngusCroll (https://goo.gl/pxwQGp) + + function toType(obj) { + return {}.toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase(); + } + + function getSpecialTransitionEndEvent() { + return { + bindType: TRANSITION_END, + delegateType: TRANSITION_END, + handle: function handle(event) { + if ($(event.target).is(this)) { + return event.handleObj.handler.apply(this, arguments); // eslint-disable-line prefer-rest-params + } + + return undefined; // eslint-disable-line no-undefined + } + }; + } + + function transitionEndEmulator(duration) { + var _this = this; + + var called = false; + $(this).one(Util.TRANSITION_END, function () { + called = true; + }); + setTimeout(function () { + if (!called) { + Util.triggerTransitionEnd(_this); + } + }, duration); + return this; + } + + function setTransitionEndSupport() { + $.fn.emulateTransitionEnd = transitionEndEmulator; + $.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent(); + } + /** + * -------------------------------------------------------------------------- + * Public Util Api + * -------------------------------------------------------------------------- + */ + + + var Util = { + TRANSITION_END: 'bsTransitionEnd', + getUID: function getUID(prefix) { + do { + // eslint-disable-next-line no-bitwise + prefix += ~~(Math.random() * MAX_UID); // "~~" acts like a faster Math.floor() here + } while (document.getElementById(prefix)); + + return prefix; + }, + getSelectorFromElement: function getSelectorFromElement(element) { + var selector = element.getAttribute('data-target'); + + if (!selector || selector === '#') { + var hrefAttr = element.getAttribute('href'); + selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : ''; + } + + try { + return document.querySelector(selector) ? selector : null; + } catch (err) { + return null; + } + }, + getTransitionDurationFromElement: function getTransitionDurationFromElement(element) { + if (!element) { + return 0; + } // Get transition-duration of the element + + + var transitionDuration = $(element).css('transition-duration'); + var transitionDelay = $(element).css('transition-delay'); + var floatTransitionDuration = parseFloat(transitionDuration); + var floatTransitionDelay = parseFloat(transitionDelay); // Return 0 if element or transition duration is not found + + if (!floatTransitionDuration && !floatTransitionDelay) { + return 0; + } // If multiple durations are defined, take the first + + + transitionDuration = transitionDuration.split(',')[0]; + transitionDelay = transitionDelay.split(',')[0]; + return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER; + }, + reflow: function reflow(element) { + return element.offsetHeight; + }, + triggerTransitionEnd: function triggerTransitionEnd(element) { + $(element).trigger(TRANSITION_END); + }, + // TODO: Remove in v5 + supportsTransitionEnd: function supportsTransitionEnd() { + return Boolean(TRANSITION_END); + }, + isElement: function isElement(obj) { + return (obj[0] || obj).nodeType; + }, + typeCheckConfig: function typeCheckConfig(componentName, config, configTypes) { + for (var property in configTypes) { + if (Object.prototype.hasOwnProperty.call(configTypes, property)) { + var expectedTypes = configTypes[property]; + var value = config[property]; + var valueType = value && Util.isElement(value) ? 'element' : toType(value); + + if (!new RegExp(expectedTypes).test(valueType)) { + throw new Error(componentName.toUpperCase() + ": " + ("Option \"" + property + "\" provided type \"" + valueType + "\" ") + ("but expected type \"" + expectedTypes + "\".")); + } + } + } + }, + findShadowRoot: function findShadowRoot(element) { + if (!document.documentElement.attachShadow) { + return null; + } // Can find the shadow root otherwise it'll return the document + + + if (typeof element.getRootNode === 'function') { + var root = element.getRootNode(); + return root instanceof ShadowRoot ? root : null; + } + + if (element instanceof ShadowRoot) { + return element; + } // when we don't find a shadow root + + + if (!element.parentNode) { + return null; + } + + return Util.findShadowRoot(element.parentNode); + }, + jQueryDetection: function jQueryDetection() { + if (typeof $ === 'undefined') { + throw new TypeError('Bootstrap\'s JavaScript requires jQuery. jQuery must be included before Bootstrap\'s JavaScript.'); + } + + var version = $.fn.jquery.split(' ')[0].split('.'); + var minMajor = 1; + var ltMajor = 2; + var minMinor = 9; + var minPatch = 1; + var maxMajor = 4; + + if (version[0] < ltMajor && version[1] < minMinor || version[0] === minMajor && version[1] === minMinor && version[2] < minPatch || version[0] >= maxMajor) { + throw new Error('Bootstrap\'s JavaScript requires at least jQuery v1.9.1 but less than v4.0.0'); + } + } + }; + Util.jQueryDetection(); + setTransitionEndSupport(); + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME = 'alert'; + var VERSION = '4.4.1'; + var DATA_KEY = 'bs.alert'; + var EVENT_KEY = "." + DATA_KEY; + var DATA_API_KEY = '.data-api'; + var JQUERY_NO_CONFLICT = $.fn[NAME]; + var Selector = { + DISMISS: '[data-dismiss="alert"]' + }; + var Event = { + CLOSE: "close" + EVENT_KEY, + CLOSED: "closed" + EVENT_KEY, + CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY + }; + var ClassName = { + ALERT: 'alert', + FADE: 'fade', + SHOW: 'show' + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Alert = + /*#__PURE__*/ + function () { + function Alert(element) { + this._element = element; + } // Getters + + + var _proto = Alert.prototype; + + // Public + _proto.close = function close(element) { + var rootElement = this._element; + + if (element) { + rootElement = this._getRootElement(element); + } + + var customEvent = this._triggerCloseEvent(rootElement); + + if (customEvent.isDefaultPrevented()) { + return; + } + + this._removeElement(rootElement); + }; + + _proto.dispose = function dispose() { + $.removeData(this._element, DATA_KEY); + this._element = null; + } // Private + ; + + _proto._getRootElement = function _getRootElement(element) { + var selector = Util.getSelectorFromElement(element); + var parent = false; + + if (selector) { + parent = document.querySelector(selector); + } + + if (!parent) { + parent = $(element).closest("." + ClassName.ALERT)[0]; + } + + return parent; + }; + + _proto._triggerCloseEvent = function _triggerCloseEvent(element) { + var closeEvent = $.Event(Event.CLOSE); + $(element).trigger(closeEvent); + return closeEvent; + }; + + _proto._removeElement = function _removeElement(element) { + var _this = this; + + $(element).removeClass(ClassName.SHOW); + + if (!$(element).hasClass(ClassName.FADE)) { + this._destroyElement(element); + + return; + } + + var transitionDuration = Util.getTransitionDurationFromElement(element); + $(element).one(Util.TRANSITION_END, function (event) { + return _this._destroyElement(element, event); + }).emulateTransitionEnd(transitionDuration); + }; + + _proto._destroyElement = function _destroyElement(element) { + $(element).detach().trigger(Event.CLOSED).remove(); + } // Static + ; + + Alert._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var $element = $(this); + var data = $element.data(DATA_KEY); + + if (!data) { + data = new Alert(this); + $element.data(DATA_KEY, data); + } + + if (config === 'close') { + data[config](this); + } + }); + }; + + Alert._handleDismiss = function _handleDismiss(alertInstance) { + return function (event) { + if (event) { + event.preventDefault(); + } + + alertInstance.close(this); + }; + }; + + _createClass(Alert, null, [{ + key: "VERSION", + get: function get() { + return VERSION; + } + }]); + + return Alert; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $(document).on(Event.CLICK_DATA_API, Selector.DISMISS, Alert._handleDismiss(new Alert())); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $.fn[NAME] = Alert._jQueryInterface; + $.fn[NAME].Constructor = Alert; + + $.fn[NAME].noConflict = function () { + $.fn[NAME] = JQUERY_NO_CONFLICT; + return Alert._jQueryInterface; + }; + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$1 = 'button'; + var VERSION$1 = '4.4.1'; + var DATA_KEY$1 = 'bs.button'; + var EVENT_KEY$1 = "." + DATA_KEY$1; + var DATA_API_KEY$1 = '.data-api'; + var JQUERY_NO_CONFLICT$1 = $.fn[NAME$1]; + var ClassName$1 = { + ACTIVE: 'active', + BUTTON: 'btn', + FOCUS: 'focus' + }; + var Selector$1 = { + DATA_TOGGLE_CARROT: '[data-toggle^="button"]', + DATA_TOGGLES: '[data-toggle="buttons"]', + DATA_TOGGLE: '[data-toggle="button"]', + DATA_TOGGLES_BUTTONS: '[data-toggle="buttons"] .btn', + INPUT: 'input:not([type="hidden"])', + ACTIVE: '.active', + BUTTON: '.btn' + }; + var Event$1 = { + CLICK_DATA_API: "click" + EVENT_KEY$1 + DATA_API_KEY$1, + FOCUS_BLUR_DATA_API: "focus" + EVENT_KEY$1 + DATA_API_KEY$1 + " " + ("blur" + EVENT_KEY$1 + DATA_API_KEY$1), + LOAD_DATA_API: "load" + EVENT_KEY$1 + DATA_API_KEY$1 + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Button = + /*#__PURE__*/ + function () { + function Button(element) { + this._element = element; + } // Getters + + + var _proto = Button.prototype; + + // Public + _proto.toggle = function toggle() { + var triggerChangeEvent = true; + var addAriaPressed = true; + var rootElement = $(this._element).closest(Selector$1.DATA_TOGGLES)[0]; + + if (rootElement) { + var input = this._element.querySelector(Selector$1.INPUT); + + if (input) { + if (input.type === 'radio') { + if (input.checked && this._element.classList.contains(ClassName$1.ACTIVE)) { + triggerChangeEvent = false; + } else { + var activeElement = rootElement.querySelector(Selector$1.ACTIVE); + + if (activeElement) { + $(activeElement).removeClass(ClassName$1.ACTIVE); + } + } + } else if (input.type === 'checkbox') { + if (this._element.tagName === 'LABEL' && input.checked === this._element.classList.contains(ClassName$1.ACTIVE)) { + triggerChangeEvent = false; + } + } else { + // if it's not a radio button or checkbox don't add a pointless/invalid checked property to the input + triggerChangeEvent = false; + } + + if (triggerChangeEvent) { + input.checked = !this._element.classList.contains(ClassName$1.ACTIVE); + $(input).trigger('change'); + } + + input.focus(); + addAriaPressed = false; + } + } + + if (!(this._element.hasAttribute('disabled') || this._element.classList.contains('disabled'))) { + if (addAriaPressed) { + this._element.setAttribute('aria-pressed', !this._element.classList.contains(ClassName$1.ACTIVE)); + } + + if (triggerChangeEvent) { + $(this._element).toggleClass(ClassName$1.ACTIVE); + } + } + }; + + _proto.dispose = function dispose() { + $.removeData(this._element, DATA_KEY$1); + this._element = null; + } // Static + ; + + Button._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $(this).data(DATA_KEY$1); + + if (!data) { + data = new Button(this); + $(this).data(DATA_KEY$1, data); + } + + if (config === 'toggle') { + data[config](); + } + }); + }; + + _createClass(Button, null, [{ + key: "VERSION", + get: function get() { + return VERSION$1; + } + }]); + + return Button; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $(document).on(Event$1.CLICK_DATA_API, Selector$1.DATA_TOGGLE_CARROT, function (event) { + var button = event.target; + + if (!$(button).hasClass(ClassName$1.BUTTON)) { + button = $(button).closest(Selector$1.BUTTON)[0]; + } + + if (!button || button.hasAttribute('disabled') || button.classList.contains('disabled')) { + event.preventDefault(); // work around Firefox bug #1540995 + } else { + var inputBtn = button.querySelector(Selector$1.INPUT); + + if (inputBtn && (inputBtn.hasAttribute('disabled') || inputBtn.classList.contains('disabled'))) { + event.preventDefault(); // work around Firefox bug #1540995 + + return; + } + + Button._jQueryInterface.call($(button), 'toggle'); + } + }).on(Event$1.FOCUS_BLUR_DATA_API, Selector$1.DATA_TOGGLE_CARROT, function (event) { + var button = $(event.target).closest(Selector$1.BUTTON)[0]; + $(button).toggleClass(ClassName$1.FOCUS, /^focus(in)?$/.test(event.type)); + }); + $(window).on(Event$1.LOAD_DATA_API, function () { + // ensure correct active class is set to match the controls' actual values/states + // find all checkboxes/readio buttons inside data-toggle groups + var buttons = [].slice.call(document.querySelectorAll(Selector$1.DATA_TOGGLES_BUTTONS)); + + for (var i = 0, len = buttons.length; i < len; i++) { + var button = buttons[i]; + var input = button.querySelector(Selector$1.INPUT); + + if (input.checked || input.hasAttribute('checked')) { + button.classList.add(ClassName$1.ACTIVE); + } else { + button.classList.remove(ClassName$1.ACTIVE); + } + } // find all button toggles + + + buttons = [].slice.call(document.querySelectorAll(Selector$1.DATA_TOGGLE)); + + for (var _i = 0, _len = buttons.length; _i < _len; _i++) { + var _button = buttons[_i]; + + if (_button.getAttribute('aria-pressed') === 'true') { + _button.classList.add(ClassName$1.ACTIVE); + } else { + _button.classList.remove(ClassName$1.ACTIVE); + } + } + }); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $.fn[NAME$1] = Button._jQueryInterface; + $.fn[NAME$1].Constructor = Button; + + $.fn[NAME$1].noConflict = function () { + $.fn[NAME$1] = JQUERY_NO_CONFLICT$1; + return Button._jQueryInterface; + }; + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$2 = 'carousel'; + var VERSION$2 = '4.4.1'; + var DATA_KEY$2 = 'bs.carousel'; + var EVENT_KEY$2 = "." + DATA_KEY$2; + var DATA_API_KEY$2 = '.data-api'; + var JQUERY_NO_CONFLICT$2 = $.fn[NAME$2]; + var ARROW_LEFT_KEYCODE = 37; // KeyboardEvent.which value for left arrow key + + var ARROW_RIGHT_KEYCODE = 39; // KeyboardEvent.which value for right arrow key + + var TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch + + var SWIPE_THRESHOLD = 40; + var Default = { + interval: 5000, + keyboard: true, + slide: false, + pause: 'hover', + wrap: true, + touch: true + }; + var DefaultType = { + interval: '(number|boolean)', + keyboard: 'boolean', + slide: '(boolean|string)', + pause: '(string|boolean)', + wrap: 'boolean', + touch: 'boolean' + }; + var Direction = { + NEXT: 'next', + PREV: 'prev', + LEFT: 'left', + RIGHT: 'right' + }; + var Event$2 = { + SLIDE: "slide" + EVENT_KEY$2, + SLID: "slid" + EVENT_KEY$2, + KEYDOWN: "keydown" + EVENT_KEY$2, + MOUSEENTER: "mouseenter" + EVENT_KEY$2, + MOUSELEAVE: "mouseleave" + EVENT_KEY$2, + TOUCHSTART: "touchstart" + EVENT_KEY$2, + TOUCHMOVE: "touchmove" + EVENT_KEY$2, + TOUCHEND: "touchend" + EVENT_KEY$2, + POINTERDOWN: "pointerdown" + EVENT_KEY$2, + POINTERUP: "pointerup" + EVENT_KEY$2, + DRAG_START: "dragstart" + EVENT_KEY$2, + LOAD_DATA_API: "load" + EVENT_KEY$2 + DATA_API_KEY$2, + CLICK_DATA_API: "click" + EVENT_KEY$2 + DATA_API_KEY$2 + }; + var ClassName$2 = { + CAROUSEL: 'carousel', + ACTIVE: 'active', + SLIDE: 'slide', + RIGHT: 'carousel-item-right', + LEFT: 'carousel-item-left', + NEXT: 'carousel-item-next', + PREV: 'carousel-item-prev', + ITEM: 'carousel-item', + POINTER_EVENT: 'pointer-event' + }; + var Selector$2 = { + ACTIVE: '.active', + ACTIVE_ITEM: '.active.carousel-item', + ITEM: '.carousel-item', + ITEM_IMG: '.carousel-item img', + NEXT_PREV: '.carousel-item-next, .carousel-item-prev', + INDICATORS: '.carousel-indicators', + DATA_SLIDE: '[data-slide], [data-slide-to]', + DATA_RIDE: '[data-ride="carousel"]' + }; + var PointerType = { + TOUCH: 'touch', + PEN: 'pen' + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Carousel = + /*#__PURE__*/ + function () { + function Carousel(element, config) { + this._items = null; + this._interval = null; + this._activeElement = null; + this._isPaused = false; + this._isSliding = false; + this.touchTimeout = null; + this.touchStartX = 0; + this.touchDeltaX = 0; + this._config = this._getConfig(config); + this._element = element; + this._indicatorsElement = this._element.querySelector(Selector$2.INDICATORS); + this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0; + this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent); + + this._addEventListeners(); + } // Getters + + + var _proto = Carousel.prototype; + + // Public + _proto.next = function next() { + if (!this._isSliding) { + this._slide(Direction.NEXT); + } + }; + + _proto.nextWhenVisible = function nextWhenVisible() { + // Don't call next when the page isn't visible + // or the carousel or its parent isn't visible + if (!document.hidden && $(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden') { + this.next(); + } + }; + + _proto.prev = function prev() { + if (!this._isSliding) { + this._slide(Direction.PREV); + } + }; + + _proto.pause = function pause(event) { + if (!event) { + this._isPaused = true; + } + + if (this._element.querySelector(Selector$2.NEXT_PREV)) { + Util.triggerTransitionEnd(this._element); + this.cycle(true); + } + + clearInterval(this._interval); + this._interval = null; + }; + + _proto.cycle = function cycle(event) { + if (!event) { + this._isPaused = false; + } + + if (this._interval) { + clearInterval(this._interval); + this._interval = null; + } + + if (this._config.interval && !this._isPaused) { + this._interval = setInterval((document.visibilityState ? this.nextWhenVisible : this.next).bind(this), this._config.interval); + } + }; + + _proto.to = function to(index) { + var _this = this; + + this._activeElement = this._element.querySelector(Selector$2.ACTIVE_ITEM); + + var activeIndex = this._getItemIndex(this._activeElement); + + if (index > this._items.length - 1 || index < 0) { + return; + } + + if (this._isSliding) { + $(this._element).one(Event$2.SLID, function () { + return _this.to(index); + }); + return; + } + + if (activeIndex === index) { + this.pause(); + this.cycle(); + return; + } + + var direction = index > activeIndex ? Direction.NEXT : Direction.PREV; + + this._slide(direction, this._items[index]); + }; + + _proto.dispose = function dispose() { + $(this._element).off(EVENT_KEY$2); + $.removeData(this._element, DATA_KEY$2); + this._items = null; + this._config = null; + this._element = null; + this._interval = null; + this._isPaused = null; + this._isSliding = null; + this._activeElement = null; + this._indicatorsElement = null; + } // Private + ; + + _proto._getConfig = function _getConfig(config) { + config = _objectSpread2({}, Default, {}, config); + Util.typeCheckConfig(NAME$2, config, DefaultType); + return config; + }; + + _proto._handleSwipe = function _handleSwipe() { + var absDeltax = Math.abs(this.touchDeltaX); + + if (absDeltax <= SWIPE_THRESHOLD) { + return; + } + + var direction = absDeltax / this.touchDeltaX; + this.touchDeltaX = 0; // swipe left + + if (direction > 0) { + this.prev(); + } // swipe right + + + if (direction < 0) { + this.next(); + } + }; + + _proto._addEventListeners = function _addEventListeners() { + var _this2 = this; + + if (this._config.keyboard) { + $(this._element).on(Event$2.KEYDOWN, function (event) { + return _this2._keydown(event); + }); + } + + if (this._config.pause === 'hover') { + $(this._element).on(Event$2.MOUSEENTER, function (event) { + return _this2.pause(event); + }).on(Event$2.MOUSELEAVE, function (event) { + return _this2.cycle(event); + }); + } + + if (this._config.touch) { + this._addTouchEventListeners(); + } + }; + + _proto._addTouchEventListeners = function _addTouchEventListeners() { + var _this3 = this; + + if (!this._touchSupported) { + return; + } + + var start = function start(event) { + if (_this3._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) { + _this3.touchStartX = event.originalEvent.clientX; + } else if (!_this3._pointerEvent) { + _this3.touchStartX = event.originalEvent.touches[0].clientX; + } + }; + + var move = function move(event) { + // ensure swiping with one touch and not pinching + if (event.originalEvent.touches && event.originalEvent.touches.length > 1) { + _this3.touchDeltaX = 0; + } else { + _this3.touchDeltaX = event.originalEvent.touches[0].clientX - _this3.touchStartX; + } + }; + + var end = function end(event) { + if (_this3._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) { + _this3.touchDeltaX = event.originalEvent.clientX - _this3.touchStartX; + } + + _this3._handleSwipe(); + + if (_this3._config.pause === 'hover') { + // If it's a touch-enabled device, mouseenter/leave are fired as + // part of the mouse compatibility events on first tap - the carousel + // would stop cycling until user tapped out of it; + // here, we listen for touchend, explicitly pause the carousel + // (as if it's the second time we tap on it, mouseenter compat event + // is NOT fired) and after a timeout (to allow for mouse compatibility + // events to fire) we explicitly restart cycling + _this3.pause(); + + if (_this3.touchTimeout) { + clearTimeout(_this3.touchTimeout); + } + + _this3.touchTimeout = setTimeout(function (event) { + return _this3.cycle(event); + }, TOUCHEVENT_COMPAT_WAIT + _this3._config.interval); + } + }; + + $(this._element.querySelectorAll(Selector$2.ITEM_IMG)).on(Event$2.DRAG_START, function (e) { + return e.preventDefault(); + }); + + if (this._pointerEvent) { + $(this._element).on(Event$2.POINTERDOWN, function (event) { + return start(event); + }); + $(this._element).on(Event$2.POINTERUP, function (event) { + return end(event); + }); + + this._element.classList.add(ClassName$2.POINTER_EVENT); + } else { + $(this._element).on(Event$2.TOUCHSTART, function (event) { + return start(event); + }); + $(this._element).on(Event$2.TOUCHMOVE, function (event) { + return move(event); + }); + $(this._element).on(Event$2.TOUCHEND, function (event) { + return end(event); + }); + } + }; + + _proto._keydown = function _keydown(event) { + if (/input|textarea/i.test(event.target.tagName)) { + return; + } + + switch (event.which) { + case ARROW_LEFT_KEYCODE: + event.preventDefault(); + this.prev(); + break; + + case ARROW_RIGHT_KEYCODE: + event.preventDefault(); + this.next(); + break; + } + }; + + _proto._getItemIndex = function _getItemIndex(element) { + this._items = element && element.parentNode ? [].slice.call(element.parentNode.querySelectorAll(Selector$2.ITEM)) : []; + return this._items.indexOf(element); + }; + + _proto._getItemByDirection = function _getItemByDirection(direction, activeElement) { + var isNextDirection = direction === Direction.NEXT; + var isPrevDirection = direction === Direction.PREV; + + var activeIndex = this._getItemIndex(activeElement); + + var lastItemIndex = this._items.length - 1; + var isGoingToWrap = isPrevDirection && activeIndex === 0 || isNextDirection && activeIndex === lastItemIndex; + + if (isGoingToWrap && !this._config.wrap) { + return activeElement; + } + + var delta = direction === Direction.PREV ? -1 : 1; + var itemIndex = (activeIndex + delta) % this._items.length; + return itemIndex === -1 ? this._items[this._items.length - 1] : this._items[itemIndex]; + }; + + _proto._triggerSlideEvent = function _triggerSlideEvent(relatedTarget, eventDirectionName) { + var targetIndex = this._getItemIndex(relatedTarget); + + var fromIndex = this._getItemIndex(this._element.querySelector(Selector$2.ACTIVE_ITEM)); + + var slideEvent = $.Event(Event$2.SLIDE, { + relatedTarget: relatedTarget, + direction: eventDirectionName, + from: fromIndex, + to: targetIndex + }); + $(this._element).trigger(slideEvent); + return slideEvent; + }; + + _proto._setActiveIndicatorElement = function _setActiveIndicatorElement(element) { + if (this._indicatorsElement) { + var indicators = [].slice.call(this._indicatorsElement.querySelectorAll(Selector$2.ACTIVE)); + $(indicators).removeClass(ClassName$2.ACTIVE); + + var nextIndicator = this._indicatorsElement.children[this._getItemIndex(element)]; + + if (nextIndicator) { + $(nextIndicator).addClass(ClassName$2.ACTIVE); + } + } + }; + + _proto._slide = function _slide(direction, element) { + var _this4 = this; + + var activeElement = this._element.querySelector(Selector$2.ACTIVE_ITEM); + + var activeElementIndex = this._getItemIndex(activeElement); + + var nextElement = element || activeElement && this._getItemByDirection(direction, activeElement); + + var nextElementIndex = this._getItemIndex(nextElement); + + var isCycling = Boolean(this._interval); + var directionalClassName; + var orderClassName; + var eventDirectionName; + + if (direction === Direction.NEXT) { + directionalClassName = ClassName$2.LEFT; + orderClassName = ClassName$2.NEXT; + eventDirectionName = Direction.LEFT; + } else { + directionalClassName = ClassName$2.RIGHT; + orderClassName = ClassName$2.PREV; + eventDirectionName = Direction.RIGHT; + } + + if (nextElement && $(nextElement).hasClass(ClassName$2.ACTIVE)) { + this._isSliding = false; + return; + } + + var slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName); + + if (slideEvent.isDefaultPrevented()) { + return; + } + + if (!activeElement || !nextElement) { + // Some weirdness is happening, so we bail + return; + } + + this._isSliding = true; + + if (isCycling) { + this.pause(); + } + + this._setActiveIndicatorElement(nextElement); + + var slidEvent = $.Event(Event$2.SLID, { + relatedTarget: nextElement, + direction: eventDirectionName, + from: activeElementIndex, + to: nextElementIndex + }); + + if ($(this._element).hasClass(ClassName$2.SLIDE)) { + $(nextElement).addClass(orderClassName); + Util.reflow(nextElement); + $(activeElement).addClass(directionalClassName); + $(nextElement).addClass(directionalClassName); + var nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10); + + if (nextElementInterval) { + this._config.defaultInterval = this._config.defaultInterval || this._config.interval; + this._config.interval = nextElementInterval; + } else { + this._config.interval = this._config.defaultInterval || this._config.interval; + } + + var transitionDuration = Util.getTransitionDurationFromElement(activeElement); + $(activeElement).one(Util.TRANSITION_END, function () { + $(nextElement).removeClass(directionalClassName + " " + orderClassName).addClass(ClassName$2.ACTIVE); + $(activeElement).removeClass(ClassName$2.ACTIVE + " " + orderClassName + " " + directionalClassName); + _this4._isSliding = false; + setTimeout(function () { + return $(_this4._element).trigger(slidEvent); + }, 0); + }).emulateTransitionEnd(transitionDuration); + } else { + $(activeElement).removeClass(ClassName$2.ACTIVE); + $(nextElement).addClass(ClassName$2.ACTIVE); + this._isSliding = false; + $(this._element).trigger(slidEvent); + } + + if (isCycling) { + this.cycle(); + } + } // Static + ; + + Carousel._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $(this).data(DATA_KEY$2); + + var _config = _objectSpread2({}, Default, {}, $(this).data()); + + if (typeof config === 'object') { + _config = _objectSpread2({}, _config, {}, config); + } + + var action = typeof config === 'string' ? config : _config.slide; + + if (!data) { + data = new Carousel(this, _config); + $(this).data(DATA_KEY$2, data); + } + + if (typeof config === 'number') { + data.to(config); + } else if (typeof action === 'string') { + if (typeof data[action] === 'undefined') { + throw new TypeError("No method named \"" + action + "\""); + } + + data[action](); + } else if (_config.interval && _config.ride) { + data.pause(); + data.cycle(); + } + }); + }; + + Carousel._dataApiClickHandler = function _dataApiClickHandler(event) { + var selector = Util.getSelectorFromElement(this); + + if (!selector) { + return; + } + + var target = $(selector)[0]; + + if (!target || !$(target).hasClass(ClassName$2.CAROUSEL)) { + return; + } + + var config = _objectSpread2({}, $(target).data(), {}, $(this).data()); + + var slideIndex = this.getAttribute('data-slide-to'); + + if (slideIndex) { + config.interval = false; + } + + Carousel._jQueryInterface.call($(target), config); + + if (slideIndex) { + $(target).data(DATA_KEY$2).to(slideIndex); + } + + event.preventDefault(); + }; + + _createClass(Carousel, null, [{ + key: "VERSION", + get: function get() { + return VERSION$2; + } + }, { + key: "Default", + get: function get() { + return Default; + } + }]); + + return Carousel; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $(document).on(Event$2.CLICK_DATA_API, Selector$2.DATA_SLIDE, Carousel._dataApiClickHandler); + $(window).on(Event$2.LOAD_DATA_API, function () { + var carousels = [].slice.call(document.querySelectorAll(Selector$2.DATA_RIDE)); + + for (var i = 0, len = carousels.length; i < len; i++) { + var $carousel = $(carousels[i]); + + Carousel._jQueryInterface.call($carousel, $carousel.data()); + } + }); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $.fn[NAME$2] = Carousel._jQueryInterface; + $.fn[NAME$2].Constructor = Carousel; + + $.fn[NAME$2].noConflict = function () { + $.fn[NAME$2] = JQUERY_NO_CONFLICT$2; + return Carousel._jQueryInterface; + }; + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$3 = 'collapse'; + var VERSION$3 = '4.4.1'; + var DATA_KEY$3 = 'bs.collapse'; + var EVENT_KEY$3 = "." + DATA_KEY$3; + var DATA_API_KEY$3 = '.data-api'; + var JQUERY_NO_CONFLICT$3 = $.fn[NAME$3]; + var Default$1 = { + toggle: true, + parent: '' + }; + var DefaultType$1 = { + toggle: 'boolean', + parent: '(string|element)' + }; + var Event$3 = { + SHOW: "show" + EVENT_KEY$3, + SHOWN: "shown" + EVENT_KEY$3, + HIDE: "hide" + EVENT_KEY$3, + HIDDEN: "hidden" + EVENT_KEY$3, + CLICK_DATA_API: "click" + EVENT_KEY$3 + DATA_API_KEY$3 + }; + var ClassName$3 = { + SHOW: 'show', + COLLAPSE: 'collapse', + COLLAPSING: 'collapsing', + COLLAPSED: 'collapsed' + }; + var Dimension = { + WIDTH: 'width', + HEIGHT: 'height' + }; + var Selector$3 = { + ACTIVES: '.show, .collapsing', + DATA_TOGGLE: '[data-toggle="collapse"]' + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Collapse = + /*#__PURE__*/ + function () { + function Collapse(element, config) { + this._isTransitioning = false; + this._element = element; + this._config = this._getConfig(config); + this._triggerArray = [].slice.call(document.querySelectorAll("[data-toggle=\"collapse\"][href=\"#" + element.id + "\"]," + ("[data-toggle=\"collapse\"][data-target=\"#" + element.id + "\"]"))); + var toggleList = [].slice.call(document.querySelectorAll(Selector$3.DATA_TOGGLE)); + + for (var i = 0, len = toggleList.length; i < len; i++) { + var elem = toggleList[i]; + var selector = Util.getSelectorFromElement(elem); + var filterElement = [].slice.call(document.querySelectorAll(selector)).filter(function (foundElem) { + return foundElem === element; + }); + + if (selector !== null && filterElement.length > 0) { + this._selector = selector; + + this._triggerArray.push(elem); + } + } + + this._parent = this._config.parent ? this._getParent() : null; + + if (!this._config.parent) { + this._addAriaAndCollapsedClass(this._element, this._triggerArray); + } + + if (this._config.toggle) { + this.toggle(); + } + } // Getters + + + var _proto = Collapse.prototype; + + // Public + _proto.toggle = function toggle() { + if ($(this._element).hasClass(ClassName$3.SHOW)) { + this.hide(); + } else { + this.show(); + } + }; + + _proto.show = function show() { + var _this = this; + + if (this._isTransitioning || $(this._element).hasClass(ClassName$3.SHOW)) { + return; + } + + var actives; + var activesData; + + if (this._parent) { + actives = [].slice.call(this._parent.querySelectorAll(Selector$3.ACTIVES)).filter(function (elem) { + if (typeof _this._config.parent === 'string') { + return elem.getAttribute('data-parent') === _this._config.parent; + } + + return elem.classList.contains(ClassName$3.COLLAPSE); + }); + + if (actives.length === 0) { + actives = null; + } + } + + if (actives) { + activesData = $(actives).not(this._selector).data(DATA_KEY$3); + + if (activesData && activesData._isTransitioning) { + return; + } + } + + var startEvent = $.Event(Event$3.SHOW); + $(this._element).trigger(startEvent); + + if (startEvent.isDefaultPrevented()) { + return; + } + + if (actives) { + Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide'); + + if (!activesData) { + $(actives).data(DATA_KEY$3, null); + } + } + + var dimension = this._getDimension(); + + $(this._element).removeClass(ClassName$3.COLLAPSE).addClass(ClassName$3.COLLAPSING); + this._element.style[dimension] = 0; + + if (this._triggerArray.length) { + $(this._triggerArray).removeClass(ClassName$3.COLLAPSED).attr('aria-expanded', true); + } + + this.setTransitioning(true); + + var complete = function complete() { + $(_this._element).removeClass(ClassName$3.COLLAPSING).addClass(ClassName$3.COLLAPSE).addClass(ClassName$3.SHOW); + _this._element.style[dimension] = ''; + + _this.setTransitioning(false); + + $(_this._element).trigger(Event$3.SHOWN); + }; + + var capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); + var scrollSize = "scroll" + capitalizedDimension; + var transitionDuration = Util.getTransitionDurationFromElement(this._element); + $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration); + this._element.style[dimension] = this._element[scrollSize] + "px"; + }; + + _proto.hide = function hide() { + var _this2 = this; + + if (this._isTransitioning || !$(this._element).hasClass(ClassName$3.SHOW)) { + return; + } + + var startEvent = $.Event(Event$3.HIDE); + $(this._element).trigger(startEvent); + + if (startEvent.isDefaultPrevented()) { + return; + } + + var dimension = this._getDimension(); + + this._element.style[dimension] = this._element.getBoundingClientRect()[dimension] + "px"; + Util.reflow(this._element); + $(this._element).addClass(ClassName$3.COLLAPSING).removeClass(ClassName$3.COLLAPSE).removeClass(ClassName$3.SHOW); + var triggerArrayLength = this._triggerArray.length; + + if (triggerArrayLength > 0) { + for (var i = 0; i < triggerArrayLength; i++) { + var trigger = this._triggerArray[i]; + var selector = Util.getSelectorFromElement(trigger); + + if (selector !== null) { + var $elem = $([].slice.call(document.querySelectorAll(selector))); + + if (!$elem.hasClass(ClassName$3.SHOW)) { + $(trigger).addClass(ClassName$3.COLLAPSED).attr('aria-expanded', false); + } + } + } + } + + this.setTransitioning(true); + + var complete = function complete() { + _this2.setTransitioning(false); + + $(_this2._element).removeClass(ClassName$3.COLLAPSING).addClass(ClassName$3.COLLAPSE).trigger(Event$3.HIDDEN); + }; + + this._element.style[dimension] = ''; + var transitionDuration = Util.getTransitionDurationFromElement(this._element); + $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration); + }; + + _proto.setTransitioning = function setTransitioning(isTransitioning) { + this._isTransitioning = isTransitioning; + }; + + _proto.dispose = function dispose() { + $.removeData(this._element, DATA_KEY$3); + this._config = null; + this._parent = null; + this._element = null; + this._triggerArray = null; + this._isTransitioning = null; + } // Private + ; + + _proto._getConfig = function _getConfig(config) { + config = _objectSpread2({}, Default$1, {}, config); + config.toggle = Boolean(config.toggle); // Coerce string values + + Util.typeCheckConfig(NAME$3, config, DefaultType$1); + return config; + }; + + _proto._getDimension = function _getDimension() { + var hasWidth = $(this._element).hasClass(Dimension.WIDTH); + return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT; + }; + + _proto._getParent = function _getParent() { + var _this3 = this; + + var parent; + + if (Util.isElement(this._config.parent)) { + parent = this._config.parent; // It's a jQuery object + + if (typeof this._config.parent.jquery !== 'undefined') { + parent = this._config.parent[0]; + } + } else { + parent = document.querySelector(this._config.parent); + } + + var selector = "[data-toggle=\"collapse\"][data-parent=\"" + this._config.parent + "\"]"; + var children = [].slice.call(parent.querySelectorAll(selector)); + $(children).each(function (i, element) { + _this3._addAriaAndCollapsedClass(Collapse._getTargetFromElement(element), [element]); + }); + return parent; + }; + + _proto._addAriaAndCollapsedClass = function _addAriaAndCollapsedClass(element, triggerArray) { + var isOpen = $(element).hasClass(ClassName$3.SHOW); + + if (triggerArray.length) { + $(triggerArray).toggleClass(ClassName$3.COLLAPSED, !isOpen).attr('aria-expanded', isOpen); + } + } // Static + ; + + Collapse._getTargetFromElement = function _getTargetFromElement(element) { + var selector = Util.getSelectorFromElement(element); + return selector ? document.querySelector(selector) : null; + }; + + Collapse._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var $this = $(this); + var data = $this.data(DATA_KEY$3); + + var _config = _objectSpread2({}, Default$1, {}, $this.data(), {}, typeof config === 'object' && config ? config : {}); + + if (!data && _config.toggle && /show|hide/.test(config)) { + _config.toggle = false; + } + + if (!data) { + data = new Collapse(this, _config); + $this.data(DATA_KEY$3, data); + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError("No method named \"" + config + "\""); + } + + data[config](); + } + }); + }; + + _createClass(Collapse, null, [{ + key: "VERSION", + get: function get() { + return VERSION$3; + } + }, { + key: "Default", + get: function get() { + return Default$1; + } + }]); + + return Collapse; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $(document).on(Event$3.CLICK_DATA_API, Selector$3.DATA_TOGGLE, function (event) { + // preventDefault only for elements (which change the URL) not inside the collapsible element + if (event.currentTarget.tagName === 'A') { + event.preventDefault(); + } + + var $trigger = $(this); + var selector = Util.getSelectorFromElement(this); + var selectors = [].slice.call(document.querySelectorAll(selector)); + $(selectors).each(function () { + var $target = $(this); + var data = $target.data(DATA_KEY$3); + var config = data ? 'toggle' : $trigger.data(); + + Collapse._jQueryInterface.call($target, config); + }); + }); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $.fn[NAME$3] = Collapse._jQueryInterface; + $.fn[NAME$3].Constructor = Collapse; + + $.fn[NAME$3].noConflict = function () { + $.fn[NAME$3] = JQUERY_NO_CONFLICT$3; + return Collapse._jQueryInterface; + }; + + /**! + * @fileOverview Kickass library to create and place poppers near their reference elements. + * @version 1.16.0 + * @license + * Copyright (c) 2016 Federico Zivolo and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + var isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && typeof navigator !== 'undefined'; + + var timeoutDuration = function () { + var longerTimeoutBrowsers = ['Edge', 'Trident', 'Firefox']; + for (var i = 0; i < longerTimeoutBrowsers.length; i += 1) { + if (isBrowser && navigator.userAgent.indexOf(longerTimeoutBrowsers[i]) >= 0) { + return 1; + } + } + return 0; + }(); + + function microtaskDebounce(fn) { + var called = false; + return function () { + if (called) { + return; + } + called = true; + window.Promise.resolve().then(function () { + called = false; + fn(); + }); + }; + } + + function taskDebounce(fn) { + var scheduled = false; + return function () { + if (!scheduled) { + scheduled = true; + setTimeout(function () { + scheduled = false; + fn(); + }, timeoutDuration); + } + }; + } + + var supportsMicroTasks = isBrowser && window.Promise; + + /** + * Create a debounced version of a method, that's asynchronously deferred + * but called in the minimum time possible. + * + * @method + * @memberof Popper.Utils + * @argument {Function} fn + * @returns {Function} + */ + var debounce = supportsMicroTasks ? microtaskDebounce : taskDebounce; + + /** + * Check if the given variable is a function + * @method + * @memberof Popper.Utils + * @argument {Any} functionToCheck - variable to check + * @returns {Boolean} answer to: is a function? + */ + function isFunction(functionToCheck) { + var getType = {}; + return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; + } + + /** + * Get CSS computed property of the given element + * @method + * @memberof Popper.Utils + * @argument {Eement} element + * @argument {String} property + */ + function getStyleComputedProperty(element, property) { + if (element.nodeType !== 1) { + return []; + } + // NOTE: 1 DOM access here + var window = element.ownerDocument.defaultView; + var css = window.getComputedStyle(element, null); + return property ? css[property] : css; + } + + /** + * Returns the parentNode or the host of the element + * @method + * @memberof Popper.Utils + * @argument {Element} element + * @returns {Element} parent + */ + function getParentNode(element) { + if (element.nodeName === 'HTML') { + return element; + } + return element.parentNode || element.host; + } + + /** + * Returns the scrolling parent of the given element + * @method + * @memberof Popper.Utils + * @argument {Element} element + * @returns {Element} scroll parent + */ + function getScrollParent(element) { + // Return body, `getScroll` will take care to get the correct `scrollTop` from it + if (!element) { + return document.body; + } + + switch (element.nodeName) { + case 'HTML': + case 'BODY': + return element.ownerDocument.body; + case '#document': + return element.body; + } + + // Firefox want us to check `-x` and `-y` variations as well + + var _getStyleComputedProp = getStyleComputedProperty(element), + overflow = _getStyleComputedProp.overflow, + overflowX = _getStyleComputedProp.overflowX, + overflowY = _getStyleComputedProp.overflowY; + + if (/(auto|scroll|overlay)/.test(overflow + overflowY + overflowX)) { + return element; + } + + return getScrollParent(getParentNode(element)); + } + + /** + * Returns the reference node of the reference object, or the reference object itself. + * @method + * @memberof Popper.Utils + * @param {Element|Object} reference - the reference element (the popper will be relative to this) + * @returns {Element} parent + */ + function getReferenceNode(reference) { + return reference && reference.referenceNode ? reference.referenceNode : reference; + } + + var isIE11 = isBrowser && !!(window.MSInputMethodContext && document.documentMode); + var isIE10 = isBrowser && /MSIE 10/.test(navigator.userAgent); + + /** + * Determines if the browser is Internet Explorer + * @method + * @memberof Popper.Utils + * @param {Number} version to check + * @returns {Boolean} isIE + */ + function isIE(version) { + if (version === 11) { + return isIE11; + } + if (version === 10) { + return isIE10; + } + return isIE11 || isIE10; + } + + /** + * Returns the offset parent of the given element + * @method + * @memberof Popper.Utils + * @argument {Element} element + * @returns {Element} offset parent + */ + function getOffsetParent(element) { + if (!element) { + return document.documentElement; + } + + var noOffsetParent = isIE(10) ? document.body : null; + + // NOTE: 1 DOM access here + var offsetParent = element.offsetParent || null; + // Skip hidden elements which don't have an offsetParent + while (offsetParent === noOffsetParent && element.nextElementSibling) { + offsetParent = (element = element.nextElementSibling).offsetParent; + } + + var nodeName = offsetParent && offsetParent.nodeName; + + if (!nodeName || nodeName === 'BODY' || nodeName === 'HTML') { + return element ? element.ownerDocument.documentElement : document.documentElement; + } + + // .offsetParent will return the closest TH, TD or TABLE in case + // no offsetParent is present, I hate this job... + if (['TH', 'TD', 'TABLE'].indexOf(offsetParent.nodeName) !== -1 && getStyleComputedProperty(offsetParent, 'position') === 'static') { + return getOffsetParent(offsetParent); + } + + return offsetParent; + } + + function isOffsetContainer(element) { + var nodeName = element.nodeName; + + if (nodeName === 'BODY') { + return false; + } + return nodeName === 'HTML' || getOffsetParent(element.firstElementChild) === element; + } + + /** + * Finds the root node (document, shadowDOM root) of the given element + * @method + * @memberof Popper.Utils + * @argument {Element} node + * @returns {Element} root node + */ + function getRoot(node) { + if (node.parentNode !== null) { + return getRoot(node.parentNode); + } + + return node; + } + + /** + * Finds the offset parent common to the two provided nodes + * @method + * @memberof Popper.Utils + * @argument {Element} element1 + * @argument {Element} element2 + * @returns {Element} common offset parent + */ + function findCommonOffsetParent(element1, element2) { + // This check is needed to avoid errors in case one of the elements isn't defined for any reason + if (!element1 || !element1.nodeType || !element2 || !element2.nodeType) { + return document.documentElement; + } + + // Here we make sure to give as "start" the element that comes first in the DOM + var order = element1.compareDocumentPosition(element2) & Node.DOCUMENT_POSITION_FOLLOWING; + var start = order ? element1 : element2; + var end = order ? element2 : element1; + + // Get common ancestor container + var range = document.createRange(); + range.setStart(start, 0); + range.setEnd(end, 0); + var commonAncestorContainer = range.commonAncestorContainer; + + // Both nodes are inside #document + + if (element1 !== commonAncestorContainer && element2 !== commonAncestorContainer || start.contains(end)) { + if (isOffsetContainer(commonAncestorContainer)) { + return commonAncestorContainer; + } + + return getOffsetParent(commonAncestorContainer); + } + + // one of the nodes is inside shadowDOM, find which one + var element1root = getRoot(element1); + if (element1root.host) { + return findCommonOffsetParent(element1root.host, element2); + } else { + return findCommonOffsetParent(element1, getRoot(element2).host); + } + } + + /** + * Gets the scroll value of the given element in the given side (top and left) + * @method + * @memberof Popper.Utils + * @argument {Element} element + * @argument {String} side `top` or `left` + * @returns {number} amount of scrolled pixels + */ + function getScroll(element) { + var side = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'top'; + + var upperSide = side === 'top' ? 'scrollTop' : 'scrollLeft'; + var nodeName = element.nodeName; + + if (nodeName === 'BODY' || nodeName === 'HTML') { + var html = element.ownerDocument.documentElement; + var scrollingElement = element.ownerDocument.scrollingElement || html; + return scrollingElement[upperSide]; + } + + return element[upperSide]; + } + + /* + * Sum or subtract the element scroll values (left and top) from a given rect object + * @method + * @memberof Popper.Utils + * @param {Object} rect - Rect object you want to change + * @param {HTMLElement} element - The element from the function reads the scroll values + * @param {Boolean} subtract - set to true if you want to subtract the scroll values + * @return {Object} rect - The modifier rect object + */ + function includeScroll(rect, element) { + var subtract = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + + var scrollTop = getScroll(element, 'top'); + var scrollLeft = getScroll(element, 'left'); + var modifier = subtract ? -1 : 1; + rect.top += scrollTop * modifier; + rect.bottom += scrollTop * modifier; + rect.left += scrollLeft * modifier; + rect.right += scrollLeft * modifier; + return rect; + } + + /* + * Helper to detect borders of a given element + * @method + * @memberof Popper.Utils + * @param {CSSStyleDeclaration} styles + * Result of `getStyleComputedProperty` on the given element + * @param {String} axis - `x` or `y` + * @return {number} borders - The borders size of the given axis + */ + + function getBordersSize(styles, axis) { + var sideA = axis === 'x' ? 'Left' : 'Top'; + var sideB = sideA === 'Left' ? 'Right' : 'Bottom'; + + return parseFloat(styles['border' + sideA + 'Width'], 10) + parseFloat(styles['border' + sideB + 'Width'], 10); + } + + function getSize(axis, body, html, computedStyle) { + return Math.max(body['offset' + axis], body['scroll' + axis], html['client' + axis], html['offset' + axis], html['scroll' + axis], isIE(10) ? parseInt(html['offset' + axis]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Top' : 'Left')]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Bottom' : 'Right')]) : 0); + } + + function getWindowSizes(document) { + var body = document.body; + var html = document.documentElement; + var computedStyle = isIE(10) && getComputedStyle(html); + + return { + height: getSize('Height', body, html, computedStyle), + width: getSize('Width', body, html, computedStyle) + }; + } + + var classCallCheck = function (instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + }; + + var createClass = function () { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + return function (Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps); + if (staticProps) defineProperties(Constructor, staticProps); + return Constructor; + }; + }(); + + + + + + var defineProperty = function (obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; + }; + + var _extends = Object.assign || function (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target; + }; + + /** + * Given element offsets, generate an output similar to getBoundingClientRect + * @method + * @memberof Popper.Utils + * @argument {Object} offsets + * @returns {Object} ClientRect like output + */ + function getClientRect(offsets) { + return _extends({}, offsets, { + right: offsets.left + offsets.width, + bottom: offsets.top + offsets.height + }); + } + + /** + * Get bounding client rect of given element + * @method + * @memberof Popper.Utils + * @param {HTMLElement} element + * @return {Object} client rect + */ + function getBoundingClientRect(element) { + var rect = {}; + + // IE10 10 FIX: Please, don't ask, the element isn't + // considered in DOM in some circumstances... + // This isn't reproducible in IE10 compatibility mode of IE11 + try { + if (isIE(10)) { + rect = element.getBoundingClientRect(); + var scrollTop = getScroll(element, 'top'); + var scrollLeft = getScroll(element, 'left'); + rect.top += scrollTop; + rect.left += scrollLeft; + rect.bottom += scrollTop; + rect.right += scrollLeft; + } else { + rect = element.getBoundingClientRect(); + } + } catch (e) {} + + var result = { + left: rect.left, + top: rect.top, + width: rect.right - rect.left, + height: rect.bottom - rect.top + }; + + // subtract scrollbar size from sizes + var sizes = element.nodeName === 'HTML' ? getWindowSizes(element.ownerDocument) : {}; + var width = sizes.width || element.clientWidth || result.width; + var height = sizes.height || element.clientHeight || result.height; + + var horizScrollbar = element.offsetWidth - width; + var vertScrollbar = element.offsetHeight - height; + + // if an hypothetical scrollbar is detected, we must be sure it's not a `border` + // we make this check conditional for performance reasons + if (horizScrollbar || vertScrollbar) { + var styles = getStyleComputedProperty(element); + horizScrollbar -= getBordersSize(styles, 'x'); + vertScrollbar -= getBordersSize(styles, 'y'); + + result.width -= horizScrollbar; + result.height -= vertScrollbar; + } + + return getClientRect(result); + } + + function getOffsetRectRelativeToArbitraryNode(children, parent) { + var fixedPosition = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + + var isIE10 = isIE(10); + var isHTML = parent.nodeName === 'HTML'; + var childrenRect = getBoundingClientRect(children); + var parentRect = getBoundingClientRect(parent); + var scrollParent = getScrollParent(children); + + var styles = getStyleComputedProperty(parent); + var borderTopWidth = parseFloat(styles.borderTopWidth, 10); + var borderLeftWidth = parseFloat(styles.borderLeftWidth, 10); + + // In cases where the parent is fixed, we must ignore negative scroll in offset calc + if (fixedPosition && isHTML) { + parentRect.top = Math.max(parentRect.top, 0); + parentRect.left = Math.max(parentRect.left, 0); + } + var offsets = getClientRect({ + top: childrenRect.top - parentRect.top - borderTopWidth, + left: childrenRect.left - parentRect.left - borderLeftWidth, + width: childrenRect.width, + height: childrenRect.height + }); + offsets.marginTop = 0; + offsets.marginLeft = 0; + + // Subtract margins of documentElement in case it's being used as parent + // we do this only on HTML because it's the only element that behaves + // differently when margins are applied to it. The margins are included in + // the box of the documentElement, in the other cases not. + if (!isIE10 && isHTML) { + var marginTop = parseFloat(styles.marginTop, 10); + var marginLeft = parseFloat(styles.marginLeft, 10); + + offsets.top -= borderTopWidth - marginTop; + offsets.bottom -= borderTopWidth - marginTop; + offsets.left -= borderLeftWidth - marginLeft; + offsets.right -= borderLeftWidth - marginLeft; + + // Attach marginTop and marginLeft because in some circumstances we may need them + offsets.marginTop = marginTop; + offsets.marginLeft = marginLeft; + } + + if (isIE10 && !fixedPosition ? parent.contains(scrollParent) : parent === scrollParent && scrollParent.nodeName !== 'BODY') { + offsets = includeScroll(offsets, parent); + } + + return offsets; + } + + function getViewportOffsetRectRelativeToArtbitraryNode(element) { + var excludeScroll = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + + var html = element.ownerDocument.documentElement; + var relativeOffset = getOffsetRectRelativeToArbitraryNode(element, html); + var width = Math.max(html.clientWidth, window.innerWidth || 0); + var height = Math.max(html.clientHeight, window.innerHeight || 0); + + var scrollTop = !excludeScroll ? getScroll(html) : 0; + var scrollLeft = !excludeScroll ? getScroll(html, 'left') : 0; + + var offset = { + top: scrollTop - relativeOffset.top + relativeOffset.marginTop, + left: scrollLeft - relativeOffset.left + relativeOffset.marginLeft, + width: width, + height: height + }; + + return getClientRect(offset); + } + + /** + * Check if the given element is fixed or is inside a fixed parent + * @method + * @memberof Popper.Utils + * @argument {Element} element + * @argument {Element} customContainer + * @returns {Boolean} answer to "isFixed?" + */ + function isFixed(element) { + var nodeName = element.nodeName; + if (nodeName === 'BODY' || nodeName === 'HTML') { + return false; + } + if (getStyleComputedProperty(element, 'position') === 'fixed') { + return true; + } + var parentNode = getParentNode(element); + if (!parentNode) { + return false; + } + return isFixed(parentNode); + } + + /** + * Finds the first parent of an element that has a transformed property defined + * @method + * @memberof Popper.Utils + * @argument {Element} element + * @returns {Element} first transformed parent or documentElement + */ + + function getFixedPositionOffsetParent(element) { + // This check is needed to avoid errors in case one of the elements isn't defined for any reason + if (!element || !element.parentElement || isIE()) { + return document.documentElement; + } + var el = element.parentElement; + while (el && getStyleComputedProperty(el, 'transform') === 'none') { + el = el.parentElement; + } + return el || document.documentElement; + } + + /** + * Computed the boundaries limits and return them + * @method + * @memberof Popper.Utils + * @param {HTMLElement} popper + * @param {HTMLElement} reference + * @param {number} padding + * @param {HTMLElement} boundariesElement - Element used to define the boundaries + * @param {Boolean} fixedPosition - Is in fixed position mode + * @returns {Object} Coordinates of the boundaries + */ + function getBoundaries(popper, reference, padding, boundariesElement) { + var fixedPosition = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false; + + // NOTE: 1 DOM access here + + var boundaries = { top: 0, left: 0 }; + var offsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, getReferenceNode(reference)); + + // Handle viewport case + if (boundariesElement === 'viewport') { + boundaries = getViewportOffsetRectRelativeToArtbitraryNode(offsetParent, fixedPosition); + } else { + // Handle other cases based on DOM element used as boundaries + var boundariesNode = void 0; + if (boundariesElement === 'scrollParent') { + boundariesNode = getScrollParent(getParentNode(reference)); + if (boundariesNode.nodeName === 'BODY') { + boundariesNode = popper.ownerDocument.documentElement; + } + } else if (boundariesElement === 'window') { + boundariesNode = popper.ownerDocument.documentElement; + } else { + boundariesNode = boundariesElement; + } + + var offsets = getOffsetRectRelativeToArbitraryNode(boundariesNode, offsetParent, fixedPosition); + + // In case of HTML, we need a different computation + if (boundariesNode.nodeName === 'HTML' && !isFixed(offsetParent)) { + var _getWindowSizes = getWindowSizes(popper.ownerDocument), + height = _getWindowSizes.height, + width = _getWindowSizes.width; + + boundaries.top += offsets.top - offsets.marginTop; + boundaries.bottom = height + offsets.top; + boundaries.left += offsets.left - offsets.marginLeft; + boundaries.right = width + offsets.left; + } else { + // for all the other DOM elements, this one is good + boundaries = offsets; + } + } + + // Add paddings + padding = padding || 0; + var isPaddingNumber = typeof padding === 'number'; + boundaries.left += isPaddingNumber ? padding : padding.left || 0; + boundaries.top += isPaddingNumber ? padding : padding.top || 0; + boundaries.right -= isPaddingNumber ? padding : padding.right || 0; + boundaries.bottom -= isPaddingNumber ? padding : padding.bottom || 0; + + return boundaries; + } + + function getArea(_ref) { + var width = _ref.width, + height = _ref.height; + + return width * height; + } + + /** + * Utility used to transform the `auto` placement to the placement with more + * available space. + * @method + * @memberof Popper.Utils + * @argument {Object} data - The data object generated by update method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function computeAutoPlacement(placement, refRect, popper, reference, boundariesElement) { + var padding = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 0; + + if (placement.indexOf('auto') === -1) { + return placement; + } + + var boundaries = getBoundaries(popper, reference, padding, boundariesElement); + + var rects = { + top: { + width: boundaries.width, + height: refRect.top - boundaries.top + }, + right: { + width: boundaries.right - refRect.right, + height: boundaries.height + }, + bottom: { + width: boundaries.width, + height: boundaries.bottom - refRect.bottom + }, + left: { + width: refRect.left - boundaries.left, + height: boundaries.height + } + }; + + var sortedAreas = Object.keys(rects).map(function (key) { + return _extends({ + key: key + }, rects[key], { + area: getArea(rects[key]) + }); + }).sort(function (a, b) { + return b.area - a.area; + }); + + var filteredAreas = sortedAreas.filter(function (_ref2) { + var width = _ref2.width, + height = _ref2.height; + return width >= popper.clientWidth && height >= popper.clientHeight; + }); + + var computedPlacement = filteredAreas.length > 0 ? filteredAreas[0].key : sortedAreas[0].key; + + var variation = placement.split('-')[1]; + + return computedPlacement + (variation ? '-' + variation : ''); + } + + /** + * Get offsets to the reference element + * @method + * @memberof Popper.Utils + * @param {Object} state + * @param {Element} popper - the popper element + * @param {Element} reference - the reference element (the popper will be relative to this) + * @param {Element} fixedPosition - is in fixed position mode + * @returns {Object} An object containing the offsets which will be applied to the popper + */ + function getReferenceOffsets(state, popper, reference) { + var fixedPosition = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; + + var commonOffsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, getReferenceNode(reference)); + return getOffsetRectRelativeToArbitraryNode(reference, commonOffsetParent, fixedPosition); + } + + /** + * Get the outer sizes of the given element (offset size + margins) + * @method + * @memberof Popper.Utils + * @argument {Element} element + * @returns {Object} object containing width and height properties + */ + function getOuterSizes(element) { + var window = element.ownerDocument.defaultView; + var styles = window.getComputedStyle(element); + var x = parseFloat(styles.marginTop || 0) + parseFloat(styles.marginBottom || 0); + var y = parseFloat(styles.marginLeft || 0) + parseFloat(styles.marginRight || 0); + var result = { + width: element.offsetWidth + y, + height: element.offsetHeight + x + }; + return result; + } + + /** + * Get the opposite placement of the given one + * @method + * @memberof Popper.Utils + * @argument {String} placement + * @returns {String} flipped placement + */ + function getOppositePlacement(placement) { + var hash = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' }; + return placement.replace(/left|right|bottom|top/g, function (matched) { + return hash[matched]; + }); + } + + /** + * Get offsets to the popper + * @method + * @memberof Popper.Utils + * @param {Object} position - CSS position the Popper will get applied + * @param {HTMLElement} popper - the popper element + * @param {Object} referenceOffsets - the reference offsets (the popper will be relative to this) + * @param {String} placement - one of the valid placement options + * @returns {Object} popperOffsets - An object containing the offsets which will be applied to the popper + */ + function getPopperOffsets(popper, referenceOffsets, placement) { + placement = placement.split('-')[0]; + + // Get popper node sizes + var popperRect = getOuterSizes(popper); + + // Add position, width and height to our offsets object + var popperOffsets = { + width: popperRect.width, + height: popperRect.height + }; + + // depending by the popper placement we have to compute its offsets slightly differently + var isHoriz = ['right', 'left'].indexOf(placement) !== -1; + var mainSide = isHoriz ? 'top' : 'left'; + var secondarySide = isHoriz ? 'left' : 'top'; + var measurement = isHoriz ? 'height' : 'width'; + var secondaryMeasurement = !isHoriz ? 'height' : 'width'; + + popperOffsets[mainSide] = referenceOffsets[mainSide] + referenceOffsets[measurement] / 2 - popperRect[measurement] / 2; + if (placement === secondarySide) { + popperOffsets[secondarySide] = referenceOffsets[secondarySide] - popperRect[secondaryMeasurement]; + } else { + popperOffsets[secondarySide] = referenceOffsets[getOppositePlacement(secondarySide)]; + } + + return popperOffsets; + } + + /** + * Mimics the `find` method of Array + * @method + * @memberof Popper.Utils + * @argument {Array} arr + * @argument prop + * @argument value + * @returns index or -1 + */ + function find(arr, check) { + // use native find if supported + if (Array.prototype.find) { + return arr.find(check); + } + + // use `filter` to obtain the same behavior of `find` + return arr.filter(check)[0]; + } + + /** + * Return the index of the matching object + * @method + * @memberof Popper.Utils + * @argument {Array} arr + * @argument prop + * @argument value + * @returns index or -1 + */ + function findIndex(arr, prop, value) { + // use native findIndex if supported + if (Array.prototype.findIndex) { + return arr.findIndex(function (cur) { + return cur[prop] === value; + }); + } + + // use `find` + `indexOf` if `findIndex` isn't supported + var match = find(arr, function (obj) { + return obj[prop] === value; + }); + return arr.indexOf(match); + } + + /** + * Loop trough the list of modifiers and run them in order, + * each of them will then edit the data object. + * @method + * @memberof Popper.Utils + * @param {dataObject} data + * @param {Array} modifiers + * @param {String} ends - Optional modifier name used as stopper + * @returns {dataObject} + */ + function runModifiers(modifiers, data, ends) { + var modifiersToRun = ends === undefined ? modifiers : modifiers.slice(0, findIndex(modifiers, 'name', ends)); + + modifiersToRun.forEach(function (modifier) { + if (modifier['function']) { + // eslint-disable-line dot-notation + console.warn('`modifier.function` is deprecated, use `modifier.fn`!'); + } + var fn = modifier['function'] || modifier.fn; // eslint-disable-line dot-notation + if (modifier.enabled && isFunction(fn)) { + // Add properties to offsets to make them a complete clientRect object + // we do this before each modifier to make sure the previous one doesn't + // mess with these values + data.offsets.popper = getClientRect(data.offsets.popper); + data.offsets.reference = getClientRect(data.offsets.reference); + + data = fn(data, modifier); + } + }); + + return data; + } + + /** + * Updates the position of the popper, computing the new offsets and applying + * the new style.
+ * Prefer `scheduleUpdate` over `update` because of performance reasons. + * @method + * @memberof Popper + */ + function update() { + // if popper is destroyed, don't perform any further update + if (this.state.isDestroyed) { + return; + } + + var data = { + instance: this, + styles: {}, + arrowStyles: {}, + attributes: {}, + flipped: false, + offsets: {} + }; + + // compute reference element offsets + data.offsets.reference = getReferenceOffsets(this.state, this.popper, this.reference, this.options.positionFixed); + + // compute auto placement, store placement inside the data object, + // modifiers will be able to edit `placement` if needed + // and refer to originalPlacement to know the original value + data.placement = computeAutoPlacement(this.options.placement, data.offsets.reference, this.popper, this.reference, this.options.modifiers.flip.boundariesElement, this.options.modifiers.flip.padding); + + // store the computed placement inside `originalPlacement` + data.originalPlacement = data.placement; + + data.positionFixed = this.options.positionFixed; + + // compute the popper offsets + data.offsets.popper = getPopperOffsets(this.popper, data.offsets.reference, data.placement); + + data.offsets.popper.position = this.options.positionFixed ? 'fixed' : 'absolute'; + + // run the modifiers + data = runModifiers(this.modifiers, data); + + // the first `update` will call `onCreate` callback + // the other ones will call `onUpdate` callback + if (!this.state.isCreated) { + this.state.isCreated = true; + this.options.onCreate(data); + } else { + this.options.onUpdate(data); + } + } + + /** + * Helper used to know if the given modifier is enabled. + * @method + * @memberof Popper.Utils + * @returns {Boolean} + */ + function isModifierEnabled(modifiers, modifierName) { + return modifiers.some(function (_ref) { + var name = _ref.name, + enabled = _ref.enabled; + return enabled && name === modifierName; + }); + } + + /** + * Get the prefixed supported property name + * @method + * @memberof Popper.Utils + * @argument {String} property (camelCase) + * @returns {String} prefixed property (camelCase or PascalCase, depending on the vendor prefix) + */ + function getSupportedPropertyName(property) { + var prefixes = [false, 'ms', 'Webkit', 'Moz', 'O']; + var upperProp = property.charAt(0).toUpperCase() + property.slice(1); + + for (var i = 0; i < prefixes.length; i++) { + var prefix = prefixes[i]; + var toCheck = prefix ? '' + prefix + upperProp : property; + if (typeof document.body.style[toCheck] !== 'undefined') { + return toCheck; + } + } + return null; + } + + /** + * Destroys the popper. + * @method + * @memberof Popper + */ + function destroy() { + this.state.isDestroyed = true; + + // touch DOM only if `applyStyle` modifier is enabled + if (isModifierEnabled(this.modifiers, 'applyStyle')) { + this.popper.removeAttribute('x-placement'); + this.popper.style.position = ''; + this.popper.style.top = ''; + this.popper.style.left = ''; + this.popper.style.right = ''; + this.popper.style.bottom = ''; + this.popper.style.willChange = ''; + this.popper.style[getSupportedPropertyName('transform')] = ''; + } + + this.disableEventListeners(); + + // remove the popper if user explicitly asked for the deletion on destroy + // do not use `remove` because IE11 doesn't support it + if (this.options.removeOnDestroy) { + this.popper.parentNode.removeChild(this.popper); + } + return this; + } + + /** + * Get the window associated with the element + * @argument {Element} element + * @returns {Window} + */ + function getWindow(element) { + var ownerDocument = element.ownerDocument; + return ownerDocument ? ownerDocument.defaultView : window; + } + + function attachToScrollParents(scrollParent, event, callback, scrollParents) { + var isBody = scrollParent.nodeName === 'BODY'; + var target = isBody ? scrollParent.ownerDocument.defaultView : scrollParent; + target.addEventListener(event, callback, { passive: true }); + + if (!isBody) { + attachToScrollParents(getScrollParent(target.parentNode), event, callback, scrollParents); + } + scrollParents.push(target); + } + + /** + * Setup needed event listeners used to update the popper position + * @method + * @memberof Popper.Utils + * @private + */ + function setupEventListeners(reference, options, state, updateBound) { + // Resize event listener on window + state.updateBound = updateBound; + getWindow(reference).addEventListener('resize', state.updateBound, { passive: true }); + + // Scroll event listener on scroll parents + var scrollElement = getScrollParent(reference); + attachToScrollParents(scrollElement, 'scroll', state.updateBound, state.scrollParents); + state.scrollElement = scrollElement; + state.eventsEnabled = true; + + return state; + } + + /** + * It will add resize/scroll events and start recalculating + * position of the popper element when they are triggered. + * @method + * @memberof Popper + */ + function enableEventListeners() { + if (!this.state.eventsEnabled) { + this.state = setupEventListeners(this.reference, this.options, this.state, this.scheduleUpdate); + } + } + + /** + * Remove event listeners used to update the popper position + * @method + * @memberof Popper.Utils + * @private + */ + function removeEventListeners(reference, state) { + // Remove resize event listener on window + getWindow(reference).removeEventListener('resize', state.updateBound); + + // Remove scroll event listener on scroll parents + state.scrollParents.forEach(function (target) { + target.removeEventListener('scroll', state.updateBound); + }); + + // Reset state + state.updateBound = null; + state.scrollParents = []; + state.scrollElement = null; + state.eventsEnabled = false; + return state; + } + + /** + * It will remove resize/scroll events and won't recalculate popper position + * when they are triggered. It also won't trigger `onUpdate` callback anymore, + * unless you call `update` method manually. + * @method + * @memberof Popper + */ + function disableEventListeners() { + if (this.state.eventsEnabled) { + cancelAnimationFrame(this.scheduleUpdate); + this.state = removeEventListeners(this.reference, this.state); + } + } + + /** + * Tells if a given input is a number + * @method + * @memberof Popper.Utils + * @param {*} input to check + * @return {Boolean} + */ + function isNumeric(n) { + return n !== '' && !isNaN(parseFloat(n)) && isFinite(n); + } + + /** + * Set the style to the given popper + * @method + * @memberof Popper.Utils + * @argument {Element} element - Element to apply the style to + * @argument {Object} styles + * Object with a list of properties and values which will be applied to the element + */ + function setStyles(element, styles) { + Object.keys(styles).forEach(function (prop) { + var unit = ''; + // add unit if the value is numeric and is one of the following + if (['width', 'height', 'top', 'right', 'bottom', 'left'].indexOf(prop) !== -1 && isNumeric(styles[prop])) { + unit = 'px'; + } + element.style[prop] = styles[prop] + unit; + }); + } + + /** + * Set the attributes to the given popper + * @method + * @memberof Popper.Utils + * @argument {Element} element - Element to apply the attributes to + * @argument {Object} styles + * Object with a list of properties and values which will be applied to the element + */ + function setAttributes(element, attributes) { + Object.keys(attributes).forEach(function (prop) { + var value = attributes[prop]; + if (value !== false) { + element.setAttribute(prop, attributes[prop]); + } else { + element.removeAttribute(prop); + } + }); + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by `update` method + * @argument {Object} data.styles - List of style properties - values to apply to popper element + * @argument {Object} data.attributes - List of attribute properties - values to apply to popper element + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The same data object + */ + function applyStyle(data) { + // any property present in `data.styles` will be applied to the popper, + // in this way we can make the 3rd party modifiers add custom styles to it + // Be aware, modifiers could override the properties defined in the previous + // lines of this modifier! + setStyles(data.instance.popper, data.styles); + + // any property present in `data.attributes` will be applied to the popper, + // they will be set as HTML attributes of the element + setAttributes(data.instance.popper, data.attributes); + + // if arrowElement is defined and arrowStyles has some properties + if (data.arrowElement && Object.keys(data.arrowStyles).length) { + setStyles(data.arrowElement, data.arrowStyles); + } + + return data; + } + + /** + * Set the x-placement attribute before everything else because it could be used + * to add margins to the popper margins needs to be calculated to get the + * correct popper offsets. + * @method + * @memberof Popper.modifiers + * @param {HTMLElement} reference - The reference element used to position the popper + * @param {HTMLElement} popper - The HTML element used as popper + * @param {Object} options - Popper.js options + */ + function applyStyleOnLoad(reference, popper, options, modifierOptions, state) { + // compute reference element offsets + var referenceOffsets = getReferenceOffsets(state, popper, reference, options.positionFixed); + + // compute auto placement, store placement inside the data object, + // modifiers will be able to edit `placement` if needed + // and refer to originalPlacement to know the original value + var placement = computeAutoPlacement(options.placement, referenceOffsets, popper, reference, options.modifiers.flip.boundariesElement, options.modifiers.flip.padding); + + popper.setAttribute('x-placement', placement); + + // Apply `position` to popper before anything else because + // without the position applied we can't guarantee correct computations + setStyles(popper, { position: options.positionFixed ? 'fixed' : 'absolute' }); + + return options; + } + + /** + * @function + * @memberof Popper.Utils + * @argument {Object} data - The data object generated by `update` method + * @argument {Boolean} shouldRound - If the offsets should be rounded at all + * @returns {Object} The popper's position offsets rounded + * + * The tale of pixel-perfect positioning. It's still not 100% perfect, but as + * good as it can be within reason. + * Discussion here: https://github.com/FezVrasta/popper.js/pull/715 + * + * Low DPI screens cause a popper to be blurry if not using full pixels (Safari + * as well on High DPI screens). + * + * Firefox prefers no rounding for positioning and does not have blurriness on + * high DPI screens. + * + * Only horizontal placement and left/right values need to be considered. + */ + function getRoundedOffsets(data, shouldRound) { + var _data$offsets = data.offsets, + popper = _data$offsets.popper, + reference = _data$offsets.reference; + var round = Math.round, + floor = Math.floor; + + var noRound = function noRound(v) { + return v; + }; + + var referenceWidth = round(reference.width); + var popperWidth = round(popper.width); + + var isVertical = ['left', 'right'].indexOf(data.placement) !== -1; + var isVariation = data.placement.indexOf('-') !== -1; + var sameWidthParity = referenceWidth % 2 === popperWidth % 2; + var bothOddWidth = referenceWidth % 2 === 1 && popperWidth % 2 === 1; + + var horizontalToInteger = !shouldRound ? noRound : isVertical || isVariation || sameWidthParity ? round : floor; + var verticalToInteger = !shouldRound ? noRound : round; + + return { + left: horizontalToInteger(bothOddWidth && !isVariation && shouldRound ? popper.left - 1 : popper.left), + top: verticalToInteger(popper.top), + bottom: verticalToInteger(popper.bottom), + right: horizontalToInteger(popper.right) + }; + } + + var isFirefox = isBrowser && /Firefox/i.test(navigator.userAgent); + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by `update` method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function computeStyle(data, options) { + var x = options.x, + y = options.y; + var popper = data.offsets.popper; + + // Remove this legacy support in Popper.js v2 + + var legacyGpuAccelerationOption = find(data.instance.modifiers, function (modifier) { + return modifier.name === 'applyStyle'; + }).gpuAcceleration; + if (legacyGpuAccelerationOption !== undefined) { + console.warn('WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!'); + } + var gpuAcceleration = legacyGpuAccelerationOption !== undefined ? legacyGpuAccelerationOption : options.gpuAcceleration; + + var offsetParent = getOffsetParent(data.instance.popper); + var offsetParentRect = getBoundingClientRect(offsetParent); + + // Styles + var styles = { + position: popper.position + }; + + var offsets = getRoundedOffsets(data, window.devicePixelRatio < 2 || !isFirefox); + + var sideA = x === 'bottom' ? 'top' : 'bottom'; + var sideB = y === 'right' ? 'left' : 'right'; + + // if gpuAcceleration is set to `true` and transform is supported, + // we use `translate3d` to apply the position to the popper we + // automatically use the supported prefixed version if needed + var prefixedProperty = getSupportedPropertyName('transform'); + + // now, let's make a step back and look at this code closely (wtf?) + // If the content of the popper grows once it's been positioned, it + // may happen that the popper gets misplaced because of the new content + // overflowing its reference element + // To avoid this problem, we provide two options (x and y), which allow + // the consumer to define the offset origin. + // If we position a popper on top of a reference element, we can set + // `x` to `top` to make the popper grow towards its top instead of + // its bottom. + var left = void 0, + top = void 0; + if (sideA === 'bottom') { + // when offsetParent is the positioning is relative to the bottom of the screen (excluding the scrollbar) + // and not the bottom of the html element + if (offsetParent.nodeName === 'HTML') { + top = -offsetParent.clientHeight + offsets.bottom; + } else { + top = -offsetParentRect.height + offsets.bottom; + } + } else { + top = offsets.top; + } + if (sideB === 'right') { + if (offsetParent.nodeName === 'HTML') { + left = -offsetParent.clientWidth + offsets.right; + } else { + left = -offsetParentRect.width + offsets.right; + } + } else { + left = offsets.left; + } + if (gpuAcceleration && prefixedProperty) { + styles[prefixedProperty] = 'translate3d(' + left + 'px, ' + top + 'px, 0)'; + styles[sideA] = 0; + styles[sideB] = 0; + styles.willChange = 'transform'; + } else { + // othwerise, we use the standard `top`, `left`, `bottom` and `right` properties + var invertTop = sideA === 'bottom' ? -1 : 1; + var invertLeft = sideB === 'right' ? -1 : 1; + styles[sideA] = top * invertTop; + styles[sideB] = left * invertLeft; + styles.willChange = sideA + ', ' + sideB; + } + + // Attributes + var attributes = { + 'x-placement': data.placement + }; + + // Update `data` attributes, styles and arrowStyles + data.attributes = _extends({}, attributes, data.attributes); + data.styles = _extends({}, styles, data.styles); + data.arrowStyles = _extends({}, data.offsets.arrow, data.arrowStyles); + + return data; + } + + /** + * Helper used to know if the given modifier depends from another one.
+ * It checks if the needed modifier is listed and enabled. + * @method + * @memberof Popper.Utils + * @param {Array} modifiers - list of modifiers + * @param {String} requestingName - name of requesting modifier + * @param {String} requestedName - name of requested modifier + * @returns {Boolean} + */ + function isModifierRequired(modifiers, requestingName, requestedName) { + var requesting = find(modifiers, function (_ref) { + var name = _ref.name; + return name === requestingName; + }); + + var isRequired = !!requesting && modifiers.some(function (modifier) { + return modifier.name === requestedName && modifier.enabled && modifier.order < requesting.order; + }); + + if (!isRequired) { + var _requesting = '`' + requestingName + '`'; + var requested = '`' + requestedName + '`'; + console.warn(requested + ' modifier is required by ' + _requesting + ' modifier in order to work, be sure to include it before ' + _requesting + '!'); + } + return isRequired; + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by update method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function arrow(data, options) { + var _data$offsets$arrow; + + // arrow depends on keepTogether in order to work + if (!isModifierRequired(data.instance.modifiers, 'arrow', 'keepTogether')) { + return data; + } + + var arrowElement = options.element; + + // if arrowElement is a string, suppose it's a CSS selector + if (typeof arrowElement === 'string') { + arrowElement = data.instance.popper.querySelector(arrowElement); + + // if arrowElement is not found, don't run the modifier + if (!arrowElement) { + return data; + } + } else { + // if the arrowElement isn't a query selector we must check that the + // provided DOM node is child of its popper node + if (!data.instance.popper.contains(arrowElement)) { + console.warn('WARNING: `arrow.element` must be child of its popper element!'); + return data; + } + } + + var placement = data.placement.split('-')[0]; + var _data$offsets = data.offsets, + popper = _data$offsets.popper, + reference = _data$offsets.reference; + + var isVertical = ['left', 'right'].indexOf(placement) !== -1; + + var len = isVertical ? 'height' : 'width'; + var sideCapitalized = isVertical ? 'Top' : 'Left'; + var side = sideCapitalized.toLowerCase(); + var altSide = isVertical ? 'left' : 'top'; + var opSide = isVertical ? 'bottom' : 'right'; + var arrowElementSize = getOuterSizes(arrowElement)[len]; + + // + // extends keepTogether behavior making sure the popper and its + // reference have enough pixels in conjunction + // + + // top/left side + if (reference[opSide] - arrowElementSize < popper[side]) { + data.offsets.popper[side] -= popper[side] - (reference[opSide] - arrowElementSize); + } + // bottom/right side + if (reference[side] + arrowElementSize > popper[opSide]) { + data.offsets.popper[side] += reference[side] + arrowElementSize - popper[opSide]; + } + data.offsets.popper = getClientRect(data.offsets.popper); + + // compute center of the popper + var center = reference[side] + reference[len] / 2 - arrowElementSize / 2; + + // Compute the sideValue using the updated popper offsets + // take popper margin in account because we don't have this info available + var css = getStyleComputedProperty(data.instance.popper); + var popperMarginSide = parseFloat(css['margin' + sideCapitalized], 10); + var popperBorderSide = parseFloat(css['border' + sideCapitalized + 'Width'], 10); + var sideValue = center - data.offsets.popper[side] - popperMarginSide - popperBorderSide; + + // prevent arrowElement from being placed not contiguously to its popper + sideValue = Math.max(Math.min(popper[len] - arrowElementSize, sideValue), 0); + + data.arrowElement = arrowElement; + data.offsets.arrow = (_data$offsets$arrow = {}, defineProperty(_data$offsets$arrow, side, Math.round(sideValue)), defineProperty(_data$offsets$arrow, altSide, ''), _data$offsets$arrow); + + return data; + } + + /** + * Get the opposite placement variation of the given one + * @method + * @memberof Popper.Utils + * @argument {String} placement variation + * @returns {String} flipped placement variation + */ + function getOppositeVariation(variation) { + if (variation === 'end') { + return 'start'; + } else if (variation === 'start') { + return 'end'; + } + return variation; + } + + /** + * List of accepted placements to use as values of the `placement` option.
+ * Valid placements are: + * - `auto` + * - `top` + * - `right` + * - `bottom` + * - `left` + * + * Each placement can have a variation from this list: + * - `-start` + * - `-end` + * + * Variations are interpreted easily if you think of them as the left to right + * written languages. Horizontally (`top` and `bottom`), `start` is left and `end` + * is right.
+ * Vertically (`left` and `right`), `start` is top and `end` is bottom. + * + * Some valid examples are: + * - `top-end` (on top of reference, right aligned) + * - `right-start` (on right of reference, top aligned) + * - `bottom` (on bottom, centered) + * - `auto-end` (on the side with more space available, alignment depends by placement) + * + * @static + * @type {Array} + * @enum {String} + * @readonly + * @method placements + * @memberof Popper + */ + var placements = ['auto-start', 'auto', 'auto-end', 'top-start', 'top', 'top-end', 'right-start', 'right', 'right-end', 'bottom-end', 'bottom', 'bottom-start', 'left-end', 'left', 'left-start']; + + // Get rid of `auto` `auto-start` and `auto-end` + var validPlacements = placements.slice(3); + + /** + * Given an initial placement, returns all the subsequent placements + * clockwise (or counter-clockwise). + * + * @method + * @memberof Popper.Utils + * @argument {String} placement - A valid placement (it accepts variations) + * @argument {Boolean} counter - Set to true to walk the placements counterclockwise + * @returns {Array} placements including their variations + */ + function clockwise(placement) { + var counter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + + var index = validPlacements.indexOf(placement); + var arr = validPlacements.slice(index + 1).concat(validPlacements.slice(0, index)); + return counter ? arr.reverse() : arr; + } + + var BEHAVIORS = { + FLIP: 'flip', + CLOCKWISE: 'clockwise', + COUNTERCLOCKWISE: 'counterclockwise' + }; + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by update method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function flip(data, options) { + // if `inner` modifier is enabled, we can't use the `flip` modifier + if (isModifierEnabled(data.instance.modifiers, 'inner')) { + return data; + } + + if (data.flipped && data.placement === data.originalPlacement) { + // seems like flip is trying to loop, probably there's not enough space on any of the flippable sides + return data; + } + + var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, options.boundariesElement, data.positionFixed); + + var placement = data.placement.split('-')[0]; + var placementOpposite = getOppositePlacement(placement); + var variation = data.placement.split('-')[1] || ''; + + var flipOrder = []; + + switch (options.behavior) { + case BEHAVIORS.FLIP: + flipOrder = [placement, placementOpposite]; + break; + case BEHAVIORS.CLOCKWISE: + flipOrder = clockwise(placement); + break; + case BEHAVIORS.COUNTERCLOCKWISE: + flipOrder = clockwise(placement, true); + break; + default: + flipOrder = options.behavior; + } + + flipOrder.forEach(function (step, index) { + if (placement !== step || flipOrder.length === index + 1) { + return data; + } + + placement = data.placement.split('-')[0]; + placementOpposite = getOppositePlacement(placement); + + var popperOffsets = data.offsets.popper; + var refOffsets = data.offsets.reference; + + // using floor because the reference offsets may contain decimals we are not going to consider here + var floor = Math.floor; + var overlapsRef = placement === 'left' && floor(popperOffsets.right) > floor(refOffsets.left) || placement === 'right' && floor(popperOffsets.left) < floor(refOffsets.right) || placement === 'top' && floor(popperOffsets.bottom) > floor(refOffsets.top) || placement === 'bottom' && floor(popperOffsets.top) < floor(refOffsets.bottom); + + var overflowsLeft = floor(popperOffsets.left) < floor(boundaries.left); + var overflowsRight = floor(popperOffsets.right) > floor(boundaries.right); + var overflowsTop = floor(popperOffsets.top) < floor(boundaries.top); + var overflowsBottom = floor(popperOffsets.bottom) > floor(boundaries.bottom); + + var overflowsBoundaries = placement === 'left' && overflowsLeft || placement === 'right' && overflowsRight || placement === 'top' && overflowsTop || placement === 'bottom' && overflowsBottom; + + // flip the variation if required + var isVertical = ['top', 'bottom'].indexOf(placement) !== -1; + + // flips variation if reference element overflows boundaries + var flippedVariationByRef = !!options.flipVariations && (isVertical && variation === 'start' && overflowsLeft || isVertical && variation === 'end' && overflowsRight || !isVertical && variation === 'start' && overflowsTop || !isVertical && variation === 'end' && overflowsBottom); + + // flips variation if popper content overflows boundaries + var flippedVariationByContent = !!options.flipVariationsByContent && (isVertical && variation === 'start' && overflowsRight || isVertical && variation === 'end' && overflowsLeft || !isVertical && variation === 'start' && overflowsBottom || !isVertical && variation === 'end' && overflowsTop); + + var flippedVariation = flippedVariationByRef || flippedVariationByContent; + + if (overlapsRef || overflowsBoundaries || flippedVariation) { + // this boolean to detect any flip loop + data.flipped = true; + + if (overlapsRef || overflowsBoundaries) { + placement = flipOrder[index + 1]; + } + + if (flippedVariation) { + variation = getOppositeVariation(variation); + } + + data.placement = placement + (variation ? '-' + variation : ''); + + // this object contains `position`, we want to preserve it along with + // any additional property we may add in the future + data.offsets.popper = _extends({}, data.offsets.popper, getPopperOffsets(data.instance.popper, data.offsets.reference, data.placement)); + + data = runModifiers(data.instance.modifiers, data, 'flip'); + } + }); + return data; + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by update method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function keepTogether(data) { + var _data$offsets = data.offsets, + popper = _data$offsets.popper, + reference = _data$offsets.reference; + + var placement = data.placement.split('-')[0]; + var floor = Math.floor; + var isVertical = ['top', 'bottom'].indexOf(placement) !== -1; + var side = isVertical ? 'right' : 'bottom'; + var opSide = isVertical ? 'left' : 'top'; + var measurement = isVertical ? 'width' : 'height'; + + if (popper[side] < floor(reference[opSide])) { + data.offsets.popper[opSide] = floor(reference[opSide]) - popper[measurement]; + } + if (popper[opSide] > floor(reference[side])) { + data.offsets.popper[opSide] = floor(reference[side]); + } + + return data; + } + + /** + * Converts a string containing value + unit into a px value number + * @function + * @memberof {modifiers~offset} + * @private + * @argument {String} str - Value + unit string + * @argument {String} measurement - `height` or `width` + * @argument {Object} popperOffsets + * @argument {Object} referenceOffsets + * @returns {Number|String} + * Value in pixels, or original string if no values were extracted + */ + function toValue(str, measurement, popperOffsets, referenceOffsets) { + // separate value from unit + var split = str.match(/((?:\-|\+)?\d*\.?\d*)(.*)/); + var value = +split[1]; + var unit = split[2]; + + // If it's not a number it's an operator, I guess + if (!value) { + return str; + } + + if (unit.indexOf('%') === 0) { + var element = void 0; + switch (unit) { + case '%p': + element = popperOffsets; + break; + case '%': + case '%r': + default: + element = referenceOffsets; + } + + var rect = getClientRect(element); + return rect[measurement] / 100 * value; + } else if (unit === 'vh' || unit === 'vw') { + // if is a vh or vw, we calculate the size based on the viewport + var size = void 0; + if (unit === 'vh') { + size = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); + } else { + size = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); + } + return size / 100 * value; + } else { + // if is an explicit pixel unit, we get rid of the unit and keep the value + // if is an implicit unit, it's px, and we return just the value + return value; + } + } + + /** + * Parse an `offset` string to extrapolate `x` and `y` numeric offsets. + * @function + * @memberof {modifiers~offset} + * @private + * @argument {String} offset + * @argument {Object} popperOffsets + * @argument {Object} referenceOffsets + * @argument {String} basePlacement + * @returns {Array} a two cells array with x and y offsets in numbers + */ + function parseOffset(offset, popperOffsets, referenceOffsets, basePlacement) { + var offsets = [0, 0]; + + // Use height if placement is left or right and index is 0 otherwise use width + // in this way the first offset will use an axis and the second one + // will use the other one + var useHeight = ['right', 'left'].indexOf(basePlacement) !== -1; + + // Split the offset string to obtain a list of values and operands + // The regex addresses values with the plus or minus sign in front (+10, -20, etc) + var fragments = offset.split(/(\+|\-)/).map(function (frag) { + return frag.trim(); + }); + + // Detect if the offset string contains a pair of values or a single one + // they could be separated by comma or space + var divider = fragments.indexOf(find(fragments, function (frag) { + return frag.search(/,|\s/) !== -1; + })); + + if (fragments[divider] && fragments[divider].indexOf(',') === -1) { + console.warn('Offsets separated by white space(s) are deprecated, use a comma (,) instead.'); + } + + // If divider is found, we divide the list of values and operands to divide + // them by ofset X and Y. + var splitRegex = /\s*,\s*|\s+/; + var ops = divider !== -1 ? [fragments.slice(0, divider).concat([fragments[divider].split(splitRegex)[0]]), [fragments[divider].split(splitRegex)[1]].concat(fragments.slice(divider + 1))] : [fragments]; + + // Convert the values with units to absolute pixels to allow our computations + ops = ops.map(function (op, index) { + // Most of the units rely on the orientation of the popper + var measurement = (index === 1 ? !useHeight : useHeight) ? 'height' : 'width'; + var mergeWithPrevious = false; + return op + // This aggregates any `+` or `-` sign that aren't considered operators + // e.g.: 10 + +5 => [10, +, +5] + .reduce(function (a, b) { + if (a[a.length - 1] === '' && ['+', '-'].indexOf(b) !== -1) { + a[a.length - 1] = b; + mergeWithPrevious = true; + return a; + } else if (mergeWithPrevious) { + a[a.length - 1] += b; + mergeWithPrevious = false; + return a; + } else { + return a.concat(b); + } + }, []) + // Here we convert the string values into number values (in px) + .map(function (str) { + return toValue(str, measurement, popperOffsets, referenceOffsets); + }); + }); + + // Loop trough the offsets arrays and execute the operations + ops.forEach(function (op, index) { + op.forEach(function (frag, index2) { + if (isNumeric(frag)) { + offsets[index] += frag * (op[index2 - 1] === '-' ? -1 : 1); + } + }); + }); + return offsets; + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by update method + * @argument {Object} options - Modifiers configuration and options + * @argument {Number|String} options.offset=0 + * The offset value as described in the modifier description + * @returns {Object} The data object, properly modified + */ + function offset(data, _ref) { + var offset = _ref.offset; + var placement = data.placement, + _data$offsets = data.offsets, + popper = _data$offsets.popper, + reference = _data$offsets.reference; + + var basePlacement = placement.split('-')[0]; + + var offsets = void 0; + if (isNumeric(+offset)) { + offsets = [+offset, 0]; + } else { + offsets = parseOffset(offset, popper, reference, basePlacement); + } + + if (basePlacement === 'left') { + popper.top += offsets[0]; + popper.left -= offsets[1]; + } else if (basePlacement === 'right') { + popper.top += offsets[0]; + popper.left += offsets[1]; + } else if (basePlacement === 'top') { + popper.left += offsets[0]; + popper.top -= offsets[1]; + } else if (basePlacement === 'bottom') { + popper.left += offsets[0]; + popper.top += offsets[1]; + } + + data.popper = popper; + return data; + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by `update` method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function preventOverflow(data, options) { + var boundariesElement = options.boundariesElement || getOffsetParent(data.instance.popper); + + // If offsetParent is the reference element, we really want to + // go one step up and use the next offsetParent as reference to + // avoid to make this modifier completely useless and look like broken + if (data.instance.reference === boundariesElement) { + boundariesElement = getOffsetParent(boundariesElement); + } + + // NOTE: DOM access here + // resets the popper's position so that the document size can be calculated excluding + // the size of the popper element itself + var transformProp = getSupportedPropertyName('transform'); + var popperStyles = data.instance.popper.style; // assignment to help minification + var top = popperStyles.top, + left = popperStyles.left, + transform = popperStyles[transformProp]; + + popperStyles.top = ''; + popperStyles.left = ''; + popperStyles[transformProp] = ''; + + var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, boundariesElement, data.positionFixed); + + // NOTE: DOM access here + // restores the original style properties after the offsets have been computed + popperStyles.top = top; + popperStyles.left = left; + popperStyles[transformProp] = transform; + + options.boundaries = boundaries; + + var order = options.priority; + var popper = data.offsets.popper; + + var check = { + primary: function primary(placement) { + var value = popper[placement]; + if (popper[placement] < boundaries[placement] && !options.escapeWithReference) { + value = Math.max(popper[placement], boundaries[placement]); + } + return defineProperty({}, placement, value); + }, + secondary: function secondary(placement) { + var mainSide = placement === 'right' ? 'left' : 'top'; + var value = popper[mainSide]; + if (popper[placement] > boundaries[placement] && !options.escapeWithReference) { + value = Math.min(popper[mainSide], boundaries[placement] - (placement === 'right' ? popper.width : popper.height)); + } + return defineProperty({}, mainSide, value); + } + }; + + order.forEach(function (placement) { + var side = ['left', 'top'].indexOf(placement) !== -1 ? 'primary' : 'secondary'; + popper = _extends({}, popper, check[side](placement)); + }); + + data.offsets.popper = popper; + + return data; + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by `update` method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function shift(data) { + var placement = data.placement; + var basePlacement = placement.split('-')[0]; + var shiftvariation = placement.split('-')[1]; + + // if shift shiftvariation is specified, run the modifier + if (shiftvariation) { + var _data$offsets = data.offsets, + reference = _data$offsets.reference, + popper = _data$offsets.popper; + + var isVertical = ['bottom', 'top'].indexOf(basePlacement) !== -1; + var side = isVertical ? 'left' : 'top'; + var measurement = isVertical ? 'width' : 'height'; + + var shiftOffsets = { + start: defineProperty({}, side, reference[side]), + end: defineProperty({}, side, reference[side] + reference[measurement] - popper[measurement]) + }; + + data.offsets.popper = _extends({}, popper, shiftOffsets[shiftvariation]); + } + + return data; + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by update method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function hide(data) { + if (!isModifierRequired(data.instance.modifiers, 'hide', 'preventOverflow')) { + return data; + } + + var refRect = data.offsets.reference; + var bound = find(data.instance.modifiers, function (modifier) { + return modifier.name === 'preventOverflow'; + }).boundaries; + + if (refRect.bottom < bound.top || refRect.left > bound.right || refRect.top > bound.bottom || refRect.right < bound.left) { + // Avoid unnecessary DOM access if visibility hasn't changed + if (data.hide === true) { + return data; + } + + data.hide = true; + data.attributes['x-out-of-boundaries'] = ''; + } else { + // Avoid unnecessary DOM access if visibility hasn't changed + if (data.hide === false) { + return data; + } + + data.hide = false; + data.attributes['x-out-of-boundaries'] = false; + } + + return data; + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by `update` method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function inner(data) { + var placement = data.placement; + var basePlacement = placement.split('-')[0]; + var _data$offsets = data.offsets, + popper = _data$offsets.popper, + reference = _data$offsets.reference; + + var isHoriz = ['left', 'right'].indexOf(basePlacement) !== -1; + + var subtractLength = ['top', 'left'].indexOf(basePlacement) === -1; + + popper[isHoriz ? 'left' : 'top'] = reference[basePlacement] - (subtractLength ? popper[isHoriz ? 'width' : 'height'] : 0); + + data.placement = getOppositePlacement(placement); + data.offsets.popper = getClientRect(popper); + + return data; + } + + /** + * Modifier function, each modifier can have a function of this type assigned + * to its `fn` property.
+ * These functions will be called on each update, this means that you must + * make sure they are performant enough to avoid performance bottlenecks. + * + * @function ModifierFn + * @argument {dataObject} data - The data object generated by `update` method + * @argument {Object} options - Modifiers configuration and options + * @returns {dataObject} The data object, properly modified + */ + + /** + * Modifiers are plugins used to alter the behavior of your poppers.
+ * Popper.js uses a set of 9 modifiers to provide all the basic functionalities + * needed by the library. + * + * Usually you don't want to override the `order`, `fn` and `onLoad` props. + * All the other properties are configurations that could be tweaked. + * @namespace modifiers + */ + var modifiers = { + /** + * Modifier used to shift the popper on the start or end of its reference + * element.
+ * It will read the variation of the `placement` property.
+ * It can be one either `-end` or `-start`. + * @memberof modifiers + * @inner + */ + shift: { + /** @prop {number} order=100 - Index used to define the order of execution */ + order: 100, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: shift + }, + + /** + * The `offset` modifier can shift your popper on both its axis. + * + * It accepts the following units: + * - `px` or unit-less, interpreted as pixels + * - `%` or `%r`, percentage relative to the length of the reference element + * - `%p`, percentage relative to the length of the popper element + * - `vw`, CSS viewport width unit + * - `vh`, CSS viewport height unit + * + * For length is intended the main axis relative to the placement of the popper.
+ * This means that if the placement is `top` or `bottom`, the length will be the + * `width`. In case of `left` or `right`, it will be the `height`. + * + * You can provide a single value (as `Number` or `String`), or a pair of values + * as `String` divided by a comma or one (or more) white spaces.
+ * The latter is a deprecated method because it leads to confusion and will be + * removed in v2.
+ * Additionally, it accepts additions and subtractions between different units. + * Note that multiplications and divisions aren't supported. + * + * Valid examples are: + * ``` + * 10 + * '10%' + * '10, 10' + * '10%, 10' + * '10 + 10%' + * '10 - 5vh + 3%' + * '-10px + 5vh, 5px - 6%' + * ``` + * > **NB**: If you desire to apply offsets to your poppers in a way that may make them overlap + * > with their reference element, unfortunately, you will have to disable the `flip` modifier. + * > You can read more on this at this [issue](https://github.com/FezVrasta/popper.js/issues/373). + * + * @memberof modifiers + * @inner + */ + offset: { + /** @prop {number} order=200 - Index used to define the order of execution */ + order: 200, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: offset, + /** @prop {Number|String} offset=0 + * The offset value as described in the modifier description + */ + offset: 0 + }, + + /** + * Modifier used to prevent the popper from being positioned outside the boundary. + * + * A scenario exists where the reference itself is not within the boundaries.
+ * We can say it has "escaped the boundaries" — or just "escaped".
+ * In this case we need to decide whether the popper should either: + * + * - detach from the reference and remain "trapped" in the boundaries, or + * - if it should ignore the boundary and "escape with its reference" + * + * When `escapeWithReference` is set to`true` and reference is completely + * outside its boundaries, the popper will overflow (or completely leave) + * the boundaries in order to remain attached to the edge of the reference. + * + * @memberof modifiers + * @inner + */ + preventOverflow: { + /** @prop {number} order=300 - Index used to define the order of execution */ + order: 300, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: preventOverflow, + /** + * @prop {Array} [priority=['left','right','top','bottom']] + * Popper will try to prevent overflow following these priorities by default, + * then, it could overflow on the left and on top of the `boundariesElement` + */ + priority: ['left', 'right', 'top', 'bottom'], + /** + * @prop {number} padding=5 + * Amount of pixel used to define a minimum distance between the boundaries + * and the popper. This makes sure the popper always has a little padding + * between the edges of its container + */ + padding: 5, + /** + * @prop {String|HTMLElement} boundariesElement='scrollParent' + * Boundaries used by the modifier. Can be `scrollParent`, `window`, + * `viewport` or any DOM element. + */ + boundariesElement: 'scrollParent' + }, + + /** + * Modifier used to make sure the reference and its popper stay near each other + * without leaving any gap between the two. Especially useful when the arrow is + * enabled and you want to ensure that it points to its reference element. + * It cares only about the first axis. You can still have poppers with margin + * between the popper and its reference element. + * @memberof modifiers + * @inner + */ + keepTogether: { + /** @prop {number} order=400 - Index used to define the order of execution */ + order: 400, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: keepTogether + }, + + /** + * This modifier is used to move the `arrowElement` of the popper to make + * sure it is positioned between the reference element and its popper element. + * It will read the outer size of the `arrowElement` node to detect how many + * pixels of conjunction are needed. + * + * It has no effect if no `arrowElement` is provided. + * @memberof modifiers + * @inner + */ + arrow: { + /** @prop {number} order=500 - Index used to define the order of execution */ + order: 500, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: arrow, + /** @prop {String|HTMLElement} element='[x-arrow]' - Selector or node used as arrow */ + element: '[x-arrow]' + }, + + /** + * Modifier used to flip the popper's placement when it starts to overlap its + * reference element. + * + * Requires the `preventOverflow` modifier before it in order to work. + * + * **NOTE:** this modifier will interrupt the current update cycle and will + * restart it if it detects the need to flip the placement. + * @memberof modifiers + * @inner + */ + flip: { + /** @prop {number} order=600 - Index used to define the order of execution */ + order: 600, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: flip, + /** + * @prop {String|Array} behavior='flip' + * The behavior used to change the popper's placement. It can be one of + * `flip`, `clockwise`, `counterclockwise` or an array with a list of valid + * placements (with optional variations) + */ + behavior: 'flip', + /** + * @prop {number} padding=5 + * The popper will flip if it hits the edges of the `boundariesElement` + */ + padding: 5, + /** + * @prop {String|HTMLElement} boundariesElement='viewport' + * The element which will define the boundaries of the popper position. + * The popper will never be placed outside of the defined boundaries + * (except if `keepTogether` is enabled) + */ + boundariesElement: 'viewport', + /** + * @prop {Boolean} flipVariations=false + * The popper will switch placement variation between `-start` and `-end` when + * the reference element overlaps its boundaries. + * + * The original placement should have a set variation. + */ + flipVariations: false, + /** + * @prop {Boolean} flipVariationsByContent=false + * The popper will switch placement variation between `-start` and `-end` when + * the popper element overlaps its reference boundaries. + * + * The original placement should have a set variation. + */ + flipVariationsByContent: false + }, + + /** + * Modifier used to make the popper flow toward the inner of the reference element. + * By default, when this modifier is disabled, the popper will be placed outside + * the reference element. + * @memberof modifiers + * @inner + */ + inner: { + /** @prop {number} order=700 - Index used to define the order of execution */ + order: 700, + /** @prop {Boolean} enabled=false - Whether the modifier is enabled or not */ + enabled: false, + /** @prop {ModifierFn} */ + fn: inner + }, + + /** + * Modifier used to hide the popper when its reference element is outside of the + * popper boundaries. It will set a `x-out-of-boundaries` attribute which can + * be used to hide with a CSS selector the popper when its reference is + * out of boundaries. + * + * Requires the `preventOverflow` modifier before it in order to work. + * @memberof modifiers + * @inner + */ + hide: { + /** @prop {number} order=800 - Index used to define the order of execution */ + order: 800, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: hide + }, + + /** + * Computes the style that will be applied to the popper element to gets + * properly positioned. + * + * Note that this modifier will not touch the DOM, it just prepares the styles + * so that `applyStyle` modifier can apply it. This separation is useful + * in case you need to replace `applyStyle` with a custom implementation. + * + * This modifier has `850` as `order` value to maintain backward compatibility + * with previous versions of Popper.js. Expect the modifiers ordering method + * to change in future major versions of the library. + * + * @memberof modifiers + * @inner + */ + computeStyle: { + /** @prop {number} order=850 - Index used to define the order of execution */ + order: 850, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: computeStyle, + /** + * @prop {Boolean} gpuAcceleration=true + * If true, it uses the CSS 3D transformation to position the popper. + * Otherwise, it will use the `top` and `left` properties + */ + gpuAcceleration: true, + /** + * @prop {string} [x='bottom'] + * Where to anchor the X axis (`bottom` or `top`). AKA X offset origin. + * Change this if your popper should grow in a direction different from `bottom` + */ + x: 'bottom', + /** + * @prop {string} [x='left'] + * Where to anchor the Y axis (`left` or `right`). AKA Y offset origin. + * Change this if your popper should grow in a direction different from `right` + */ + y: 'right' + }, + + /** + * Applies the computed styles to the popper element. + * + * All the DOM manipulations are limited to this modifier. This is useful in case + * you want to integrate Popper.js inside a framework or view library and you + * want to delegate all the DOM manipulations to it. + * + * Note that if you disable this modifier, you must make sure the popper element + * has its position set to `absolute` before Popper.js can do its work! + * + * Just disable this modifier and define your own to achieve the desired effect. + * + * @memberof modifiers + * @inner + */ + applyStyle: { + /** @prop {number} order=900 - Index used to define the order of execution */ + order: 900, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: applyStyle, + /** @prop {Function} */ + onLoad: applyStyleOnLoad, + /** + * @deprecated since version 1.10.0, the property moved to `computeStyle` modifier + * @prop {Boolean} gpuAcceleration=true + * If true, it uses the CSS 3D transformation to position the popper. + * Otherwise, it will use the `top` and `left` properties + */ + gpuAcceleration: undefined + } + }; + + /** + * The `dataObject` is an object containing all the information used by Popper.js. + * This object is passed to modifiers and to the `onCreate` and `onUpdate` callbacks. + * @name dataObject + * @property {Object} data.instance The Popper.js instance + * @property {String} data.placement Placement applied to popper + * @property {String} data.originalPlacement Placement originally defined on init + * @property {Boolean} data.flipped True if popper has been flipped by flip modifier + * @property {Boolean} data.hide True if the reference element is out of boundaries, useful to know when to hide the popper + * @property {HTMLElement} data.arrowElement Node used as arrow by arrow modifier + * @property {Object} data.styles Any CSS property defined here will be applied to the popper. It expects the JavaScript nomenclature (eg. `marginBottom`) + * @property {Object} data.arrowStyles Any CSS property defined here will be applied to the popper arrow. It expects the JavaScript nomenclature (eg. `marginBottom`) + * @property {Object} data.boundaries Offsets of the popper boundaries + * @property {Object} data.offsets The measurements of popper, reference and arrow elements + * @property {Object} data.offsets.popper `top`, `left`, `width`, `height` values + * @property {Object} data.offsets.reference `top`, `left`, `width`, `height` values + * @property {Object} data.offsets.arrow] `top` and `left` offsets, only one of them will be different from 0 + */ + + /** + * Default options provided to Popper.js constructor.
+ * These can be overridden using the `options` argument of Popper.js.
+ * To override an option, simply pass an object with the same + * structure of the `options` object, as the 3rd argument. For example: + * ``` + * new Popper(ref, pop, { + * modifiers: { + * preventOverflow: { enabled: false } + * } + * }) + * ``` + * @type {Object} + * @static + * @memberof Popper + */ + var Defaults = { + /** + * Popper's placement. + * @prop {Popper.placements} placement='bottom' + */ + placement: 'bottom', + + /** + * Set this to true if you want popper to position it self in 'fixed' mode + * @prop {Boolean} positionFixed=false + */ + positionFixed: false, + + /** + * Whether events (resize, scroll) are initially enabled. + * @prop {Boolean} eventsEnabled=true + */ + eventsEnabled: true, + + /** + * Set to true if you want to automatically remove the popper when + * you call the `destroy` method. + * @prop {Boolean} removeOnDestroy=false + */ + removeOnDestroy: false, + + /** + * Callback called when the popper is created.
+ * By default, it is set to no-op.
+ * Access Popper.js instance with `data.instance`. + * @prop {onCreate} + */ + onCreate: function onCreate() {}, + + /** + * Callback called when the popper is updated. This callback is not called + * on the initialization/creation of the popper, but only on subsequent + * updates.
+ * By default, it is set to no-op.
+ * Access Popper.js instance with `data.instance`. + * @prop {onUpdate} + */ + onUpdate: function onUpdate() {}, + + /** + * List of modifiers used to modify the offsets before they are applied to the popper. + * They provide most of the functionalities of Popper.js. + * @prop {modifiers} + */ + modifiers: modifiers + }; + + /** + * @callback onCreate + * @param {dataObject} data + */ + + /** + * @callback onUpdate + * @param {dataObject} data + */ + + // Utils + // Methods + var Popper = function () { + /** + * Creates a new Popper.js instance. + * @class Popper + * @param {Element|referenceObject} reference - The reference element used to position the popper + * @param {Element} popper - The HTML / XML element used as the popper + * @param {Object} options - Your custom options to override the ones defined in [Defaults](#defaults) + * @return {Object} instance - The generated Popper.js instance + */ + function Popper(reference, popper) { + var _this = this; + + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + classCallCheck(this, Popper); + + this.scheduleUpdate = function () { + return requestAnimationFrame(_this.update); + }; + + // make update() debounced, so that it only runs at most once-per-tick + this.update = debounce(this.update.bind(this)); + + // with {} we create a new object with the options inside it + this.options = _extends({}, Popper.Defaults, options); + + // init state + this.state = { + isDestroyed: false, + isCreated: false, + scrollParents: [] + }; + + // get reference and popper elements (allow jQuery wrappers) + this.reference = reference && reference.jquery ? reference[0] : reference; + this.popper = popper && popper.jquery ? popper[0] : popper; + + // Deep merge modifiers options + this.options.modifiers = {}; + Object.keys(_extends({}, Popper.Defaults.modifiers, options.modifiers)).forEach(function (name) { + _this.options.modifiers[name] = _extends({}, Popper.Defaults.modifiers[name] || {}, options.modifiers ? options.modifiers[name] : {}); + }); + + // Refactoring modifiers' list (Object => Array) + this.modifiers = Object.keys(this.options.modifiers).map(function (name) { + return _extends({ + name: name + }, _this.options.modifiers[name]); + }) + // sort the modifiers by order + .sort(function (a, b) { + return a.order - b.order; + }); + + // modifiers have the ability to execute arbitrary code when Popper.js get inited + // such code is executed in the same order of its modifier + // they could add new properties to their options configuration + // BE AWARE: don't add options to `options.modifiers.name` but to `modifierOptions`! + this.modifiers.forEach(function (modifierOptions) { + if (modifierOptions.enabled && isFunction(modifierOptions.onLoad)) { + modifierOptions.onLoad(_this.reference, _this.popper, _this.options, modifierOptions, _this.state); + } + }); + + // fire the first update to position the popper in the right place + this.update(); + + var eventsEnabled = this.options.eventsEnabled; + if (eventsEnabled) { + // setup event listeners, they will take care of update the position in specific situations + this.enableEventListeners(); + } + + this.state.eventsEnabled = eventsEnabled; + } + + // We can't use class properties because they don't get listed in the + // class prototype and break stuff like Sinon stubs + + + createClass(Popper, [{ + key: 'update', + value: function update$$1() { + return update.call(this); + } + }, { + key: 'destroy', + value: function destroy$$1() { + return destroy.call(this); + } + }, { + key: 'enableEventListeners', + value: function enableEventListeners$$1() { + return enableEventListeners.call(this); + } + }, { + key: 'disableEventListeners', + value: function disableEventListeners$$1() { + return disableEventListeners.call(this); + } + + /** + * Schedules an update. It will run on the next UI update available. + * @method scheduleUpdate + * @memberof Popper + */ + + + /** + * Collection of utilities useful when writing custom modifiers. + * Starting from version 1.7, this method is available only if you + * include `popper-utils.js` before `popper.js`. + * + * **DEPRECATION**: This way to access PopperUtils is deprecated + * and will be removed in v2! Use the PopperUtils module directly instead. + * Due to the high instability of the methods contained in Utils, we can't + * guarantee them to follow semver. Use them at your own risk! + * @static + * @private + * @type {Object} + * @deprecated since version 1.8 + * @member Utils + * @memberof Popper + */ + + }]); + return Popper; + }(); + + /** + * The `referenceObject` is an object that provides an interface compatible with Popper.js + * and lets you use it as replacement of a real DOM node.
+ * You can use this method to position a popper relatively to a set of coordinates + * in case you don't have a DOM node to use as reference. + * + * ``` + * new Popper(referenceObject, popperNode); + * ``` + * + * NB: This feature isn't supported in Internet Explorer 10. + * @name referenceObject + * @property {Function} data.getBoundingClientRect + * A function that returns a set of coordinates compatible with the native `getBoundingClientRect` method. + * @property {number} data.clientWidth + * An ES6 getter that will return the width of the virtual reference element. + * @property {number} data.clientHeight + * An ES6 getter that will return the height of the virtual reference element. + */ + + + Popper.Utils = (typeof window !== 'undefined' ? window : global).PopperUtils; + Popper.placements = placements; + Popper.Defaults = Defaults; + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$4 = 'dropdown'; + var VERSION$4 = '4.4.1'; + var DATA_KEY$4 = 'bs.dropdown'; + var EVENT_KEY$4 = "." + DATA_KEY$4; + var DATA_API_KEY$4 = '.data-api'; + var JQUERY_NO_CONFLICT$4 = $.fn[NAME$4]; + var ESCAPE_KEYCODE = 27; // KeyboardEvent.which value for Escape (Esc) key + + var SPACE_KEYCODE = 32; // KeyboardEvent.which value for space key + + var TAB_KEYCODE = 9; // KeyboardEvent.which value for tab key + + var ARROW_UP_KEYCODE = 38; // KeyboardEvent.which value for up arrow key + + var ARROW_DOWN_KEYCODE = 40; // KeyboardEvent.which value for down arrow key + + var RIGHT_MOUSE_BUTTON_WHICH = 3; // MouseEvent.which value for the right button (assuming a right-handed mouse) + + var REGEXP_KEYDOWN = new RegExp(ARROW_UP_KEYCODE + "|" + ARROW_DOWN_KEYCODE + "|" + ESCAPE_KEYCODE); + var Event$4 = { + HIDE: "hide" + EVENT_KEY$4, + HIDDEN: "hidden" + EVENT_KEY$4, + SHOW: "show" + EVENT_KEY$4, + SHOWN: "shown" + EVENT_KEY$4, + CLICK: "click" + EVENT_KEY$4, + CLICK_DATA_API: "click" + EVENT_KEY$4 + DATA_API_KEY$4, + KEYDOWN_DATA_API: "keydown" + EVENT_KEY$4 + DATA_API_KEY$4, + KEYUP_DATA_API: "keyup" + EVENT_KEY$4 + DATA_API_KEY$4 + }; + var ClassName$4 = { + DISABLED: 'disabled', + SHOW: 'show', + DROPUP: 'dropup', + DROPRIGHT: 'dropright', + DROPLEFT: 'dropleft', + MENURIGHT: 'dropdown-menu-right', + MENULEFT: 'dropdown-menu-left', + POSITION_STATIC: 'position-static' + }; + var Selector$4 = { + DATA_TOGGLE: '[data-toggle="dropdown"]', + FORM_CHILD: '.dropdown form', + MENU: '.dropdown-menu', + NAVBAR_NAV: '.navbar-nav', + VISIBLE_ITEMS: '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)' + }; + var AttachmentMap = { + TOP: 'top-start', + TOPEND: 'top-end', + BOTTOM: 'bottom-start', + BOTTOMEND: 'bottom-end', + RIGHT: 'right-start', + RIGHTEND: 'right-end', + LEFT: 'left-start', + LEFTEND: 'left-end' + }; + var Default$2 = { + offset: 0, + flip: true, + boundary: 'scrollParent', + reference: 'toggle', + display: 'dynamic', + popperConfig: null + }; + var DefaultType$2 = { + offset: '(number|string|function)', + flip: 'boolean', + boundary: '(string|element)', + reference: '(string|element)', + display: 'string', + popperConfig: '(null|object)' + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Dropdown = + /*#__PURE__*/ + function () { + function Dropdown(element, config) { + this._element = element; + this._popper = null; + this._config = this._getConfig(config); + this._menu = this._getMenuElement(); + this._inNavbar = this._detectNavbar(); + + this._addEventListeners(); + } // Getters + + + var _proto = Dropdown.prototype; + + // Public + _proto.toggle = function toggle() { + if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED)) { + return; + } + + var isActive = $(this._menu).hasClass(ClassName$4.SHOW); + + Dropdown._clearMenus(); + + if (isActive) { + return; + } + + this.show(true); + }; + + _proto.show = function show(usePopper) { + if (usePopper === void 0) { + usePopper = false; + } + + if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED) || $(this._menu).hasClass(ClassName$4.SHOW)) { + return; + } + + var relatedTarget = { + relatedTarget: this._element + }; + var showEvent = $.Event(Event$4.SHOW, relatedTarget); + + var parent = Dropdown._getParentFromElement(this._element); + + $(parent).trigger(showEvent); + + if (showEvent.isDefaultPrevented()) { + return; + } // Disable totally Popper.js for Dropdown in Navbar + + + if (!this._inNavbar && usePopper) { + /** + * Check for Popper dependency + * Popper - https://popper.js.org + */ + if (typeof Popper === 'undefined') { + throw new TypeError('Bootstrap\'s dropdowns require Popper.js (https://popper.js.org/)'); + } + + var referenceElement = this._element; + + if (this._config.reference === 'parent') { + referenceElement = parent; + } else if (Util.isElement(this._config.reference)) { + referenceElement = this._config.reference; // Check if it's jQuery element + + if (typeof this._config.reference.jquery !== 'undefined') { + referenceElement = this._config.reference[0]; + } + } // If boundary is not `scrollParent`, then set position to `static` + // to allow the menu to "escape" the scroll parent's boundaries + // https://github.com/twbs/bootstrap/issues/24251 + + + if (this._config.boundary !== 'scrollParent') { + $(parent).addClass(ClassName$4.POSITION_STATIC); + } + + this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig()); + } // If this is a touch-enabled device we add extra + // empty mouseover listeners to the body's immediate children; + // only needed because of broken event delegation on iOS + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + + + if ('ontouchstart' in document.documentElement && $(parent).closest(Selector$4.NAVBAR_NAV).length === 0) { + $(document.body).children().on('mouseover', null, $.noop); + } + + this._element.focus(); + + this._element.setAttribute('aria-expanded', true); + + $(this._menu).toggleClass(ClassName$4.SHOW); + $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.SHOWN, relatedTarget)); + }; + + _proto.hide = function hide() { + if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED) || !$(this._menu).hasClass(ClassName$4.SHOW)) { + return; + } + + var relatedTarget = { + relatedTarget: this._element + }; + var hideEvent = $.Event(Event$4.HIDE, relatedTarget); + + var parent = Dropdown._getParentFromElement(this._element); + + $(parent).trigger(hideEvent); + + if (hideEvent.isDefaultPrevented()) { + return; + } + + if (this._popper) { + this._popper.destroy(); + } + + $(this._menu).toggleClass(ClassName$4.SHOW); + $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.HIDDEN, relatedTarget)); + }; + + _proto.dispose = function dispose() { + $.removeData(this._element, DATA_KEY$4); + $(this._element).off(EVENT_KEY$4); + this._element = null; + this._menu = null; + + if (this._popper !== null) { + this._popper.destroy(); + + this._popper = null; + } + }; + + _proto.update = function update() { + this._inNavbar = this._detectNavbar(); + + if (this._popper !== null) { + this._popper.scheduleUpdate(); + } + } // Private + ; + + _proto._addEventListeners = function _addEventListeners() { + var _this = this; + + $(this._element).on(Event$4.CLICK, function (event) { + event.preventDefault(); + event.stopPropagation(); + + _this.toggle(); + }); + }; + + _proto._getConfig = function _getConfig(config) { + config = _objectSpread2({}, this.constructor.Default, {}, $(this._element).data(), {}, config); + Util.typeCheckConfig(NAME$4, config, this.constructor.DefaultType); + return config; + }; + + _proto._getMenuElement = function _getMenuElement() { + if (!this._menu) { + var parent = Dropdown._getParentFromElement(this._element); + + if (parent) { + this._menu = parent.querySelector(Selector$4.MENU); + } + } + + return this._menu; + }; + + _proto._getPlacement = function _getPlacement() { + var $parentDropdown = $(this._element.parentNode); + var placement = AttachmentMap.BOTTOM; // Handle dropup + + if ($parentDropdown.hasClass(ClassName$4.DROPUP)) { + placement = AttachmentMap.TOP; + + if ($(this._menu).hasClass(ClassName$4.MENURIGHT)) { + placement = AttachmentMap.TOPEND; + } + } else if ($parentDropdown.hasClass(ClassName$4.DROPRIGHT)) { + placement = AttachmentMap.RIGHT; + } else if ($parentDropdown.hasClass(ClassName$4.DROPLEFT)) { + placement = AttachmentMap.LEFT; + } else if ($(this._menu).hasClass(ClassName$4.MENURIGHT)) { + placement = AttachmentMap.BOTTOMEND; + } + + return placement; + }; + + _proto._detectNavbar = function _detectNavbar() { + return $(this._element).closest('.navbar').length > 0; + }; + + _proto._getOffset = function _getOffset() { + var _this2 = this; + + var offset = {}; + + if (typeof this._config.offset === 'function') { + offset.fn = function (data) { + data.offsets = _objectSpread2({}, data.offsets, {}, _this2._config.offset(data.offsets, _this2._element) || {}); + return data; + }; + } else { + offset.offset = this._config.offset; + } + + return offset; + }; + + _proto._getPopperConfig = function _getPopperConfig() { + var popperConfig = { + placement: this._getPlacement(), + modifiers: { + offset: this._getOffset(), + flip: { + enabled: this._config.flip + }, + preventOverflow: { + boundariesElement: this._config.boundary + } + } + }; // Disable Popper.js if we have a static display + + if (this._config.display === 'static') { + popperConfig.modifiers.applyStyle = { + enabled: false + }; + } + + return _objectSpread2({}, popperConfig, {}, this._config.popperConfig); + } // Static + ; + + Dropdown._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $(this).data(DATA_KEY$4); + + var _config = typeof config === 'object' ? config : null; + + if (!data) { + data = new Dropdown(this, _config); + $(this).data(DATA_KEY$4, data); + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError("No method named \"" + config + "\""); + } + + data[config](); + } + }); + }; + + Dropdown._clearMenus = function _clearMenus(event) { + if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH || event.type === 'keyup' && event.which !== TAB_KEYCODE)) { + return; + } + + var toggles = [].slice.call(document.querySelectorAll(Selector$4.DATA_TOGGLE)); + + for (var i = 0, len = toggles.length; i < len; i++) { + var parent = Dropdown._getParentFromElement(toggles[i]); + + var context = $(toggles[i]).data(DATA_KEY$4); + var relatedTarget = { + relatedTarget: toggles[i] + }; + + if (event && event.type === 'click') { + relatedTarget.clickEvent = event; + } + + if (!context) { + continue; + } + + var dropdownMenu = context._menu; + + if (!$(parent).hasClass(ClassName$4.SHOW)) { + continue; + } + + if (event && (event.type === 'click' && /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) && $.contains(parent, event.target)) { + continue; + } + + var hideEvent = $.Event(Event$4.HIDE, relatedTarget); + $(parent).trigger(hideEvent); + + if (hideEvent.isDefaultPrevented()) { + continue; + } // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + + + if ('ontouchstart' in document.documentElement) { + $(document.body).children().off('mouseover', null, $.noop); + } + + toggles[i].setAttribute('aria-expanded', 'false'); + + if (context._popper) { + context._popper.destroy(); + } + + $(dropdownMenu).removeClass(ClassName$4.SHOW); + $(parent).removeClass(ClassName$4.SHOW).trigger($.Event(Event$4.HIDDEN, relatedTarget)); + } + }; + + Dropdown._getParentFromElement = function _getParentFromElement(element) { + var parent; + var selector = Util.getSelectorFromElement(element); + + if (selector) { + parent = document.querySelector(selector); + } + + return parent || element.parentNode; + } // eslint-disable-next-line complexity + ; + + Dropdown._dataApiKeydownHandler = function _dataApiKeydownHandler(event) { + // If not input/textarea: + // - And not a key in REGEXP_KEYDOWN => not a dropdown command + // If input/textarea: + // - If space key => not a dropdown command + // - If key is other than escape + // - If key is not up or down => not a dropdown command + // - If trigger inside the menu => not a dropdown command + if (/input|textarea/i.test(event.target.tagName) ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE && (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE || $(event.target).closest(Selector$4.MENU).length) : !REGEXP_KEYDOWN.test(event.which)) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + if (this.disabled || $(this).hasClass(ClassName$4.DISABLED)) { + return; + } + + var parent = Dropdown._getParentFromElement(this); + + var isActive = $(parent).hasClass(ClassName$4.SHOW); + + if (!isActive && event.which === ESCAPE_KEYCODE) { + return; + } + + if (!isActive || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) { + if (event.which === ESCAPE_KEYCODE) { + var toggle = parent.querySelector(Selector$4.DATA_TOGGLE); + $(toggle).trigger('focus'); + } + + $(this).trigger('click'); + return; + } + + var items = [].slice.call(parent.querySelectorAll(Selector$4.VISIBLE_ITEMS)).filter(function (item) { + return $(item).is(':visible'); + }); + + if (items.length === 0) { + return; + } + + var index = items.indexOf(event.target); + + if (event.which === ARROW_UP_KEYCODE && index > 0) { + // Up + index--; + } + + if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { + // Down + index++; + } + + if (index < 0) { + index = 0; + } + + items[index].focus(); + }; + + _createClass(Dropdown, null, [{ + key: "VERSION", + get: function get() { + return VERSION$4; + } + }, { + key: "Default", + get: function get() { + return Default$2; + } + }, { + key: "DefaultType", + get: function get() { + return DefaultType$2; + } + }]); + + return Dropdown; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $(document).on(Event$4.KEYDOWN_DATA_API, Selector$4.DATA_TOGGLE, Dropdown._dataApiKeydownHandler).on(Event$4.KEYDOWN_DATA_API, Selector$4.MENU, Dropdown._dataApiKeydownHandler).on(Event$4.CLICK_DATA_API + " " + Event$4.KEYUP_DATA_API, Dropdown._clearMenus).on(Event$4.CLICK_DATA_API, Selector$4.DATA_TOGGLE, function (event) { + event.preventDefault(); + event.stopPropagation(); + + Dropdown._jQueryInterface.call($(this), 'toggle'); + }).on(Event$4.CLICK_DATA_API, Selector$4.FORM_CHILD, function (e) { + e.stopPropagation(); + }); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $.fn[NAME$4] = Dropdown._jQueryInterface; + $.fn[NAME$4].Constructor = Dropdown; + + $.fn[NAME$4].noConflict = function () { + $.fn[NAME$4] = JQUERY_NO_CONFLICT$4; + return Dropdown._jQueryInterface; + }; + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$5 = 'modal'; + var VERSION$5 = '4.4.1'; + var DATA_KEY$5 = 'bs.modal'; + var EVENT_KEY$5 = "." + DATA_KEY$5; + var DATA_API_KEY$5 = '.data-api'; + var JQUERY_NO_CONFLICT$5 = $.fn[NAME$5]; + var ESCAPE_KEYCODE$1 = 27; // KeyboardEvent.which value for Escape (Esc) key + + var Default$3 = { + backdrop: true, + keyboard: true, + focus: true, + show: true + }; + var DefaultType$3 = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + focus: 'boolean', + show: 'boolean' + }; + var Event$5 = { + HIDE: "hide" + EVENT_KEY$5, + HIDE_PREVENTED: "hidePrevented" + EVENT_KEY$5, + HIDDEN: "hidden" + EVENT_KEY$5, + SHOW: "show" + EVENT_KEY$5, + SHOWN: "shown" + EVENT_KEY$5, + FOCUSIN: "focusin" + EVENT_KEY$5, + RESIZE: "resize" + EVENT_KEY$5, + CLICK_DISMISS: "click.dismiss" + EVENT_KEY$5, + KEYDOWN_DISMISS: "keydown.dismiss" + EVENT_KEY$5, + MOUSEUP_DISMISS: "mouseup.dismiss" + EVENT_KEY$5, + MOUSEDOWN_DISMISS: "mousedown.dismiss" + EVENT_KEY$5, + CLICK_DATA_API: "click" + EVENT_KEY$5 + DATA_API_KEY$5 + }; + var ClassName$5 = { + SCROLLABLE: 'modal-dialog-scrollable', + SCROLLBAR_MEASURER: 'modal-scrollbar-measure', + BACKDROP: 'modal-backdrop', + OPEN: 'modal-open', + FADE: 'fade', + SHOW: 'show', + STATIC: 'modal-static' + }; + var Selector$5 = { + DIALOG: '.modal-dialog', + MODAL_BODY: '.modal-body', + DATA_TOGGLE: '[data-toggle="modal"]', + DATA_DISMISS: '[data-dismiss="modal"]', + FIXED_CONTENT: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top', + STICKY_CONTENT: '.sticky-top' + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Modal = + /*#__PURE__*/ + function () { + function Modal(element, config) { + this._config = this._getConfig(config); + this._element = element; + this._dialog = element.querySelector(Selector$5.DIALOG); + this._backdrop = null; + this._isShown = false; + this._isBodyOverflowing = false; + this._ignoreBackdropClick = false; + this._isTransitioning = false; + this._scrollbarWidth = 0; + } // Getters + + + var _proto = Modal.prototype; + + // Public + _proto.toggle = function toggle(relatedTarget) { + return this._isShown ? this.hide() : this.show(relatedTarget); + }; + + _proto.show = function show(relatedTarget) { + var _this = this; + + if (this._isShown || this._isTransitioning) { + return; + } + + if ($(this._element).hasClass(ClassName$5.FADE)) { + this._isTransitioning = true; + } + + var showEvent = $.Event(Event$5.SHOW, { + relatedTarget: relatedTarget + }); + $(this._element).trigger(showEvent); + + if (this._isShown || showEvent.isDefaultPrevented()) { + return; + } + + this._isShown = true; + + this._checkScrollbar(); + + this._setScrollbar(); + + this._adjustDialog(); + + this._setEscapeEvent(); + + this._setResizeEvent(); + + $(this._element).on(Event$5.CLICK_DISMISS, Selector$5.DATA_DISMISS, function (event) { + return _this.hide(event); + }); + $(this._dialog).on(Event$5.MOUSEDOWN_DISMISS, function () { + $(_this._element).one(Event$5.MOUSEUP_DISMISS, function (event) { + if ($(event.target).is(_this._element)) { + _this._ignoreBackdropClick = true; + } + }); + }); + + this._showBackdrop(function () { + return _this._showElement(relatedTarget); + }); + }; + + _proto.hide = function hide(event) { + var _this2 = this; + + if (event) { + event.preventDefault(); + } + + if (!this._isShown || this._isTransitioning) { + return; + } + + var hideEvent = $.Event(Event$5.HIDE); + $(this._element).trigger(hideEvent); + + if (!this._isShown || hideEvent.isDefaultPrevented()) { + return; + } + + this._isShown = false; + var transition = $(this._element).hasClass(ClassName$5.FADE); + + if (transition) { + this._isTransitioning = true; + } + + this._setEscapeEvent(); + + this._setResizeEvent(); + + $(document).off(Event$5.FOCUSIN); + $(this._element).removeClass(ClassName$5.SHOW); + $(this._element).off(Event$5.CLICK_DISMISS); + $(this._dialog).off(Event$5.MOUSEDOWN_DISMISS); + + if (transition) { + var transitionDuration = Util.getTransitionDurationFromElement(this._element); + $(this._element).one(Util.TRANSITION_END, function (event) { + return _this2._hideModal(event); + }).emulateTransitionEnd(transitionDuration); + } else { + this._hideModal(); + } + }; + + _proto.dispose = function dispose() { + [window, this._element, this._dialog].forEach(function (htmlElement) { + return $(htmlElement).off(EVENT_KEY$5); + }); + /** + * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API` + * Do not move `document` in `htmlElements` array + * It will remove `Event.CLICK_DATA_API` event that should remain + */ + + $(document).off(Event$5.FOCUSIN); + $.removeData(this._element, DATA_KEY$5); + this._config = null; + this._element = null; + this._dialog = null; + this._backdrop = null; + this._isShown = null; + this._isBodyOverflowing = null; + this._ignoreBackdropClick = null; + this._isTransitioning = null; + this._scrollbarWidth = null; + }; + + _proto.handleUpdate = function handleUpdate() { + this._adjustDialog(); + } // Private + ; + + _proto._getConfig = function _getConfig(config) { + config = _objectSpread2({}, Default$3, {}, config); + Util.typeCheckConfig(NAME$5, config, DefaultType$3); + return config; + }; + + _proto._triggerBackdropTransition = function _triggerBackdropTransition() { + var _this3 = this; + + if (this._config.backdrop === 'static') { + var hideEventPrevented = $.Event(Event$5.HIDE_PREVENTED); + $(this._element).trigger(hideEventPrevented); + + if (hideEventPrevented.defaultPrevented) { + return; + } + + this._element.classList.add(ClassName$5.STATIC); + + var modalTransitionDuration = Util.getTransitionDurationFromElement(this._element); + $(this._element).one(Util.TRANSITION_END, function () { + _this3._element.classList.remove(ClassName$5.STATIC); + }).emulateTransitionEnd(modalTransitionDuration); + + this._element.focus(); + } else { + this.hide(); + } + }; + + _proto._showElement = function _showElement(relatedTarget) { + var _this4 = this; + + var transition = $(this._element).hasClass(ClassName$5.FADE); + var modalBody = this._dialog ? this._dialog.querySelector(Selector$5.MODAL_BODY) : null; + + if (!this._element.parentNode || this._element.parentNode.nodeType !== Node.ELEMENT_NODE) { + // Don't move modal's DOM position + document.body.appendChild(this._element); + } + + this._element.style.display = 'block'; + + this._element.removeAttribute('aria-hidden'); + + this._element.setAttribute('aria-modal', true); + + if ($(this._dialog).hasClass(ClassName$5.SCROLLABLE) && modalBody) { + modalBody.scrollTop = 0; + } else { + this._element.scrollTop = 0; + } + + if (transition) { + Util.reflow(this._element); + } + + $(this._element).addClass(ClassName$5.SHOW); + + if (this._config.focus) { + this._enforceFocus(); + } + + var shownEvent = $.Event(Event$5.SHOWN, { + relatedTarget: relatedTarget + }); + + var transitionComplete = function transitionComplete() { + if (_this4._config.focus) { + _this4._element.focus(); + } + + _this4._isTransitioning = false; + $(_this4._element).trigger(shownEvent); + }; + + if (transition) { + var transitionDuration = Util.getTransitionDurationFromElement(this._dialog); + $(this._dialog).one(Util.TRANSITION_END, transitionComplete).emulateTransitionEnd(transitionDuration); + } else { + transitionComplete(); + } + }; + + _proto._enforceFocus = function _enforceFocus() { + var _this5 = this; + + $(document).off(Event$5.FOCUSIN) // Guard against infinite focus loop + .on(Event$5.FOCUSIN, function (event) { + if (document !== event.target && _this5._element !== event.target && $(_this5._element).has(event.target).length === 0) { + _this5._element.focus(); + } + }); + }; + + _proto._setEscapeEvent = function _setEscapeEvent() { + var _this6 = this; + + if (this._isShown && this._config.keyboard) { + $(this._element).on(Event$5.KEYDOWN_DISMISS, function (event) { + if (event.which === ESCAPE_KEYCODE$1) { + _this6._triggerBackdropTransition(); + } + }); + } else if (!this._isShown) { + $(this._element).off(Event$5.KEYDOWN_DISMISS); + } + }; + + _proto._setResizeEvent = function _setResizeEvent() { + var _this7 = this; + + if (this._isShown) { + $(window).on(Event$5.RESIZE, function (event) { + return _this7.handleUpdate(event); + }); + } else { + $(window).off(Event$5.RESIZE); + } + }; + + _proto._hideModal = function _hideModal() { + var _this8 = this; + + this._element.style.display = 'none'; + + this._element.setAttribute('aria-hidden', true); + + this._element.removeAttribute('aria-modal'); + + this._isTransitioning = false; + + this._showBackdrop(function () { + $(document.body).removeClass(ClassName$5.OPEN); + + _this8._resetAdjustments(); + + _this8._resetScrollbar(); + + $(_this8._element).trigger(Event$5.HIDDEN); + }); + }; + + _proto._removeBackdrop = function _removeBackdrop() { + if (this._backdrop) { + $(this._backdrop).remove(); + this._backdrop = null; + } + }; + + _proto._showBackdrop = function _showBackdrop(callback) { + var _this9 = this; + + var animate = $(this._element).hasClass(ClassName$5.FADE) ? ClassName$5.FADE : ''; + + if (this._isShown && this._config.backdrop) { + this._backdrop = document.createElement('div'); + this._backdrop.className = ClassName$5.BACKDROP; + + if (animate) { + this._backdrop.classList.add(animate); + } + + $(this._backdrop).appendTo(document.body); + $(this._element).on(Event$5.CLICK_DISMISS, function (event) { + if (_this9._ignoreBackdropClick) { + _this9._ignoreBackdropClick = false; + return; + } + + if (event.target !== event.currentTarget) { + return; + } + + _this9._triggerBackdropTransition(); + }); + + if (animate) { + Util.reflow(this._backdrop); + } + + $(this._backdrop).addClass(ClassName$5.SHOW); + + if (!callback) { + return; + } + + if (!animate) { + callback(); + return; + } + + var backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop); + $(this._backdrop).one(Util.TRANSITION_END, callback).emulateTransitionEnd(backdropTransitionDuration); + } else if (!this._isShown && this._backdrop) { + $(this._backdrop).removeClass(ClassName$5.SHOW); + + var callbackRemove = function callbackRemove() { + _this9._removeBackdrop(); + + if (callback) { + callback(); + } + }; + + if ($(this._element).hasClass(ClassName$5.FADE)) { + var _backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop); + + $(this._backdrop).one(Util.TRANSITION_END, callbackRemove).emulateTransitionEnd(_backdropTransitionDuration); + } else { + callbackRemove(); + } + } else if (callback) { + callback(); + } + } // ---------------------------------------------------------------------- + // the following methods are used to handle overflowing modals + // todo (fat): these should probably be refactored out of modal.js + // ---------------------------------------------------------------------- + ; + + _proto._adjustDialog = function _adjustDialog() { + var isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; + + if (!this._isBodyOverflowing && isModalOverflowing) { + this._element.style.paddingLeft = this._scrollbarWidth + "px"; + } + + if (this._isBodyOverflowing && !isModalOverflowing) { + this._element.style.paddingRight = this._scrollbarWidth + "px"; + } + }; + + _proto._resetAdjustments = function _resetAdjustments() { + this._element.style.paddingLeft = ''; + this._element.style.paddingRight = ''; + }; + + _proto._checkScrollbar = function _checkScrollbar() { + var rect = document.body.getBoundingClientRect(); + this._isBodyOverflowing = rect.left + rect.right < window.innerWidth; + this._scrollbarWidth = this._getScrollbarWidth(); + }; + + _proto._setScrollbar = function _setScrollbar() { + var _this10 = this; + + if (this._isBodyOverflowing) { + // Note: DOMNode.style.paddingRight returns the actual value or '' if not set + // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set + var fixedContent = [].slice.call(document.querySelectorAll(Selector$5.FIXED_CONTENT)); + var stickyContent = [].slice.call(document.querySelectorAll(Selector$5.STICKY_CONTENT)); // Adjust fixed content padding + + $(fixedContent).each(function (index, element) { + var actualPadding = element.style.paddingRight; + var calculatedPadding = $(element).css('padding-right'); + $(element).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + _this10._scrollbarWidth + "px"); + }); // Adjust sticky content margin + + $(stickyContent).each(function (index, element) { + var actualMargin = element.style.marginRight; + var calculatedMargin = $(element).css('margin-right'); + $(element).data('margin-right', actualMargin).css('margin-right', parseFloat(calculatedMargin) - _this10._scrollbarWidth + "px"); + }); // Adjust body padding + + var actualPadding = document.body.style.paddingRight; + var calculatedPadding = $(document.body).css('padding-right'); + $(document.body).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + this._scrollbarWidth + "px"); + } + + $(document.body).addClass(ClassName$5.OPEN); + }; + + _proto._resetScrollbar = function _resetScrollbar() { + // Restore fixed content padding + var fixedContent = [].slice.call(document.querySelectorAll(Selector$5.FIXED_CONTENT)); + $(fixedContent).each(function (index, element) { + var padding = $(element).data('padding-right'); + $(element).removeData('padding-right'); + element.style.paddingRight = padding ? padding : ''; + }); // Restore sticky content + + var elements = [].slice.call(document.querySelectorAll("" + Selector$5.STICKY_CONTENT)); + $(elements).each(function (index, element) { + var margin = $(element).data('margin-right'); + + if (typeof margin !== 'undefined') { + $(element).css('margin-right', margin).removeData('margin-right'); + } + }); // Restore body padding + + var padding = $(document.body).data('padding-right'); + $(document.body).removeData('padding-right'); + document.body.style.paddingRight = padding ? padding : ''; + }; + + _proto._getScrollbarWidth = function _getScrollbarWidth() { + // thx d.walsh + var scrollDiv = document.createElement('div'); + scrollDiv.className = ClassName$5.SCROLLBAR_MEASURER; + document.body.appendChild(scrollDiv); + var scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth; + document.body.removeChild(scrollDiv); + return scrollbarWidth; + } // Static + ; + + Modal._jQueryInterface = function _jQueryInterface(config, relatedTarget) { + return this.each(function () { + var data = $(this).data(DATA_KEY$5); + + var _config = _objectSpread2({}, Default$3, {}, $(this).data(), {}, typeof config === 'object' && config ? config : {}); + + if (!data) { + data = new Modal(this, _config); + $(this).data(DATA_KEY$5, data); + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError("No method named \"" + config + "\""); + } + + data[config](relatedTarget); + } else if (_config.show) { + data.show(relatedTarget); + } + }); + }; + + _createClass(Modal, null, [{ + key: "VERSION", + get: function get() { + return VERSION$5; + } + }, { + key: "Default", + get: function get() { + return Default$3; + } + }]); + + return Modal; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $(document).on(Event$5.CLICK_DATA_API, Selector$5.DATA_TOGGLE, function (event) { + var _this11 = this; + + var target; + var selector = Util.getSelectorFromElement(this); + + if (selector) { + target = document.querySelector(selector); + } + + var config = $(target).data(DATA_KEY$5) ? 'toggle' : _objectSpread2({}, $(target).data(), {}, $(this).data()); + + if (this.tagName === 'A' || this.tagName === 'AREA') { + event.preventDefault(); + } + + var $target = $(target).one(Event$5.SHOW, function (showEvent) { + if (showEvent.isDefaultPrevented()) { + // Only register focus restorer if modal will actually get shown + return; + } + + $target.one(Event$5.HIDDEN, function () { + if ($(_this11).is(':visible')) { + _this11.focus(); + } + }); + }); + + Modal._jQueryInterface.call($(target), config, this); + }); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $.fn[NAME$5] = Modal._jQueryInterface; + $.fn[NAME$5].Constructor = Modal; + + $.fn[NAME$5].noConflict = function () { + $.fn[NAME$5] = JQUERY_NO_CONFLICT$5; + return Modal._jQueryInterface; + }; + + /** + * -------------------------------------------------------------------------- + * Bootstrap (v4.4.1): tools/sanitizer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + var uriAttrs = ['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']; + var ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; + var DefaultWhitelist = { + // Global attributes allowed on any supplied element below. + '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN], + a: ['target', 'href', 'title', 'rel'], + area: [], + b: [], + br: [], + col: [], + code: [], + div: [], + em: [], + hr: [], + h1: [], + h2: [], + h3: [], + h4: [], + h5: [], + h6: [], + i: [], + img: ['src', 'alt', 'title', 'width', 'height'], + li: [], + ol: [], + p: [], + pre: [], + s: [], + small: [], + span: [], + sub: [], + sup: [], + strong: [], + u: [], + ul: [] + }; + /** + * A pattern that recognizes a commonly useful subset of URLs that are safe. + * + * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts + */ + + var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi; + /** + * A pattern that matches safe data URLs. Only matches image, video and audio types. + * + * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts + */ + + var DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i; + + function allowedAttribute(attr, allowedAttributeList) { + var attrName = attr.nodeName.toLowerCase(); + + if (allowedAttributeList.indexOf(attrName) !== -1) { + if (uriAttrs.indexOf(attrName) !== -1) { + return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN)); + } + + return true; + } + + var regExp = allowedAttributeList.filter(function (attrRegex) { + return attrRegex instanceof RegExp; + }); // Check if a regular expression validates the attribute. + + for (var i = 0, l = regExp.length; i < l; i++) { + if (attrName.match(regExp[i])) { + return true; + } + } + + return false; + } + + function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) { + if (unsafeHtml.length === 0) { + return unsafeHtml; + } + + if (sanitizeFn && typeof sanitizeFn === 'function') { + return sanitizeFn(unsafeHtml); + } + + var domParser = new window.DOMParser(); + var createdDocument = domParser.parseFromString(unsafeHtml, 'text/html'); + var whitelistKeys = Object.keys(whiteList); + var elements = [].slice.call(createdDocument.body.querySelectorAll('*')); + + var _loop = function _loop(i, len) { + var el = elements[i]; + var elName = el.nodeName.toLowerCase(); + + if (whitelistKeys.indexOf(el.nodeName.toLowerCase()) === -1) { + el.parentNode.removeChild(el); + return "continue"; + } + + var attributeList = [].slice.call(el.attributes); + var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || []); + attributeList.forEach(function (attr) { + if (!allowedAttribute(attr, whitelistedAttributes)) { + el.removeAttribute(attr.nodeName); + } + }); + }; + + for (var i = 0, len = elements.length; i < len; i++) { + var _ret = _loop(i); + + if (_ret === "continue") continue; + } + + return createdDocument.body.innerHTML; + } + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$6 = 'tooltip'; + var VERSION$6 = '4.4.1'; + var DATA_KEY$6 = 'bs.tooltip'; + var EVENT_KEY$6 = "." + DATA_KEY$6; + var JQUERY_NO_CONFLICT$6 = $.fn[NAME$6]; + var CLASS_PREFIX = 'bs-tooltip'; + var BSCLS_PREFIX_REGEX = new RegExp("(^|\\s)" + CLASS_PREFIX + "\\S+", 'g'); + var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']; + var DefaultType$4 = { + animation: 'boolean', + template: 'string', + title: '(string|element|function)', + trigger: 'string', + delay: '(number|object)', + html: 'boolean', + selector: '(string|boolean)', + placement: '(string|function)', + offset: '(number|string|function)', + container: '(string|element|boolean)', + fallbackPlacement: '(string|array)', + boundary: '(string|element)', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + whiteList: 'object', + popperConfig: '(null|object)' + }; + var AttachmentMap$1 = { + AUTO: 'auto', + TOP: 'top', + RIGHT: 'right', + BOTTOM: 'bottom', + LEFT: 'left' + }; + var Default$4 = { + animation: true, + template: '', + trigger: 'hover focus', + title: '', + delay: 0, + html: false, + selector: false, + placement: 'top', + offset: 0, + container: false, + fallbackPlacement: 'flip', + boundary: 'scrollParent', + sanitize: true, + sanitizeFn: null, + whiteList: DefaultWhitelist, + popperConfig: null + }; + var HoverState = { + SHOW: 'show', + OUT: 'out' + }; + var Event$6 = { + HIDE: "hide" + EVENT_KEY$6, + HIDDEN: "hidden" + EVENT_KEY$6, + SHOW: "show" + EVENT_KEY$6, + SHOWN: "shown" + EVENT_KEY$6, + INSERTED: "inserted" + EVENT_KEY$6, + CLICK: "click" + EVENT_KEY$6, + FOCUSIN: "focusin" + EVENT_KEY$6, + FOCUSOUT: "focusout" + EVENT_KEY$6, + MOUSEENTER: "mouseenter" + EVENT_KEY$6, + MOUSELEAVE: "mouseleave" + EVENT_KEY$6 + }; + var ClassName$6 = { + FADE: 'fade', + SHOW: 'show' + }; + var Selector$6 = { + TOOLTIP: '.tooltip', + TOOLTIP_INNER: '.tooltip-inner', + ARROW: '.arrow' + }; + var Trigger = { + HOVER: 'hover', + FOCUS: 'focus', + CLICK: 'click', + MANUAL: 'manual' + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Tooltip = + /*#__PURE__*/ + function () { + function Tooltip(element, config) { + if (typeof Popper === 'undefined') { + throw new TypeError('Bootstrap\'s tooltips require Popper.js (https://popper.js.org/)'); + } // private + + + this._isEnabled = true; + this._timeout = 0; + this._hoverState = ''; + this._activeTrigger = {}; + this._popper = null; // Protected + + this.element = element; + this.config = this._getConfig(config); + this.tip = null; + + this._setListeners(); + } // Getters + + + var _proto = Tooltip.prototype; + + // Public + _proto.enable = function enable() { + this._isEnabled = true; + }; + + _proto.disable = function disable() { + this._isEnabled = false; + }; + + _proto.toggleEnabled = function toggleEnabled() { + this._isEnabled = !this._isEnabled; + }; + + _proto.toggle = function toggle(event) { + if (!this._isEnabled) { + return; + } + + if (event) { + var dataKey = this.constructor.DATA_KEY; + var context = $(event.currentTarget).data(dataKey); + + if (!context) { + context = new this.constructor(event.currentTarget, this._getDelegateConfig()); + $(event.currentTarget).data(dataKey, context); + } + + context._activeTrigger.click = !context._activeTrigger.click; + + if (context._isWithActiveTrigger()) { + context._enter(null, context); + } else { + context._leave(null, context); + } + } else { + if ($(this.getTipElement()).hasClass(ClassName$6.SHOW)) { + this._leave(null, this); + + return; + } + + this._enter(null, this); + } + }; + + _proto.dispose = function dispose() { + clearTimeout(this._timeout); + $.removeData(this.element, this.constructor.DATA_KEY); + $(this.element).off(this.constructor.EVENT_KEY); + $(this.element).closest('.modal').off('hide.bs.modal', this._hideModalHandler); + + if (this.tip) { + $(this.tip).remove(); + } + + this._isEnabled = null; + this._timeout = null; + this._hoverState = null; + this._activeTrigger = null; + + if (this._popper) { + this._popper.destroy(); + } + + this._popper = null; + this.element = null; + this.config = null; + this.tip = null; + }; + + _proto.show = function show() { + var _this = this; + + if ($(this.element).css('display') === 'none') { + throw new Error('Please use show on visible elements'); + } + + var showEvent = $.Event(this.constructor.Event.SHOW); + + if (this.isWithContent() && this._isEnabled) { + $(this.element).trigger(showEvent); + var shadowRoot = Util.findShadowRoot(this.element); + var isInTheDom = $.contains(shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement, this.element); + + if (showEvent.isDefaultPrevented() || !isInTheDom) { + return; + } + + var tip = this.getTipElement(); + var tipId = Util.getUID(this.constructor.NAME); + tip.setAttribute('id', tipId); + this.element.setAttribute('aria-describedby', tipId); + this.setContent(); + + if (this.config.animation) { + $(tip).addClass(ClassName$6.FADE); + } + + var placement = typeof this.config.placement === 'function' ? this.config.placement.call(this, tip, this.element) : this.config.placement; + + var attachment = this._getAttachment(placement); + + this.addAttachmentClass(attachment); + + var container = this._getContainer(); + + $(tip).data(this.constructor.DATA_KEY, this); + + if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) { + $(tip).appendTo(container); + } + + $(this.element).trigger(this.constructor.Event.INSERTED); + this._popper = new Popper(this.element, tip, this._getPopperConfig(attachment)); + $(tip).addClass(ClassName$6.SHOW); // If this is a touch-enabled device we add extra + // empty mouseover listeners to the body's immediate children; + // only needed because of broken event delegation on iOS + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + + if ('ontouchstart' in document.documentElement) { + $(document.body).children().on('mouseover', null, $.noop); + } + + var complete = function complete() { + if (_this.config.animation) { + _this._fixTransition(); + } + + var prevHoverState = _this._hoverState; + _this._hoverState = null; + $(_this.element).trigger(_this.constructor.Event.SHOWN); + + if (prevHoverState === HoverState.OUT) { + _this._leave(null, _this); + } + }; + + if ($(this.tip).hasClass(ClassName$6.FADE)) { + var transitionDuration = Util.getTransitionDurationFromElement(this.tip); + $(this.tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration); + } else { + complete(); + } + } + }; + + _proto.hide = function hide(callback) { + var _this2 = this; + + var tip = this.getTipElement(); + var hideEvent = $.Event(this.constructor.Event.HIDE); + + var complete = function complete() { + if (_this2._hoverState !== HoverState.SHOW && tip.parentNode) { + tip.parentNode.removeChild(tip); + } + + _this2._cleanTipClass(); + + _this2.element.removeAttribute('aria-describedby'); + + $(_this2.element).trigger(_this2.constructor.Event.HIDDEN); + + if (_this2._popper !== null) { + _this2._popper.destroy(); + } + + if (callback) { + callback(); + } + }; + + $(this.element).trigger(hideEvent); + + if (hideEvent.isDefaultPrevented()) { + return; + } + + $(tip).removeClass(ClassName$6.SHOW); // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + + if ('ontouchstart' in document.documentElement) { + $(document.body).children().off('mouseover', null, $.noop); + } + + this._activeTrigger[Trigger.CLICK] = false; + this._activeTrigger[Trigger.FOCUS] = false; + this._activeTrigger[Trigger.HOVER] = false; + + if ($(this.tip).hasClass(ClassName$6.FADE)) { + var transitionDuration = Util.getTransitionDurationFromElement(tip); + $(tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration); + } else { + complete(); + } + + this._hoverState = ''; + }; + + _proto.update = function update() { + if (this._popper !== null) { + this._popper.scheduleUpdate(); + } + } // Protected + ; + + _proto.isWithContent = function isWithContent() { + return Boolean(this.getTitle()); + }; + + _proto.addAttachmentClass = function addAttachmentClass(attachment) { + $(this.getTipElement()).addClass(CLASS_PREFIX + "-" + attachment); + }; + + _proto.getTipElement = function getTipElement() { + this.tip = this.tip || $(this.config.template)[0]; + return this.tip; + }; + + _proto.setContent = function setContent() { + var tip = this.getTipElement(); + this.setElementContent($(tip.querySelectorAll(Selector$6.TOOLTIP_INNER)), this.getTitle()); + $(tip).removeClass(ClassName$6.FADE + " " + ClassName$6.SHOW); + }; + + _proto.setElementContent = function setElementContent($element, content) { + if (typeof content === 'object' && (content.nodeType || content.jquery)) { + // Content is a DOM node or a jQuery + if (this.config.html) { + if (!$(content).parent().is($element)) { + $element.empty().append(content); + } + } else { + $element.text($(content).text()); + } + + return; + } + + if (this.config.html) { + if (this.config.sanitize) { + content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn); + } + + $element.html(content); + } else { + $element.text(content); + } + }; + + _proto.getTitle = function getTitle() { + var title = this.element.getAttribute('data-original-title'); + + if (!title) { + title = typeof this.config.title === 'function' ? this.config.title.call(this.element) : this.config.title; + } + + return title; + } // Private + ; + + _proto._getPopperConfig = function _getPopperConfig(attachment) { + var _this3 = this; + + var defaultBsConfig = { + placement: attachment, + modifiers: { + offset: this._getOffset(), + flip: { + behavior: this.config.fallbackPlacement + }, + arrow: { + element: Selector$6.ARROW + }, + preventOverflow: { + boundariesElement: this.config.boundary + } + }, + onCreate: function onCreate(data) { + if (data.originalPlacement !== data.placement) { + _this3._handlePopperPlacementChange(data); + } + }, + onUpdate: function onUpdate(data) { + return _this3._handlePopperPlacementChange(data); + } + }; + return _objectSpread2({}, defaultBsConfig, {}, this.config.popperConfig); + }; + + _proto._getOffset = function _getOffset() { + var _this4 = this; + + var offset = {}; + + if (typeof this.config.offset === 'function') { + offset.fn = function (data) { + data.offsets = _objectSpread2({}, data.offsets, {}, _this4.config.offset(data.offsets, _this4.element) || {}); + return data; + }; + } else { + offset.offset = this.config.offset; + } + + return offset; + }; + + _proto._getContainer = function _getContainer() { + if (this.config.container === false) { + return document.body; + } + + if (Util.isElement(this.config.container)) { + return $(this.config.container); + } + + return $(document).find(this.config.container); + }; + + _proto._getAttachment = function _getAttachment(placement) { + return AttachmentMap$1[placement.toUpperCase()]; + }; + + _proto._setListeners = function _setListeners() { + var _this5 = this; + + var triggers = this.config.trigger.split(' '); + triggers.forEach(function (trigger) { + if (trigger === 'click') { + $(_this5.element).on(_this5.constructor.Event.CLICK, _this5.config.selector, function (event) { + return _this5.toggle(event); + }); + } else if (trigger !== Trigger.MANUAL) { + var eventIn = trigger === Trigger.HOVER ? _this5.constructor.Event.MOUSEENTER : _this5.constructor.Event.FOCUSIN; + var eventOut = trigger === Trigger.HOVER ? _this5.constructor.Event.MOUSELEAVE : _this5.constructor.Event.FOCUSOUT; + $(_this5.element).on(eventIn, _this5.config.selector, function (event) { + return _this5._enter(event); + }).on(eventOut, _this5.config.selector, function (event) { + return _this5._leave(event); + }); + } + }); + + this._hideModalHandler = function () { + if (_this5.element) { + _this5.hide(); + } + }; + + $(this.element).closest('.modal').on('hide.bs.modal', this._hideModalHandler); + + if (this.config.selector) { + this.config = _objectSpread2({}, this.config, { + trigger: 'manual', + selector: '' + }); + } else { + this._fixTitle(); + } + }; + + _proto._fixTitle = function _fixTitle() { + var titleType = typeof this.element.getAttribute('data-original-title'); + + if (this.element.getAttribute('title') || titleType !== 'string') { + this.element.setAttribute('data-original-title', this.element.getAttribute('title') || ''); + this.element.setAttribute('title', ''); + } + }; + + _proto._enter = function _enter(event, context) { + var dataKey = this.constructor.DATA_KEY; + context = context || $(event.currentTarget).data(dataKey); + + if (!context) { + context = new this.constructor(event.currentTarget, this._getDelegateConfig()); + $(event.currentTarget).data(dataKey, context); + } + + if (event) { + context._activeTrigger[event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER] = true; + } + + if ($(context.getTipElement()).hasClass(ClassName$6.SHOW) || context._hoverState === HoverState.SHOW) { + context._hoverState = HoverState.SHOW; + return; + } + + clearTimeout(context._timeout); + context._hoverState = HoverState.SHOW; + + if (!context.config.delay || !context.config.delay.show) { + context.show(); + return; + } + + context._timeout = setTimeout(function () { + if (context._hoverState === HoverState.SHOW) { + context.show(); + } + }, context.config.delay.show); + }; + + _proto._leave = function _leave(event, context) { + var dataKey = this.constructor.DATA_KEY; + context = context || $(event.currentTarget).data(dataKey); + + if (!context) { + context = new this.constructor(event.currentTarget, this._getDelegateConfig()); + $(event.currentTarget).data(dataKey, context); + } + + if (event) { + context._activeTrigger[event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER] = false; + } + + if (context._isWithActiveTrigger()) { + return; + } + + clearTimeout(context._timeout); + context._hoverState = HoverState.OUT; + + if (!context.config.delay || !context.config.delay.hide) { + context.hide(); + return; + } + + context._timeout = setTimeout(function () { + if (context._hoverState === HoverState.OUT) { + context.hide(); + } + }, context.config.delay.hide); + }; + + _proto._isWithActiveTrigger = function _isWithActiveTrigger() { + for (var trigger in this._activeTrigger) { + if (this._activeTrigger[trigger]) { + return true; + } + } + + return false; + }; + + _proto._getConfig = function _getConfig(config) { + var dataAttributes = $(this.element).data(); + Object.keys(dataAttributes).forEach(function (dataAttr) { + if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) { + delete dataAttributes[dataAttr]; + } + }); + config = _objectSpread2({}, this.constructor.Default, {}, dataAttributes, {}, typeof config === 'object' && config ? config : {}); + + if (typeof config.delay === 'number') { + config.delay = { + show: config.delay, + hide: config.delay + }; + } + + if (typeof config.title === 'number') { + config.title = config.title.toString(); + } + + if (typeof config.content === 'number') { + config.content = config.content.toString(); + } + + Util.typeCheckConfig(NAME$6, config, this.constructor.DefaultType); + + if (config.sanitize) { + config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn); + } + + return config; + }; + + _proto._getDelegateConfig = function _getDelegateConfig() { + var config = {}; + + if (this.config) { + for (var key in this.config) { + if (this.constructor.Default[key] !== this.config[key]) { + config[key] = this.config[key]; + } + } + } + + return config; + }; + + _proto._cleanTipClass = function _cleanTipClass() { + var $tip = $(this.getTipElement()); + var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX); + + if (tabClass !== null && tabClass.length) { + $tip.removeClass(tabClass.join('')); + } + }; + + _proto._handlePopperPlacementChange = function _handlePopperPlacementChange(popperData) { + var popperInstance = popperData.instance; + this.tip = popperInstance.popper; + + this._cleanTipClass(); + + this.addAttachmentClass(this._getAttachment(popperData.placement)); + }; + + _proto._fixTransition = function _fixTransition() { + var tip = this.getTipElement(); + var initConfigAnimation = this.config.animation; + + if (tip.getAttribute('x-placement') !== null) { + return; + } + + $(tip).removeClass(ClassName$6.FADE); + this.config.animation = false; + this.hide(); + this.show(); + this.config.animation = initConfigAnimation; + } // Static + ; + + Tooltip._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $(this).data(DATA_KEY$6); + + var _config = typeof config === 'object' && config; + + if (!data && /dispose|hide/.test(config)) { + return; + } + + if (!data) { + data = new Tooltip(this, _config); + $(this).data(DATA_KEY$6, data); + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError("No method named \"" + config + "\""); + } + + data[config](); + } + }); + }; + + _createClass(Tooltip, null, [{ + key: "VERSION", + get: function get() { + return VERSION$6; + } + }, { + key: "Default", + get: function get() { + return Default$4; + } + }, { + key: "NAME", + get: function get() { + return NAME$6; + } + }, { + key: "DATA_KEY", + get: function get() { + return DATA_KEY$6; + } + }, { + key: "Event", + get: function get() { + return Event$6; + } + }, { + key: "EVENT_KEY", + get: function get() { + return EVENT_KEY$6; + } + }, { + key: "DefaultType", + get: function get() { + return DefaultType$4; + } + }]); + + return Tooltip; + }(); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + + $.fn[NAME$6] = Tooltip._jQueryInterface; + $.fn[NAME$6].Constructor = Tooltip; + + $.fn[NAME$6].noConflict = function () { + $.fn[NAME$6] = JQUERY_NO_CONFLICT$6; + return Tooltip._jQueryInterface; + }; + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$7 = 'popover'; + var VERSION$7 = '4.4.1'; + var DATA_KEY$7 = 'bs.popover'; + var EVENT_KEY$7 = "." + DATA_KEY$7; + var JQUERY_NO_CONFLICT$7 = $.fn[NAME$7]; + var CLASS_PREFIX$1 = 'bs-popover'; + var BSCLS_PREFIX_REGEX$1 = new RegExp("(^|\\s)" + CLASS_PREFIX$1 + "\\S+", 'g'); + + var Default$5 = _objectSpread2({}, Tooltip.Default, { + placement: 'right', + trigger: 'click', + content: '', + template: '' + }); + + var DefaultType$5 = _objectSpread2({}, Tooltip.DefaultType, { + content: '(string|element|function)' + }); + + var ClassName$7 = { + FADE: 'fade', + SHOW: 'show' + }; + var Selector$7 = { + TITLE: '.popover-header', + CONTENT: '.popover-body' + }; + var Event$7 = { + HIDE: "hide" + EVENT_KEY$7, + HIDDEN: "hidden" + EVENT_KEY$7, + SHOW: "show" + EVENT_KEY$7, + SHOWN: "shown" + EVENT_KEY$7, + INSERTED: "inserted" + EVENT_KEY$7, + CLICK: "click" + EVENT_KEY$7, + FOCUSIN: "focusin" + EVENT_KEY$7, + FOCUSOUT: "focusout" + EVENT_KEY$7, + MOUSEENTER: "mouseenter" + EVENT_KEY$7, + MOUSELEAVE: "mouseleave" + EVENT_KEY$7 + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Popover = + /*#__PURE__*/ + function (_Tooltip) { + _inheritsLoose(Popover, _Tooltip); + + function Popover() { + return _Tooltip.apply(this, arguments) || this; + } + + var _proto = Popover.prototype; + + // Overrides + _proto.isWithContent = function isWithContent() { + return this.getTitle() || this._getContent(); + }; + + _proto.addAttachmentClass = function addAttachmentClass(attachment) { + $(this.getTipElement()).addClass(CLASS_PREFIX$1 + "-" + attachment); + }; + + _proto.getTipElement = function getTipElement() { + this.tip = this.tip || $(this.config.template)[0]; + return this.tip; + }; + + _proto.setContent = function setContent() { + var $tip = $(this.getTipElement()); // We use append for html objects to maintain js events + + this.setElementContent($tip.find(Selector$7.TITLE), this.getTitle()); + + var content = this._getContent(); + + if (typeof content === 'function') { + content = content.call(this.element); + } + + this.setElementContent($tip.find(Selector$7.CONTENT), content); + $tip.removeClass(ClassName$7.FADE + " " + ClassName$7.SHOW); + } // Private + ; + + _proto._getContent = function _getContent() { + return this.element.getAttribute('data-content') || this.config.content; + }; + + _proto._cleanTipClass = function _cleanTipClass() { + var $tip = $(this.getTipElement()); + var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX$1); + + if (tabClass !== null && tabClass.length > 0) { + $tip.removeClass(tabClass.join('')); + } + } // Static + ; + + Popover._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $(this).data(DATA_KEY$7); + + var _config = typeof config === 'object' ? config : null; + + if (!data && /dispose|hide/.test(config)) { + return; + } + + if (!data) { + data = new Popover(this, _config); + $(this).data(DATA_KEY$7, data); + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError("No method named \"" + config + "\""); + } + + data[config](); + } + }); + }; + + _createClass(Popover, null, [{ + key: "VERSION", + // Getters + get: function get() { + return VERSION$7; + } + }, { + key: "Default", + get: function get() { + return Default$5; + } + }, { + key: "NAME", + get: function get() { + return NAME$7; + } + }, { + key: "DATA_KEY", + get: function get() { + return DATA_KEY$7; + } + }, { + key: "Event", + get: function get() { + return Event$7; + } + }, { + key: "EVENT_KEY", + get: function get() { + return EVENT_KEY$7; + } + }, { + key: "DefaultType", + get: function get() { + return DefaultType$5; + } + }]); + + return Popover; + }(Tooltip); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + + $.fn[NAME$7] = Popover._jQueryInterface; + $.fn[NAME$7].Constructor = Popover; + + $.fn[NAME$7].noConflict = function () { + $.fn[NAME$7] = JQUERY_NO_CONFLICT$7; + return Popover._jQueryInterface; + }; + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$8 = 'scrollspy'; + var VERSION$8 = '4.4.1'; + var DATA_KEY$8 = 'bs.scrollspy'; + var EVENT_KEY$8 = "." + DATA_KEY$8; + var DATA_API_KEY$6 = '.data-api'; + var JQUERY_NO_CONFLICT$8 = $.fn[NAME$8]; + var Default$6 = { + offset: 10, + method: 'auto', + target: '' + }; + var DefaultType$6 = { + offset: 'number', + method: 'string', + target: '(string|element)' + }; + var Event$8 = { + ACTIVATE: "activate" + EVENT_KEY$8, + SCROLL: "scroll" + EVENT_KEY$8, + LOAD_DATA_API: "load" + EVENT_KEY$8 + DATA_API_KEY$6 + }; + var ClassName$8 = { + DROPDOWN_ITEM: 'dropdown-item', + DROPDOWN_MENU: 'dropdown-menu', + ACTIVE: 'active' + }; + var Selector$8 = { + DATA_SPY: '[data-spy="scroll"]', + ACTIVE: '.active', + NAV_LIST_GROUP: '.nav, .list-group', + NAV_LINKS: '.nav-link', + NAV_ITEMS: '.nav-item', + LIST_ITEMS: '.list-group-item', + DROPDOWN: '.dropdown', + DROPDOWN_ITEMS: '.dropdown-item', + DROPDOWN_TOGGLE: '.dropdown-toggle' + }; + var OffsetMethod = { + OFFSET: 'offset', + POSITION: 'position' + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var ScrollSpy = + /*#__PURE__*/ + function () { + function ScrollSpy(element, config) { + var _this = this; + + this._element = element; + this._scrollElement = element.tagName === 'BODY' ? window : element; + this._config = this._getConfig(config); + this._selector = this._config.target + " " + Selector$8.NAV_LINKS + "," + (this._config.target + " " + Selector$8.LIST_ITEMS + ",") + (this._config.target + " " + Selector$8.DROPDOWN_ITEMS); + this._offsets = []; + this._targets = []; + this._activeTarget = null; + this._scrollHeight = 0; + $(this._scrollElement).on(Event$8.SCROLL, function (event) { + return _this._process(event); + }); + this.refresh(); + + this._process(); + } // Getters + + + var _proto = ScrollSpy.prototype; + + // Public + _proto.refresh = function refresh() { + var _this2 = this; + + var autoMethod = this._scrollElement === this._scrollElement.window ? OffsetMethod.OFFSET : OffsetMethod.POSITION; + var offsetMethod = this._config.method === 'auto' ? autoMethod : this._config.method; + var offsetBase = offsetMethod === OffsetMethod.POSITION ? this._getScrollTop() : 0; + this._offsets = []; + this._targets = []; + this._scrollHeight = this._getScrollHeight(); + var targets = [].slice.call(document.querySelectorAll(this._selector)); + targets.map(function (element) { + var target; + var targetSelector = Util.getSelectorFromElement(element); + + if (targetSelector) { + target = document.querySelector(targetSelector); + } + + if (target) { + var targetBCR = target.getBoundingClientRect(); + + if (targetBCR.width || targetBCR.height) { + // TODO (fat): remove sketch reliance on jQuery position/offset + return [$(target)[offsetMethod]().top + offsetBase, targetSelector]; + } + } + + return null; + }).filter(function (item) { + return item; + }).sort(function (a, b) { + return a[0] - b[0]; + }).forEach(function (item) { + _this2._offsets.push(item[0]); + + _this2._targets.push(item[1]); + }); + }; + + _proto.dispose = function dispose() { + $.removeData(this._element, DATA_KEY$8); + $(this._scrollElement).off(EVENT_KEY$8); + this._element = null; + this._scrollElement = null; + this._config = null; + this._selector = null; + this._offsets = null; + this._targets = null; + this._activeTarget = null; + this._scrollHeight = null; + } // Private + ; + + _proto._getConfig = function _getConfig(config) { + config = _objectSpread2({}, Default$6, {}, typeof config === 'object' && config ? config : {}); + + if (typeof config.target !== 'string') { + var id = $(config.target).attr('id'); + + if (!id) { + id = Util.getUID(NAME$8); + $(config.target).attr('id', id); + } + + config.target = "#" + id; + } + + Util.typeCheckConfig(NAME$8, config, DefaultType$6); + return config; + }; + + _proto._getScrollTop = function _getScrollTop() { + return this._scrollElement === window ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop; + }; + + _proto._getScrollHeight = function _getScrollHeight() { + return this._scrollElement.scrollHeight || Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); + }; + + _proto._getOffsetHeight = function _getOffsetHeight() { + return this._scrollElement === window ? window.innerHeight : this._scrollElement.getBoundingClientRect().height; + }; + + _proto._process = function _process() { + var scrollTop = this._getScrollTop() + this._config.offset; + + var scrollHeight = this._getScrollHeight(); + + var maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight(); + + if (this._scrollHeight !== scrollHeight) { + this.refresh(); + } + + if (scrollTop >= maxScroll) { + var target = this._targets[this._targets.length - 1]; + + if (this._activeTarget !== target) { + this._activate(target); + } + + return; + } + + if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) { + this._activeTarget = null; + + this._clear(); + + return; + } + + var offsetLength = this._offsets.length; + + for (var i = offsetLength; i--;) { + var isActiveTarget = this._activeTarget !== this._targets[i] && scrollTop >= this._offsets[i] && (typeof this._offsets[i + 1] === 'undefined' || scrollTop < this._offsets[i + 1]); + + if (isActiveTarget) { + this._activate(this._targets[i]); + } + } + }; + + _proto._activate = function _activate(target) { + this._activeTarget = target; + + this._clear(); + + var queries = this._selector.split(',').map(function (selector) { + return selector + "[data-target=\"" + target + "\"]," + selector + "[href=\"" + target + "\"]"; + }); + + var $link = $([].slice.call(document.querySelectorAll(queries.join(',')))); + + if ($link.hasClass(ClassName$8.DROPDOWN_ITEM)) { + $link.closest(Selector$8.DROPDOWN).find(Selector$8.DROPDOWN_TOGGLE).addClass(ClassName$8.ACTIVE); + $link.addClass(ClassName$8.ACTIVE); + } else { + // Set triggered link as active + $link.addClass(ClassName$8.ACTIVE); // Set triggered links parents as active + // With both
+
+
+ + + + + diff --git a/flask__webservers/users/main_with_db.py b/flask__webservers/users/main_with_db.py new file mode 100644 index 000000000..7f8fb28af --- /dev/null +++ b/flask__webservers/users/main_with_db.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://www.geeksforgeeks.org/how-to-add-authentication-to-your-app-with-flask-login/ + + +import os + +from typing import Optional + +# pip install flask==3.0.0 +import flask + +# pip install flask-login==0.6.2 +import flask_login + +# pip install flask-sqlalchemy==3.1.1 +# TODO: Bug fix for flask-login==0.6.2 - flask-sqlalchemy installed/updated Werkzeug +# pip install Werkzeug==2.3.7 +from flask_sqlalchemy import SQLAlchemy + + +# TODO: Убрать дублирование кода в шаблонах +# TODO: Не хранить пароль в чистом виде +# TODO: Сделать вариант примера с peewee + + +app = flask.Flask(__name__) +app.secret_key = "super secret string" # Change this! +app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DATABASE_URI", "sqlite:///db.sqlite") + +db = SQLAlchemy() + +login_manager = flask_login.LoginManager() +login_manager.init_app(app) + + +class User(db.Model, flask_login.UserMixin): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(250), unique=True, nullable=False) + password = db.Column(db.String(250), nullable=False) + + @classmethod + def get_by(cls, username: str) -> Optional["User"]: + return cls.query.filter_by( + username=username + ).first() + + +db.init_app(app) + +with app.app_context(): + db.create_all() + + +@login_manager.user_loader +def loader_user(user_id: int): + return db.session.get(User, user_id) + + +@app.route("/") +def index(): + return flask.render_template_string(""" + + + + + + + Home + + + + + {% if current_user.is_authenticated %} +

You are logged as {{ current_user.username }}

+ {% else %} +

You are not logged in

+ {% endif %} + + + """) + + +@app.route('/register', methods=["GET", "POST"]) +def register(): + if flask.request.method == "POST": + username = flask.request.form.get("username") + if User.get_by(username): + return "There is already a user with this nickname" + + password = flask.request.form.get("password") + + user = User( + username=username, + password=password, + ) + db.session.add(user) + db.session.commit() + + return flask.redirect(flask.url_for("login")) + + return flask.render_template_string(""" + + + + + + + Sign Up + + + + +

Create an account

+
+ + + + + +
+ + + """) + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + if flask.request.method == "POST": + username = flask.request.form.get("username") + password = flask.request.form.get("password") + + user = User.get_by(username) + if user and user.password == password: + flask_login.login_user(user) + return flask.redirect(flask.url_for("index")) + + return "Bad login" + + return flask.render_template_string(""" + + + + + + + Login + + + + +

Login to your account

+
+ + + + + +
+ + + """) + + +@app.route("/logout") +def logout(): + flask_login.logout_user() + return flask.redirect(flask.url_for("index")) + + +@app.route("/protected") +@flask_login.login_required +def protected() -> str: + return f"Logged in as: {flask_login.current_user.username}" + + +if __name__ == "__main__": + app.debug = True + app.run(port=10101) diff --git a/flask__webservers/users/main_without_db.py b/flask__webservers/users/main_without_db.py new file mode 100644 index 000000000..927caa1fd --- /dev/null +++ b/flask__webservers/users/main_without_db.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os + +# pip install flask==3.0.0 +import flask + +# pip install flask-login==0.6.2 +import flask_login + + +LOGIN = os.environ.get("ADMIN_LOGIN", "foo@bar.example") +PASSWORD = os.environ.get("ADMIN_PASSWORD", "secret") + +# Our mock database. +USERS = { + LOGIN: { + "password": PASSWORD, + }, +} + + +app = flask.Flask(__name__) +app.secret_key = "super secret string" # Change this! + +login_manager = flask_login.LoginManager() +login_manager.init_app(app) + + +class User(flask_login.UserMixin): + @classmethod + def create(cls, login: str) -> "User": + user = User() + user.id = login + return user + + +# Если авторизован +@login_manager.user_loader +def user_loader(email): + if email not in USERS: + return + + return User.create(email) + + +# Если не авторизован +@login_manager.request_loader +def request_loader(request): + email = request.form.get("email") + if email not in USERS: + return + + return User.create(email) + + +@app.route("/") +def index(): + return flask.render_template_string( + """ + + + + + + + Home + + + + + {% if current_user.is_authenticated %} +

You are logged as {{ current_user.id }}

+ Protected + {% else %} +

You are not logged in

+ {% endif %} + + + """ + ) + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + if flask.request.method == "GET": + return """ +
+ + + +
+ """ + + email = flask.request.form["email"] + if email in USERS and flask.request.form["password"] == USERS[email]["password"]: + user = User.create(email) + flask_login.login_user(user) + return flask.redirect(flask.url_for("index")) + + return "Bad login" + + +@app.route("/protected") +@flask_login.login_required +def protected() -> str: + return f"Logged in as: {flask_login.current_user.id}" + + +@app.route("/logout") +@flask_login.login_required +def logout(): + flask_login.logout_user() + return flask.redirect(flask.url_for("index")) + + +@login_manager.unauthorized_handler +def unauthorized_handler(): + return "Unauthorized", 401 + + +if __name__ == "__main__": + app.debug = True + app.run(port=10101) diff --git a/flask__websocket/ajax_vs_websocket/_static/socket.io.js b/flask__websocket/ajax_vs_websocket/_static/socket.io.js deleted file mode 100644 index d3e395011..000000000 --- a/flask__websocket/ajax_vs_websocket/_static/socket.io.js +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Socket.IO v2.2.0 - * (c) 2014-2018 Guillermo Rauch - * Released under the MIT License. - */ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.io=e():t.io=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function r(t,e){"object"===("undefined"==typeof t?"undefined":o(t))&&(e=t,t=void 0),e=e||{};var n,r=i(t),s=r.source,u=r.id,h=r.path,f=p[u]&&h in p[u].nsps,l=e.forceNew||e["force new connection"]||!1===e.multiplex||f;return l?(c("ignoring socket cache for %s",s),n=a(s,e)):(p[u]||(c("new io instance for %s",s),p[u]=a(s,e)),n=p[u]),r.query&&!e.query&&(e.query=r.query),n.socket(r.path,e)}var o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=n(1),s=n(7),a=n(12),c=n(3)("socket.io-client");t.exports=e=r;var p=e.managers={};e.protocol=s.protocol,e.connect=r,e.Manager=n(12),e.Socket=n(36)},function(t,e,n){"use strict";function r(t,e){var n=t;e=e||"undefined"!=typeof location&&location,null==t&&(t=e.protocol+"//"+e.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?e.protocol+t:e.host+t),/^(https?|wss?):\/\//.test(t)||(i("protocol-less url %s",t),t="undefined"!=typeof e?e.protocol+"//"+t:"https://"+t),i("parse %s",t),n=o(t)),n.port||(/^(http|ws)$/.test(n.protocol)?n.port="80":/^(http|ws)s$/.test(n.protocol)&&(n.port="443")),n.path=n.path||"/";var r=n.host.indexOf(":")!==-1,s=r?"["+n.host+"]":n.host;return n.id=n.protocol+"://"+s+":"+n.port,n.href=n.protocol+"://"+s+(e&&e.port===n.port?"":":"+n.port),n}var o=n(2),i=n(3)("socket.io-client:url");t.exports=r},function(t,e){var n=/^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,r=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];t.exports=function(t){var e=t,o=t.indexOf("["),i=t.indexOf("]");o!=-1&&i!=-1&&(t=t.substring(0,o)+t.substring(o,i).replace(/:/g,";")+t.substring(i,t.length));for(var s=n.exec(t||""),a={},c=14;c--;)a[r[c]]=s[c]||"";return o!=-1&&i!=-1&&(a.source=e,a.host=a.host.substring(1,a.host.length-1).replace(/;/g,":"),a.authority=a.authority.replace("[","").replace("]","").replace(/;/g,":"),a.ipv6uri=!0),a}},function(t,e,n){(function(r){function o(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type)||("undefined"==typeof navigator||!navigator.userAgent||!navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(t){var n=this.useColors;if(t[0]=(n?"%c":"")+this.namespace+(n?" %c":" ")+t[0]+(n?"%c ":" ")+"+"+e.humanize(this.diff),n){var r="color: "+this.color;t.splice(1,0,r,"color: inherit");var o=0,i=0;t[0].replace(/%[a-zA-Z%]/g,function(t){"%%"!==t&&(o++,"%c"===t&&(i=o))}),t.splice(i,0,r)}}function s(){return"object"==typeof console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function a(t){try{null==t?e.storage.removeItem("debug"):e.storage.debug=t}catch(n){}}function c(){var t;try{t=e.storage.debug}catch(n){}return!t&&"undefined"!=typeof r&&"env"in r&&(t=r.env.DEBUG),t}function p(){try{return window.localStorage}catch(t){}}e=t.exports=n(5),e.log=s,e.formatArgs=i,e.save=a,e.load=c,e.useColors=o,e.storage="undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage?chrome.storage.local:p(),e.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],e.formatters.j=function(t){try{return JSON.stringify(t)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}},e.enable(c())}).call(e,n(4))},function(t,e){function n(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function o(t){if(u===setTimeout)return setTimeout(t,0);if((u===n||!u)&&setTimeout)return u=setTimeout,setTimeout(t,0);try{return u(t,0)}catch(e){try{return u.call(null,t,0)}catch(e){return u.call(this,t,0)}}}function i(t){if(h===clearTimeout)return clearTimeout(t);if((h===r||!h)&&clearTimeout)return h=clearTimeout,clearTimeout(t);try{return h(t)}catch(e){try{return h.call(null,t)}catch(e){return h.call(this,t)}}}function s(){y&&l&&(y=!1,l.length?d=l.concat(d):m=-1,d.length&&a())}function a(){if(!y){var t=o(s);y=!0;for(var e=d.length;e;){for(l=d,d=[];++m1)for(var n=1;n100)){var e=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(t);if(e){var n=parseFloat(e[1]),r=(e[2]||"ms").toLowerCase();switch(r){case"years":case"year":case"yrs":case"yr":case"y":return n*u;case"days":case"day":case"d":return n*p;case"hours":case"hour":case"hrs":case"hr":case"h":return n*c;case"minutes":case"minute":case"mins":case"min":case"m":return n*a;case"seconds":case"second":case"secs":case"sec":case"s":return n*s;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n;default:return}}}}function r(t){return t>=p?Math.round(t/p)+"d":t>=c?Math.round(t/c)+"h":t>=a?Math.round(t/a)+"m":t>=s?Math.round(t/s)+"s":t+"ms"}function o(t){return i(t,p,"day")||i(t,c,"hour")||i(t,a,"minute")||i(t,s,"second")||t+" ms"}function i(t,e,n){if(!(t0)return n(t);if("number"===i&&isNaN(t)===!1)return e["long"]?o(t):r(t);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(t))}},function(t,e,n){function r(){}function o(t){var n=""+t.type;if(e.BINARY_EVENT!==t.type&&e.BINARY_ACK!==t.type||(n+=t.attachments+"-"),t.nsp&&"/"!==t.nsp&&(n+=t.nsp+","),null!=t.id&&(n+=t.id),null!=t.data){var r=i(t.data);if(r===!1)return g;n+=r}return f("encoded %j as %s",t,n),n}function i(t){try{return JSON.stringify(t)}catch(e){return!1}}function s(t,e){function n(t){var n=d.deconstructPacket(t),r=o(n.packet),i=n.buffers;i.unshift(r),e(i)}d.removeBlobs(t,n)}function a(){this.reconstructor=null}function c(t){var n=0,r={type:Number(t.charAt(0))};if(null==e.types[r.type])return h("unknown packet type "+r.type);if(e.BINARY_EVENT===r.type||e.BINARY_ACK===r.type){for(var o="";"-"!==t.charAt(++n)&&(o+=t.charAt(n),n!=t.length););if(o!=Number(o)||"-"!==t.charAt(n))throw new Error("Illegal attachments");r.attachments=Number(o)}if("/"===t.charAt(n+1))for(r.nsp="";++n;){var i=t.charAt(n);if(","===i)break;if(r.nsp+=i,n===t.length)break}else r.nsp="/";var s=t.charAt(n+1);if(""!==s&&Number(s)==s){for(r.id="";++n;){var i=t.charAt(n);if(null==i||Number(i)!=i){--n;break}if(r.id+=t.charAt(n),n===t.length)break}r.id=Number(r.id)}if(t.charAt(++n)){var a=p(t.substr(n)),c=a!==!1&&(r.type===e.ERROR||y(a));if(!c)return h("invalid payload");r.data=a}return f("decoded %s as %j",t,r),r}function p(t){try{return JSON.parse(t)}catch(e){return!1}}function u(t){this.reconPack=t,this.buffers=[]}function h(t){return{type:e.ERROR,data:"parser error: "+t}}var f=n(3)("socket.io-parser"),l=n(8),d=n(9),y=n(10),m=n(11);e.protocol=4,e.types=["CONNECT","DISCONNECT","EVENT","ACK","ERROR","BINARY_EVENT","BINARY_ACK"],e.CONNECT=0,e.DISCONNECT=1,e.EVENT=2,e.ACK=3,e.ERROR=4,e.BINARY_EVENT=5,e.BINARY_ACK=6,e.Encoder=r,e.Decoder=a;var g=e.ERROR+'"encode error"';r.prototype.encode=function(t,n){if(f("encoding packet %j",t),e.BINARY_EVENT===t.type||e.BINARY_ACK===t.type)s(t,n);else{var r=o(t);n([r])}},l(a.prototype),a.prototype.add=function(t){var n;if("string"==typeof t)n=c(t),e.BINARY_EVENT===n.type||e.BINARY_ACK===n.type?(this.reconstructor=new u(n),0===this.reconstructor.reconPack.attachments&&this.emit("decoded",n)):this.emit("decoded",n);else{if(!m(t)&&!t.base64)throw new Error("Unknown type: "+t);if(!this.reconstructor)throw new Error("got binary data when not reconstructing a packet");n=this.reconstructor.takeBinaryData(t),n&&(this.reconstructor=null,this.emit("decoded",n))}},a.prototype.destroy=function(){this.reconstructor&&this.reconstructor.finishedReconstruction()},u.prototype.takeBinaryData=function(t){if(this.buffers.push(t),this.buffers.length===this.reconPack.attachments){var e=d.reconstructPacket(this.reconPack,this.buffers);return this.finishedReconstruction(),e}return null},u.prototype.finishedReconstruction=function(){this.reconPack=null,this.buffers=[]}},function(t,e,n){function r(t){if(t)return o(t)}function o(t){for(var e in r.prototype)t[e]=r.prototype[e];return t}t.exports=r,r.prototype.on=r.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},r.prototype.once=function(t,e){function n(){this.off(t,n),e.apply(this,arguments)}return n.fn=e,this.on(t,n),this},r.prototype.off=r.prototype.removeListener=r.prototype.removeAllListeners=r.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n=this._callbacks["$"+t];if(!n)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var r,o=0;o0&&!this.encoding){var t=this.packetBuffer.shift();this.packet(t)}},r.prototype.cleanup=function(){h("cleanup");for(var t=this.subs.length,e=0;e=this._reconnectionAttempts)h("reconnect failed"),this.backoff.reset(),this.emitAll("reconnect_failed"),this.reconnecting=!1;else{var e=this.backoff.duration();h("will wait %dms before reconnect attempt",e),this.reconnecting=!0;var n=setTimeout(function(){t.skipReconnect||(h("attempting reconnect"),t.emitAll("reconnect_attempt",t.backoff.attempts),t.emitAll("reconnecting",t.backoff.attempts),t.skipReconnect||t.open(function(e){e?(h("reconnect attempt error"),t.reconnecting=!1,t.reconnect(),t.emitAll("reconnect_error",e.data)):(h("reconnect success"),t.onreconnect())}))},e);this.subs.push({destroy:function(){clearTimeout(n)}})}},r.prototype.onreconnect=function(){var t=this.backoff.attempts;this.reconnecting=!1,this.backoff.reset(),this.updateSocketIds(),this.emitAll("reconnect",t)}},function(t,e,n){t.exports=n(14),t.exports.parser=n(21)},function(t,e,n){function r(t,e){return this instanceof r?(e=e||{},t&&"object"==typeof t&&(e=t,t=null),t?(t=u(t),e.hostname=t.host,e.secure="https"===t.protocol||"wss"===t.protocol,e.port=t.port,t.query&&(e.query=t.query)):e.host&&(e.hostname=u(e.host).host),this.secure=null!=e.secure?e.secure:"undefined"!=typeof location&&"https:"===location.protocol,e.hostname&&!e.port&&(e.port=this.secure?"443":"80"),this.agent=e.agent||!1,this.hostname=e.hostname||("undefined"!=typeof location?location.hostname:"localhost"),this.port=e.port||("undefined"!=typeof location&&location.port?location.port:this.secure?443:80),this.query=e.query||{},"string"==typeof this.query&&(this.query=h.decode(this.query)),this.upgrade=!1!==e.upgrade,this.path=(e.path||"/engine.io").replace(/\/$/,"")+"/",this.forceJSONP=!!e.forceJSONP,this.jsonp=!1!==e.jsonp,this.forceBase64=!!e.forceBase64,this.enablesXDR=!!e.enablesXDR,this.timestampParam=e.timestampParam||"t",this.timestampRequests=e.timestampRequests,this.transports=e.transports||["polling","websocket"],this.transportOptions=e.transportOptions||{},this.readyState="",this.writeBuffer=[],this.prevBufferLen=0,this.policyPort=e.policyPort||843,this.rememberUpgrade=e.rememberUpgrade||!1,this.binaryType=null,this.onlyBinaryUpgrades=e.onlyBinaryUpgrades,this.perMessageDeflate=!1!==e.perMessageDeflate&&(e.perMessageDeflate||{}),!0===this.perMessageDeflate&&(this.perMessageDeflate={}),this.perMessageDeflate&&null==this.perMessageDeflate.threshold&&(this.perMessageDeflate.threshold=1024),this.pfx=e.pfx||null,this.key=e.key||null,this.passphrase=e.passphrase||null,this.cert=e.cert||null,this.ca=e.ca||null,this.ciphers=e.ciphers||null,this.rejectUnauthorized=void 0===e.rejectUnauthorized||e.rejectUnauthorized,this.forceNode=!!e.forceNode,this.isReactNative="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),("undefined"==typeof self||this.isReactNative)&&(e.extraHeaders&&Object.keys(e.extraHeaders).length>0&&(this.extraHeaders=e.extraHeaders),e.localAddress&&(this.localAddress=e.localAddress)),this.id=null,this.upgrades=null,this.pingInterval=null,this.pingTimeout=null,this.pingIntervalTimer=null,this.pingTimeoutTimer=null,void this.open()):new r(t,e)}function o(t){var e={};for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}var i=n(15),s=n(8),a=n(3)("engine.io-client:socket"),c=n(35),p=n(21),u=n(2),h=n(29);t.exports=r,r.priorWebsocketSuccess=!1,s(r.prototype),r.protocol=p.protocol,r.Socket=r,r.Transport=n(20),r.transports=n(15),r.parser=n(21),r.prototype.createTransport=function(t){a('creating transport "%s"',t);var e=o(this.query);e.EIO=p.protocol,e.transport=t;var n=this.transportOptions[t]||{};this.id&&(e.sid=this.id);var r=new i[t]({query:e,socket:this,agent:n.agent||this.agent,hostname:n.hostname||this.hostname,port:n.port||this.port,secure:n.secure||this.secure,path:n.path||this.path,forceJSONP:n.forceJSONP||this.forceJSONP,jsonp:n.jsonp||this.jsonp,forceBase64:n.forceBase64||this.forceBase64,enablesXDR:n.enablesXDR||this.enablesXDR,timestampRequests:n.timestampRequests||this.timestampRequests,timestampParam:n.timestampParam||this.timestampParam,policyPort:n.policyPort||this.policyPort,pfx:n.pfx||this.pfx,key:n.key||this.key,passphrase:n.passphrase||this.passphrase,cert:n.cert||this.cert,ca:n.ca||this.ca,ciphers:n.ciphers||this.ciphers,rejectUnauthorized:n.rejectUnauthorized||this.rejectUnauthorized,perMessageDeflate:n.perMessageDeflate||this.perMessageDeflate,extraHeaders:n.extraHeaders||this.extraHeaders,forceNode:n.forceNode||this.forceNode,localAddress:n.localAddress||this.localAddress,requestTimeout:n.requestTimeout||this.requestTimeout,protocols:n.protocols||void 0,isReactNative:this.isReactNative});return r},r.prototype.open=function(){var t;if(this.rememberUpgrade&&r.priorWebsocketSuccess&&this.transports.indexOf("websocket")!==-1)t="websocket";else{if(0===this.transports.length){var e=this;return void setTimeout(function(){e.emit("error","No transports available")},0)}t=this.transports[0]}this.readyState="opening";try{t=this.createTransport(t)}catch(n){return this.transports.shift(),void this.open()}t.open(),this.setTransport(t)},r.prototype.setTransport=function(t){a("setting transport %s",t.name);var e=this;this.transport&&(a("clearing existing transport %s",this.transport.name),this.transport.removeAllListeners()),this.transport=t,t.on("drain",function(){e.onDrain()}).on("packet",function(t){e.onPacket(t)}).on("error",function(t){e.onError(t)}).on("close",function(){e.onClose("transport close")})},r.prototype.probe=function(t){function e(){if(f.onlyBinaryUpgrades){var e=!this.supportsBinary&&f.transport.supportsBinary;h=h||e}h||(a('probe transport "%s" opened',t),u.send([{type:"ping",data:"probe"}]),u.once("packet",function(e){if(!h)if("pong"===e.type&&"probe"===e.data){if(a('probe transport "%s" pong',t),f.upgrading=!0,f.emit("upgrading",u),!u)return;r.priorWebsocketSuccess="websocket"===u.name,a('pausing current transport "%s"',f.transport.name),f.transport.pause(function(){h||"closed"!==f.readyState&&(a("changing transport and sending upgrade packet"),p(),f.setTransport(u),u.send([{type:"upgrade"}]),f.emit("upgrade",u),u=null,f.upgrading=!1,f.flush())})}else{a('probe transport "%s" failed',t);var n=new Error("probe error");n.transport=u.name,f.emit("upgradeError",n)}}))}function n(){h||(h=!0,p(),u.close(),u=null)}function o(e){var r=new Error("probe error: "+e);r.transport=u.name,n(),a('probe transport "%s" failed because of error: %s',t,e),f.emit("upgradeError",r)}function i(){o("transport closed")}function s(){o("socket closed")}function c(t){u&&t.name!==u.name&&(a('"%s" works - aborting "%s"',t.name,u.name),n())}function p(){u.removeListener("open",e),u.removeListener("error",o),u.removeListener("close",i),f.removeListener("close",s),f.removeListener("upgrading",c)}a('probing transport "%s"',t);var u=this.createTransport(t,{probe:1}),h=!1,f=this;r.priorWebsocketSuccess=!1,u.once("open",e),u.once("error",o),u.once("close",i),this.once("close",s),this.once("upgrading",c),u.open()},r.prototype.onOpen=function(){if(a("socket open"),this.readyState="open",r.priorWebsocketSuccess="websocket"===this.transport.name,this.emit("open"),this.flush(),"open"===this.readyState&&this.upgrade&&this.transport.pause){a("starting upgrade probes");for(var t=0,e=this.upgrades.length;t1?{type:b[o],data:t.substring(1)}:{type:b[o]}:w}var i=new Uint8Array(t),o=i[0],s=f(t,1);return k&&"blob"===n&&(s=new k([s])),{type:b[o],data:s}},e.decodeBase64Packet=function(t,e){var n=b[t.charAt(0)];if(!p)return{type:n,data:{base64:!0,data:t.substr(1)}};var r=p.decode(t.substr(1));return"blob"===e&&k&&(r=new k([r])),{type:n,data:r}},e.encodePayload=function(t,n,r){function o(t){return t.length+":"+t}function i(t,r){e.encodePacket(t,!!s&&n,!1,function(t){r(null,o(t))})}"function"==typeof n&&(r=n,n=null);var s=h(t);return n&&s?k&&!g?e.encodePayloadAsBlob(t,r):e.encodePayloadAsArrayBuffer(t,r):t.length?void c(t,i,function(t,e){return r(e.join(""))}):r("0:")},e.decodePayload=function(t,n,r){if("string"!=typeof t)return e.decodePayloadAsBinary(t,n,r);"function"==typeof n&&(r=n,n=null);var o;if(""===t)return r(w,0,1);for(var i,s,a="",c=0,p=t.length;c0;){for(var s=new Uint8Array(o),a=0===s[0],c="",p=1;255!==s[p];p++){if(c.length>310)return r(w,0,1);c+=s[p]}o=f(o,2+c.length),c=parseInt(c);var u=f(o,0,c);if(a)try{u=String.fromCharCode.apply(null,new Uint8Array(u))}catch(h){var l=new Uint8Array(u);u="";for(var p=0;pr&&(n=r),e>=r||e>=n||0===r)return new ArrayBuffer(0);for(var o=new Uint8Array(t),i=new Uint8Array(n-e),s=e,a=0;s=55296&&e<=56319&&o65535&&(e-=65536,o+=d(e>>>10&1023|55296),e=56320|1023&e),o+=d(e);return o}function o(t,e){if(t>=55296&&t<=57343){if(e)throw Error("Lone surrogate U+"+t.toString(16).toUpperCase()+" is not a scalar value");return!1}return!0}function i(t,e){return d(t>>e&63|128)}function s(t,e){if(0==(4294967168&t))return d(t);var n="";return 0==(4294965248&t)?n=d(t>>6&31|192):0==(4294901760&t)?(o(t,e)||(t=65533),n=d(t>>12&15|224),n+=i(t,6)):0==(4292870144&t)&&(n=d(t>>18&7|240),n+=i(t,12),n+=i(t,6)),n+=d(63&t|128)}function a(t,e){e=e||{};for(var r,o=!1!==e.strict,i=n(t),a=i.length,c=-1,p="";++c=f)throw Error("Invalid byte index");var t=255&h[l];if(l++,128==(192&t))return 63&t;throw Error("Invalid continuation byte")}function p(t){var e,n,r,i,s;if(l>f)throw Error("Invalid byte index");if(l==f)return!1;if(e=255&h[l],l++,0==(128&e))return e;if(192==(224&e)){if(n=c(),s=(31&e)<<6|n,s>=128)return s;throw Error("Invalid continuation byte")}if(224==(240&e)){if(n=c(),r=c(),s=(15&e)<<12|n<<6|r,s>=2048)return o(s,t)?s:65533;throw Error("Invalid continuation byte")}if(240==(248&e)&&(n=c(),r=c(),i=c(),s=(7&e)<<18|n<<12|r<<6|i,s>=65536&&s<=1114111))return s;throw Error("Invalid UTF-8 detected")}function u(t,e){e=e||{};var o=!1!==e.strict;h=n(t),f=h.length,l=0;for(var i,s=[];(i=p(o))!==!1;)s.push(i);return r(s)}/*! https://mths.be/utf8js v2.1.2 by @mathias */ -var h,f,l,d=String.fromCharCode;t.exports={version:"2.1.2",encode:a,decode:u}},function(t,e){!function(){"use strict";for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n=new Uint8Array(256),r=0;r>2],i+=t[(3&r[n])<<4|r[n+1]>>4],i+=t[(15&r[n+1])<<2|r[n+2]>>6],i+=t[63&r[n+2]];return o%3===2?i=i.substring(0,i.length-1)+"=":o%3===1&&(i=i.substring(0,i.length-2)+"=="),i},e.decode=function(t){var e,r,o,i,s,a=.75*t.length,c=t.length,p=0;"="===t[t.length-1]&&(a--,"="===t[t.length-2]&&a--);var u=new ArrayBuffer(a),h=new Uint8Array(u);for(e=0;e>4,h[p++]=(15&o)<<4|i>>2,h[p++]=(3&i)<<6|63&s;return u}}()},function(t,e){function n(t){return t.map(function(t){if(t.buffer instanceof ArrayBuffer){var e=t.buffer;if(t.byteLength!==e.byteLength){var n=new Uint8Array(t.byteLength);n.set(new Uint8Array(e,t.byteOffset,t.byteLength)),e=n.buffer}return e}return t})}function r(t,e){e=e||{};var r=new i;return n(t).forEach(function(t){r.append(t)}),e.type?r.getBlob(e.type):r.getBlob()}function o(t,e){return new Blob(n(t),e||{})}var i="undefined"!=typeof i?i:"undefined"!=typeof WebKitBlobBuilder?WebKitBlobBuilder:"undefined"!=typeof MSBlobBuilder?MSBlobBuilder:"undefined"!=typeof MozBlobBuilder&&MozBlobBuilder,s=function(){try{var t=new Blob(["hi"]);return 2===t.size}catch(e){return!1}}(),a=s&&function(){try{var t=new Blob([new Uint8Array([1,2])]);return 2===t.size}catch(e){return!1}}(),c=i&&i.prototype.append&&i.prototype.getBlob;"undefined"!=typeof Blob&&(r.prototype=Blob.prototype,o.prototype=Blob.prototype),t.exports=function(){return s?a?Blob:o:c?r:void 0}()},function(t,e){e.encode=function(t){var e="";for(var n in t)t.hasOwnProperty(n)&&(e.length&&(e+="&"),e+=encodeURIComponent(n)+"="+encodeURIComponent(t[n]));return e},e.decode=function(t){for(var e={},n=t.split("&"),r=0,o=n.length;r0);return e}function r(t){var e=0;for(u=0;u';i=document.createElement(e)}catch(t){i=document.createElement("iframe"),i.name=o.iframeId,i.src="javascript:0"}i.id=o.iframeId,o.form.appendChild(i),o.iframe=i}var o=this;if(!this.form){var i,s=document.createElement("form"),a=document.createElement("textarea"),c=this.iframeId="eio_iframe_"+this.index;s.className="socketio",s.style.position="absolute",s.style.top="-1000px",s.style.left="-1000px",s.target=c,s.method="POST",s.setAttribute("accept-charset","utf-8"),a.name="d",s.appendChild(a),document.body.appendChild(s),this.form=s,this.area=a}this.form.action=this.uri(),r(),t=t.replace(u,"\\\n"),this.area.value=t.replace(p,"\\n");try{this.form.submit()}catch(h){}this.iframe.attachEvent?this.iframe.onreadystatechange=function(){"complete"===o.iframe.readyState&&n()}:this.iframe.onload=n}}).call(e,function(){return this}())},function(t,e,n){function r(t){var e=t&&t.forceBase64;e&&(this.supportsBinary=!1),this.perMessageDeflate=t.perMessageDeflate,this.usingBrowserWebSocket=o&&!t.forceNode,this.protocols=t.protocols,this.usingBrowserWebSocket||(l=i),s.call(this,t)}var o,i,s=n(20),a=n(21),c=n(29),p=n(30),u=n(31),h=n(3)("engine.io-client:websocket");if("undefined"==typeof self)try{i=n(34)}catch(f){}else o=self.WebSocket||self.MozWebSocket;var l=o||i;t.exports=r,p(r,s),r.prototype.name="websocket",r.prototype.supportsBinary=!0,r.prototype.doOpen=function(){if(this.check()){var t=this.uri(),e=this.protocols,n={agent:this.agent,perMessageDeflate:this.perMessageDeflate};n.pfx=this.pfx,n.key=this.key,n.passphrase=this.passphrase,n.cert=this.cert,n.ca=this.ca,n.ciphers=this.ciphers,n.rejectUnauthorized=this.rejectUnauthorized,this.extraHeaders&&(n.headers=this.extraHeaders),this.localAddress&&(n.localAddress=this.localAddress);try{this.ws=this.usingBrowserWebSocket&&!this.isReactNative?e?new l(t,e):new l(t):new l(t,e,n)}catch(r){return this.emit("error",r)}void 0===this.ws.binaryType&&(this.supportsBinary=!1),this.ws.supports&&this.ws.supports.binary?(this.supportsBinary=!0,this.ws.binaryType="nodebuffer"):this.ws.binaryType="arraybuffer",this.addEventListeners()}},r.prototype.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.onOpen()},this.ws.onclose=function(){t.onClose()},this.ws.onmessage=function(e){t.onData(e.data)},this.ws.onerror=function(e){t.onError("websocket error",e)}},r.prototype.write=function(t){function e(){n.emit("flush"),setTimeout(function(){n.writable=!0,n.emit("drain")},0)}var n=this;this.writable=!1;for(var r=t.length,o=0,i=r;o0&&t.jitter<=1?t.jitter:0,this.attempts=0}t.exports=n,n.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n}return 0|Math.min(t,this.max)},n.prototype.reset=function(){this.attempts=0},n.prototype.setMin=function(t){this.ms=t},n.prototype.setMax=function(t){this.max=t},n.prototype.setJitter=function(t){this.jitter=t}}])}); -//# sourceMappingURL=socket.io.js.map \ No newline at end of file diff --git a/flask__websocket/ajax_vs_websocket/_static/socket.io.min.js b/flask__websocket/ajax_vs_websocket/_static/socket.io.min.js new file mode 100644 index 000000000..f39af5c95 --- /dev/null +++ b/flask__websocket/ajax_vs_websocket/_static/socket.io.min.js @@ -0,0 +1,7 @@ +/*! + * Socket.IO v4.7.2 + * (c) 2014-2023 Guillermo Rauch + * Released under the MIT License. + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).io=e()}(this,(function(){"use strict";function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}function e(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function n(t,e){for(var n=0;nt.length)&&(e=t.length);for(var n=0,r=new Array(e);n=t.length?{done:!0}:{done:!1,value:t[r++]}},e:function(t){throw t},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,s=!0,a=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return s=t.done,t},e:function(t){a=!0,o=t},f:function(){try{s||null==n.return||n.return()}finally{if(a)throw o}}}}var v=Object.create(null);v.open="0",v.close="1",v.ping="2",v.pong="3",v.message="4",v.upgrade="5",v.noop="6";var g=Object.create(null);Object.keys(v).forEach((function(t){g[v[t]]=t}));var m,b={type:"error",data:"parser error"},k="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===Object.prototype.toString.call(Blob),w="function"==typeof ArrayBuffer,_=function(t){return"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(t):t&&t.buffer instanceof ArrayBuffer},A=function(t,e,n){var r=t.type,i=t.data;return k&&i instanceof Blob?e?n(i):O(i,n):w&&(i instanceof ArrayBuffer||_(i))?e?n(i):O(new Blob([i]),n):n(v[r]+(i||""))},O=function(t,e){var n=new FileReader;return n.onload=function(){var t=n.result.split(",")[1];e("b"+(t||""))},n.readAsDataURL(t)};function E(t){return t instanceof Uint8Array?t:t instanceof ArrayBuffer?new Uint8Array(t):new Uint8Array(t.buffer,t.byteOffset,t.byteLength)}for(var T="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",R="undefined"==typeof Uint8Array?[]:new Uint8Array(256),C=0;C<64;C++)R[T.charCodeAt(C)]=C;var B,S="function"==typeof ArrayBuffer,N=function(t,e){if("string"!=typeof t)return{type:"message",data:x(t,e)};var n=t.charAt(0);return"b"===n?{type:"message",data:L(t.substring(1),e)}:g[n]?t.length>1?{type:g[n],data:t.substring(1)}:{type:g[n]}:b},L=function(t,e){if(S){var n=function(t){var e,n,r,i,o,s=.75*t.length,a=t.length,u=0;"="===t[t.length-1]&&(s--,"="===t[t.length-2]&&s--);var c=new ArrayBuffer(s),h=new Uint8Array(c);for(e=0;e>4,h[u++]=(15&r)<<4|i>>2,h[u++]=(3&i)<<6|63&o;return c}(t);return x(n,e)}return{base64:!0,data:t}},x=function(t,e){return"blob"===e?t instanceof Blob?t:new Blob([t]):t instanceof ArrayBuffer?t:t.buffer},P=String.fromCharCode(30);function q(){return new TransformStream({transform:function(t,e){!function(t,e){k&&t.data instanceof Blob?t.data.arrayBuffer().then(E).then(e):w&&(t.data instanceof ArrayBuffer||_(t.data))?e(E(t.data)):A(t,!1,(function(t){m||(m=new TextEncoder),e(m.encode(t))}))}(t,(function(n){var r,i=n.length;if(i<126)r=new Uint8Array(1),new DataView(r.buffer).setUint8(0,i);else if(i<65536){r=new Uint8Array(3);var o=new DataView(r.buffer);o.setUint8(0,126),o.setUint16(1,i)}else{r=new Uint8Array(9);var s=new DataView(r.buffer);s.setUint8(0,127),s.setBigUint64(1,BigInt(i))}t.data&&"string"!=typeof t.data&&(r[0]|=128),e.enqueue(r),e.enqueue(n)}))}})}function j(t){return t.reduce((function(t,e){return t+e.length}),0)}function D(t,e){if(t[0].length===e)return t.shift();for(var n=new Uint8Array(e),r=0,i=0;i1?e-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{};return t+"://"+this._hostname()+this._port()+this.opts.path+this._query(e)}},{key:"_hostname",value:function(){var t=this.opts.hostname;return-1===t.indexOf(":")?t:"["+t+"]"}},{key:"_port",value:function(){return this.opts.port&&(this.opts.secure&&Number(443!==this.opts.port)||!this.opts.secure&&80!==Number(this.opts.port))?":"+this.opts.port:""}},{key:"_query",value:function(t){var e=function(t){var e="";for(var n in t)t.hasOwnProperty(n)&&(e.length&&(e+="&"),e+=encodeURIComponent(n)+"="+encodeURIComponent(t[n]));return e}(t);return e.length?"?"+e:""}}]),i}(U),z="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_".split(""),J=64,$={},Q=0,X=0;function G(t){var e="";do{e=z[t%J]+e,t=Math.floor(t/J)}while(t>0);return e}function Z(){var t=G(+new Date);return t!==K?(Q=0,K=t):t+"."+G(Q++)}for(;X0&&void 0!==arguments[0]?arguments[0]:{};return i(t,{xd:this.xd,cookieJar:this.cookieJar},this.opts),new st(this.uri(),t)}},{key:"doWrite",value:function(t,e){var n=this,r=this.request({method:"POST",data:t});r.on("success",e),r.on("error",(function(t,e){n.onError("xhr post error",t,e)}))}},{key:"doPoll",value:function(){var t=this,e=this.request();e.on("data",this.onData.bind(this)),e.on("error",(function(e,n){t.onError("xhr poll error",e,n)})),this.pollXhr=e}}]),s}(W),st=function(t){o(i,t);var n=l(i);function i(t,r){var o;return e(this,i),H(f(o=n.call(this)),r),o.opts=r,o.method=r.method||"GET",o.uri=t,o.data=void 0!==r.data?r.data:null,o.create(),o}return r(i,[{key:"create",value:function(){var t,e=this,n=F(this.opts,"agent","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","autoUnref");n.xdomain=!!this.opts.xd;var r=this.xhr=new nt(n);try{r.open(this.method,this.uri,!0);try{if(this.opts.extraHeaders)for(var o in r.setDisableHeaderCheck&&r.setDisableHeaderCheck(!0),this.opts.extraHeaders)this.opts.extraHeaders.hasOwnProperty(o)&&r.setRequestHeader(o,this.opts.extraHeaders[o])}catch(t){}if("POST"===this.method)try{r.setRequestHeader("Content-type","text/plain;charset=UTF-8")}catch(t){}try{r.setRequestHeader("Accept","*/*")}catch(t){}null===(t=this.opts.cookieJar)||void 0===t||t.addCookies(r),"withCredentials"in r&&(r.withCredentials=this.opts.withCredentials),this.opts.requestTimeout&&(r.timeout=this.opts.requestTimeout),r.onreadystatechange=function(){var t;3===r.readyState&&(null===(t=e.opts.cookieJar)||void 0===t||t.parseCookies(r)),4===r.readyState&&(200===r.status||1223===r.status?e.onLoad():e.setTimeoutFn((function(){e.onError("number"==typeof r.status?r.status:0)}),0))},r.send(this.data)}catch(t){return void this.setTimeoutFn((function(){e.onError(t)}),0)}"undefined"!=typeof document&&(this.index=i.requestsCount++,i.requests[this.index]=this)}},{key:"onError",value:function(t){this.emitReserved("error",t,this.xhr),this.cleanup(!0)}},{key:"cleanup",value:function(t){if(void 0!==this.xhr&&null!==this.xhr){if(this.xhr.onreadystatechange=rt,t)try{this.xhr.abort()}catch(t){}"undefined"!=typeof document&&delete i.requests[this.index],this.xhr=null}}},{key:"onLoad",value:function(){var t=this.xhr.responseText;null!==t&&(this.emitReserved("data",t),this.emitReserved("success"),this.cleanup())}},{key:"abort",value:function(){this.cleanup()}}]),i}(U);if(st.requestsCount=0,st.requests={},"undefined"!=typeof document)if("function"==typeof attachEvent)attachEvent("onunload",at);else if("function"==typeof addEventListener){addEventListener("onpagehide"in I?"pagehide":"unload",at,!1)}function at(){for(var t in st.requests)st.requests.hasOwnProperty(t)&&st.requests[t].abort()}var ut="function"==typeof Promise&&"function"==typeof Promise.resolve?function(t){return Promise.resolve().then(t)}:function(t,e){return e(t,0)},ct=I.WebSocket||I.MozWebSocket,ht="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),ft=function(t){o(i,t);var n=l(i);function i(t){var r;return e(this,i),(r=n.call(this,t)).supportsBinary=!t.forceBase64,r}return r(i,[{key:"name",get:function(){return"websocket"}},{key:"doOpen",value:function(){if(this.check()){var t=this.uri(),e=this.opts.protocols,n=ht?{}:F(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(n.headers=this.opts.extraHeaders);try{this.ws=ht?new ct(t,e,n):e?new ct(t,e):new ct(t)}catch(t){return this.emitReserved("error",t)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()}}},{key:"addEventListeners",value:function(){var t=this;this.ws.onopen=function(){t.opts.autoUnref&&t.ws._socket.unref(),t.onOpen()},this.ws.onclose=function(e){return t.onClose({description:"websocket connection closed",context:e})},this.ws.onmessage=function(e){return t.onData(e.data)},this.ws.onerror=function(e){return t.onError("websocket error",e)}}},{key:"write",value:function(t){var e=this;this.writable=!1;for(var n=function(){var n=t[r],i=r===t.length-1;A(n,e.supportsBinary,(function(t){try{e.ws.send(t)}catch(t){}i&&ut((function(){e.writable=!0,e.emitReserved("drain")}),e.setTimeoutFn)}))},r=0;rMath.pow(2,21)-1){a.enqueue(b);break}i=l*Math.pow(2,32)+f.getUint32(4),r=3}else{if(j(n)t){a.enqueue(b);break}}}})}(Number.MAX_SAFE_INTEGER,t.socket.binaryType),r=e.readable.pipeThrough(n).getReader(),i=q();i.readable.pipeTo(e.writable),t.writer=i.writable.getWriter();!function e(){r.read().then((function(n){var r=n.done,i=n.value;r||(t.onPacket(i),e())})).catch((function(t){}))}();var o={type:"open"};t.query.sid&&(o.data='{"sid":"'.concat(t.query.sid,'"}')),t.writer.write(o).then((function(){return t.onOpen()}))}))})))}},{key:"write",value:function(t){var e=this;this.writable=!1;for(var n=function(){var n=t[r],i=r===t.length-1;e.writer.write(n).then((function(){i&&ut((function(){e.writable=!0,e.emitReserved("drain")}),e.setTimeoutFn)}))},r=0;r1&&void 0!==arguments[1]?arguments[1]:{};return e(this,a),(r=s.call(this)).binaryType="arraybuffer",r.writeBuffer=[],n&&"object"===t(n)&&(o=n,n=null),n?(n=vt(n),o.hostname=n.host,o.secure="https"===n.protocol||"wss"===n.protocol,o.port=n.port,n.query&&(o.query=n.query)):o.host&&(o.hostname=vt(o.host).host),H(f(r),o),r.secure=null!=o.secure?o.secure:"undefined"!=typeof location&&"https:"===location.protocol,o.hostname&&!o.port&&(o.port=r.secure?"443":"80"),r.hostname=o.hostname||("undefined"!=typeof location?location.hostname:"localhost"),r.port=o.port||("undefined"!=typeof location&&location.port?location.port:r.secure?"443":"80"),r.transports=o.transports||["polling","websocket","webtransport"],r.writeBuffer=[],r.prevBufferLen=0,r.opts=i({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},o),r.opts.path=r.opts.path.replace(/\/$/,"")+(r.opts.addTrailingSlash?"/":""),"string"==typeof r.opts.query&&(r.opts.query=function(t){for(var e={},n=t.split("&"),r=0,i=n.length;r1))return this.writeBuffer;for(var t,e=1,n=0;n=57344?n+=3:(r++,n+=4);return n}(t):Math.ceil(1.33*(t.byteLength||t.size))),n>0&&e>this.maxPayload)return this.writeBuffer.slice(0,n);e+=2}return this.writeBuffer}},{key:"write",value:function(t,e,n){return this.sendPacket("message",t,e,n),this}},{key:"send",value:function(t,e,n){return this.sendPacket("message",t,e,n),this}},{key:"sendPacket",value:function(t,e,n,r){if("function"==typeof e&&(r=e,e=void 0),"function"==typeof n&&(r=n,n=null),"closing"!==this.readyState&&"closed"!==this.readyState){(n=n||{}).compress=!1!==n.compress;var i={type:t,data:e,options:n};this.emitReserved("packetCreate",i),this.writeBuffer.push(i),r&&this.once("flush",r),this.flush()}}},{key:"close",value:function(){var t=this,e=function(){t.onClose("forced close"),t.transport.close()},n=function n(){t.off("upgrade",n),t.off("upgradeError",n),e()},r=function(){t.once("upgrade",n),t.once("upgradeError",n)};return"opening"!==this.readyState&&"open"!==this.readyState||(this.readyState="closing",this.writeBuffer.length?this.once("drain",(function(){t.upgrading?r():e()})):this.upgrading?r():e()),this}},{key:"onError",value:function(t){a.priorWebsocketSuccess=!1,this.emitReserved("error",t),this.onClose("transport error",t)}},{key:"onClose",value:function(t,e){"opening"!==this.readyState&&"open"!==this.readyState&&"closing"!==this.readyState||(this.clearTimeoutFn(this.pingTimeoutTimer),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),"function"==typeof removeEventListener&&(removeEventListener("beforeunload",this.beforeunloadEventListener,!1),removeEventListener("offline",this.offlineEventListener,!1)),this.readyState="closed",this.id=null,this.emitReserved("close",t,e),this.writeBuffer=[],this.prevBufferLen=0)}},{key:"filterUpgrades",value:function(t){for(var e=[],n=0,r=t.length;n=0&&e.num1?e-1:0),r=1;r1?n-1:0),i=1;in._opts.retries&&(n._queue.shift(),e&&e(t));else if(n._queue.shift(),e){for(var i=arguments.length,o=new Array(i>1?i-1:0),s=1;s0&&void 0!==arguments[0]&&arguments[0];if(this.connected&&0!==this._queue.length){var e=this._queue[0];e.pending&&!t||(e.pending=!0,e.tryCount++,this.flags=e.flags,this.emit.apply(this,e.args))}}},{key:"packet",value:function(t){t.nsp=this.nsp,this.io._packet(t)}},{key:"onopen",value:function(){var t=this;"function"==typeof this.auth?this.auth((function(e){t._sendConnectPacket(e)})):this._sendConnectPacket(this.auth)}},{key:"_sendConnectPacket",value:function(t){this.packet({type:Bt.CONNECT,data:this._pid?i({pid:this._pid,offset:this._lastOffset},t):t})}},{key:"onerror",value:function(t){this.connected||this.emitReserved("connect_error",t)}},{key:"onclose",value:function(t,e){this.connected=!1,delete this.id,this.emitReserved("disconnect",t,e)}},{key:"onpacket",value:function(t){if(t.nsp===this.nsp)switch(t.type){case Bt.CONNECT:t.data&&t.data.sid?this.onconnect(t.data.sid,t.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case Bt.EVENT:case Bt.BINARY_EVENT:this.onevent(t);break;case Bt.ACK:case Bt.BINARY_ACK:this.onack(t);break;case Bt.DISCONNECT:this.ondisconnect();break;case Bt.CONNECT_ERROR:this.destroy();var e=new Error(t.data.message);e.data=t.data.data,this.emitReserved("connect_error",e)}}},{key:"onevent",value:function(t){var e=t.data||[];null!=t.id&&e.push(this.ack(t.id)),this.connected?this.emitEvent(e):this.receiveBuffer.push(Object.freeze(e))}},{key:"emitEvent",value:function(t){if(this._anyListeners&&this._anyListeners.length){var e,n=y(this._anyListeners.slice());try{for(n.s();!(e=n.n()).done;){e.value.apply(this,t)}}catch(t){n.e(t)}finally{n.f()}}p(s(a.prototype),"emit",this).apply(this,t),this._pid&&t.length&&"string"==typeof t[t.length-1]&&(this._lastOffset=t[t.length-1])}},{key:"ack",value:function(t){var e=this,n=!1;return function(){if(!n){n=!0;for(var r=arguments.length,i=new Array(r),o=0;o0&&t.jitter<=1?t.jitter:0,this.attempts=0}It.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n}return 0|Math.min(t,this.max)},It.prototype.reset=function(){this.attempts=0},It.prototype.setMin=function(t){this.ms=t},It.prototype.setMax=function(t){this.max=t},It.prototype.setJitter=function(t){this.jitter=t};var Ft=function(n){o(s,n);var i=l(s);function s(n,r){var o,a;e(this,s),(o=i.call(this)).nsps={},o.subs=[],n&&"object"===t(n)&&(r=n,n=void 0),(r=r||{}).path=r.path||"/socket.io",o.opts=r,H(f(o),r),o.reconnection(!1!==r.reconnection),o.reconnectionAttempts(r.reconnectionAttempts||1/0),o.reconnectionDelay(r.reconnectionDelay||1e3),o.reconnectionDelayMax(r.reconnectionDelayMax||5e3),o.randomizationFactor(null!==(a=r.randomizationFactor)&&void 0!==a?a:.5),o.backoff=new It({min:o.reconnectionDelay(),max:o.reconnectionDelayMax(),jitter:o.randomizationFactor()}),o.timeout(null==r.timeout?2e4:r.timeout),o._readyState="closed",o.uri=n;var u=r.parser||qt;return o.encoder=new u.Encoder,o.decoder=new u.Decoder,o._autoConnect=!1!==r.autoConnect,o._autoConnect&&o.open(),o}return r(s,[{key:"reconnection",value:function(t){return arguments.length?(this._reconnection=!!t,this):this._reconnection}},{key:"reconnectionAttempts",value:function(t){return void 0===t?this._reconnectionAttempts:(this._reconnectionAttempts=t,this)}},{key:"reconnectionDelay",value:function(t){var e;return void 0===t?this._reconnectionDelay:(this._reconnectionDelay=t,null===(e=this.backoff)||void 0===e||e.setMin(t),this)}},{key:"randomizationFactor",value:function(t){var e;return void 0===t?this._randomizationFactor:(this._randomizationFactor=t,null===(e=this.backoff)||void 0===e||e.setJitter(t),this)}},{key:"reconnectionDelayMax",value:function(t){var e;return void 0===t?this._reconnectionDelayMax:(this._reconnectionDelayMax=t,null===(e=this.backoff)||void 0===e||e.setMax(t),this)}},{key:"timeout",value:function(t){return arguments.length?(this._timeout=t,this):this._timeout}},{key:"maybeReconnectOnOpen",value:function(){!this._reconnecting&&this._reconnection&&0===this.backoff.attempts&&this.reconnect()}},{key:"open",value:function(t){var e=this;if(~this._readyState.indexOf("open"))return this;this.engine=new gt(this.uri,this.opts);var n=this.engine,r=this;this._readyState="opening",this.skipReconnect=!1;var i=jt(n,"open",(function(){r.onopen(),t&&t()})),o=function(n){e.cleanup(),e._readyState="closed",e.emitReserved("error",n),t?t(n):e.maybeReconnectOnOpen()},s=jt(n,"error",o);if(!1!==this._timeout){var a=this._timeout,u=this.setTimeoutFn((function(){i(),o(new Error("timeout")),n.close()}),a);this.opts.autoUnref&&u.unref(),this.subs.push((function(){e.clearTimeoutFn(u)}))}return this.subs.push(i),this.subs.push(s),this}},{key:"connect",value:function(t){return this.open(t)}},{key:"onopen",value:function(){this.cleanup(),this._readyState="open",this.emitReserved("open");var t=this.engine;this.subs.push(jt(t,"ping",this.onping.bind(this)),jt(t,"data",this.ondata.bind(this)),jt(t,"error",this.onerror.bind(this)),jt(t,"close",this.onclose.bind(this)),jt(this.decoder,"decoded",this.ondecoded.bind(this)))}},{key:"onping",value:function(){this.emitReserved("ping")}},{key:"ondata",value:function(t){try{this.decoder.add(t)}catch(t){this.onclose("parse error",t)}}},{key:"ondecoded",value:function(t){var e=this;ut((function(){e.emitReserved("packet",t)}),this.setTimeoutFn)}},{key:"onerror",value:function(t){this.emitReserved("error",t)}},{key:"socket",value:function(t,e){var n=this.nsps[t];return n?this._autoConnect&&!n.active&&n.connect():(n=new Ut(this,t,e),this.nsps[t]=n),n}},{key:"_destroy",value:function(t){for(var e=0,n=Object.keys(this.nsps);e=this._reconnectionAttempts)this.backoff.reset(),this.emitReserved("reconnect_failed"),this._reconnecting=!1;else{var n=this.backoff.duration();this._reconnecting=!0;var r=this.setTimeoutFn((function(){e.skipReconnect||(t.emitReserved("reconnect_attempt",e.backoff.attempts),e.skipReconnect||e.open((function(n){n?(e._reconnecting=!1,e.reconnect(),t.emitReserved("reconnect_error",n)):e.onreconnect()})))}),n);this.opts.autoUnref&&r.unref(),this.subs.push((function(){t.clearTimeoutFn(r)}))}}},{key:"onreconnect",value:function(){var t=this.backoff.attempts;this._reconnecting=!1,this.backoff.reset(),this.emitReserved("reconnect",t)}}]),s}(U),Mt={};function Vt(e,n){"object"===t(e)&&(n=e,e=void 0);var r,i=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2?arguments[2]:void 0,r=t;n=n||"undefined"!=typeof location&&location,null==t&&(t=n.protocol+"//"+n.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?n.protocol+t:n.host+t),/^(https?|wss?):\/\//.test(t)||(t=void 0!==n?n.protocol+"//"+t:"https://"+t),r=vt(t)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var i=-1!==r.host.indexOf(":")?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+i+":"+r.port+e,r.href=r.protocol+"://"+i+(n&&n.port===r.port?"":":"+r.port),r}(e,(n=n||{}).path||"/socket.io"),o=i.source,s=i.id,a=i.path,u=Mt[s]&&a in Mt[s].nsps;return n.forceNew||n["force new connection"]||!1===n.multiplex||u?r=new Ft(o,n):(Mt[s]||(Mt[s]=new Ft(o,n)),r=Mt[s]),i.query&&!n.query&&(n.query=i.queryKey),r.socket(i.path,n)}return i(Vt,{Manager:Ft,Socket:Ut,io:Vt,connect:Vt}),Vt})); +//# sourceMappingURL=socket.io.min.js.map diff --git a/flask__websocket/ajax_vs_websocket/_static/socket.io.min.js.map b/flask__websocket/ajax_vs_websocket/_static/socket.io.min.js.map new file mode 100644 index 000000000..6a27b095e --- /dev/null +++ b/flask__websocket/ajax_vs_websocket/_static/socket.io.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"socket.io.min.js","sources":["../node_modules/engine.io-client/node_modules/engine.io-parser/build/esm/commons.js","../node_modules/engine.io-client/node_modules/engine.io-parser/build/esm/encodePacket.browser.js","../node_modules/engine.io-client/node_modules/engine.io-parser/build/esm/contrib/base64-arraybuffer.js","../node_modules/engine.io-client/node_modules/engine.io-parser/build/esm/index.js","../node_modules/engine.io-client/node_modules/engine.io-parser/build/esm/decodePacket.browser.js","../node_modules/@socket.io/component-emitter/index.mjs","../node_modules/engine.io-client/build/esm/globalThis.browser.js","../node_modules/engine.io-client/build/esm/util.js","../node_modules/engine.io-client/build/esm/transport.js","../node_modules/engine.io-client/build/esm/contrib/yeast.js","../node_modules/engine.io-client/build/esm/contrib/parseqs.js","../node_modules/engine.io-client/build/esm/contrib/has-cors.js","../node_modules/engine.io-client/build/esm/transports/xmlhttprequest.browser.js","../node_modules/engine.io-client/build/esm/transports/polling.js","../node_modules/engine.io-client/build/esm/transports/websocket-constructor.browser.js","../node_modules/engine.io-client/build/esm/transports/websocket.js","../node_modules/engine.io-client/build/esm/transports/webtransport.js","../node_modules/engine.io-client/build/esm/transports/index.js","../node_modules/engine.io-client/build/esm/contrib/parseuri.js","../node_modules/engine.io-client/build/esm/socket.js","../node_modules/engine.io-client/build/esm/index.js","../node_modules/socket.io-parser/build/esm/is-binary.js","../node_modules/socket.io-parser/build/esm/binary.js","../node_modules/socket.io-parser/build/esm/index.js","../build/esm/on.js","../build/esm/socket.js","../build/esm/contrib/backo2.js","../build/esm/manager.js","../build/esm/index.js","../build/esm/url.js"],"sourcesContent":["const PACKET_TYPES = Object.create(null); // no Map = no polyfill\nPACKET_TYPES[\"open\"] = \"0\";\nPACKET_TYPES[\"close\"] = \"1\";\nPACKET_TYPES[\"ping\"] = \"2\";\nPACKET_TYPES[\"pong\"] = \"3\";\nPACKET_TYPES[\"message\"] = \"4\";\nPACKET_TYPES[\"upgrade\"] = \"5\";\nPACKET_TYPES[\"noop\"] = \"6\";\nconst PACKET_TYPES_REVERSE = Object.create(null);\nObject.keys(PACKET_TYPES).forEach(key => {\n PACKET_TYPES_REVERSE[PACKET_TYPES[key]] = key;\n});\nconst ERROR_PACKET = { type: \"error\", data: \"parser error\" };\nexport { PACKET_TYPES, PACKET_TYPES_REVERSE, ERROR_PACKET };\n","import { PACKET_TYPES } from \"./commons.js\";\nconst withNativeBlob = typeof Blob === \"function\" ||\n (typeof Blob !== \"undefined\" &&\n Object.prototype.toString.call(Blob) === \"[object BlobConstructor]\");\nconst withNativeArrayBuffer = typeof ArrayBuffer === \"function\";\n// ArrayBuffer.isView method is not defined in IE10\nconst isView = obj => {\n return typeof ArrayBuffer.isView === \"function\"\n ? ArrayBuffer.isView(obj)\n : obj && obj.buffer instanceof ArrayBuffer;\n};\nconst encodePacket = ({ type, data }, supportsBinary, callback) => {\n if (withNativeBlob && data instanceof Blob) {\n if (supportsBinary) {\n return callback(data);\n }\n else {\n return encodeBlobAsBase64(data, callback);\n }\n }\n else if (withNativeArrayBuffer &&\n (data instanceof ArrayBuffer || isView(data))) {\n if (supportsBinary) {\n return callback(data);\n }\n else {\n return encodeBlobAsBase64(new Blob([data]), callback);\n }\n }\n // plain string\n return callback(PACKET_TYPES[type] + (data || \"\"));\n};\nconst encodeBlobAsBase64 = (data, callback) => {\n const fileReader = new FileReader();\n fileReader.onload = function () {\n const content = fileReader.result.split(\",\")[1];\n callback(\"b\" + (content || \"\"));\n };\n return fileReader.readAsDataURL(data);\n};\nfunction toArray(data) {\n if (data instanceof Uint8Array) {\n return data;\n }\n else if (data instanceof ArrayBuffer) {\n return new Uint8Array(data);\n }\n else {\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n }\n}\nlet TEXT_ENCODER;\nexport function encodePacketToBinary(packet, callback) {\n if (withNativeBlob && packet.data instanceof Blob) {\n return packet.data\n .arrayBuffer()\n .then(toArray)\n .then(callback);\n }\n else if (withNativeArrayBuffer &&\n (packet.data instanceof ArrayBuffer || isView(packet.data))) {\n return callback(toArray(packet.data));\n }\n encodePacket(packet, false, encoded => {\n if (!TEXT_ENCODER) {\n TEXT_ENCODER = new TextEncoder();\n }\n callback(TEXT_ENCODER.encode(encoded));\n });\n}\nexport { encodePacket };\n","// imported from https://github.com/socketio/base64-arraybuffer\nconst chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n// Use a lookup table to find the index.\nconst lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);\nfor (let i = 0; i < chars.length; i++) {\n lookup[chars.charCodeAt(i)] = i;\n}\nexport const encode = (arraybuffer) => {\n let bytes = new Uint8Array(arraybuffer), i, len = bytes.length, base64 = '';\n for (i = 0; i < len; i += 3) {\n base64 += chars[bytes[i] >> 2];\n base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];\n base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];\n base64 += chars[bytes[i + 2] & 63];\n }\n if (len % 3 === 2) {\n base64 = base64.substring(0, base64.length - 1) + '=';\n }\n else if (len % 3 === 1) {\n base64 = base64.substring(0, base64.length - 2) + '==';\n }\n return base64;\n};\nexport const decode = (base64) => {\n let bufferLength = base64.length * 0.75, len = base64.length, i, p = 0, encoded1, encoded2, encoded3, encoded4;\n if (base64[base64.length - 1] === '=') {\n bufferLength--;\n if (base64[base64.length - 2] === '=') {\n bufferLength--;\n }\n }\n const arraybuffer = new ArrayBuffer(bufferLength), bytes = new Uint8Array(arraybuffer);\n for (i = 0; i < len; i += 4) {\n encoded1 = lookup[base64.charCodeAt(i)];\n encoded2 = lookup[base64.charCodeAt(i + 1)];\n encoded3 = lookup[base64.charCodeAt(i + 2)];\n encoded4 = lookup[base64.charCodeAt(i + 3)];\n bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);\n bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);\n bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);\n }\n return arraybuffer;\n};\n","import { encodePacket, encodePacketToBinary } from \"./encodePacket.js\";\nimport { decodePacket } from \"./decodePacket.js\";\nimport { ERROR_PACKET } from \"./commons.js\";\nconst SEPARATOR = String.fromCharCode(30); // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text\nconst encodePayload = (packets, callback) => {\n // some packets may be added to the array while encoding, so the initial length must be saved\n const length = packets.length;\n const encodedPackets = new Array(length);\n let count = 0;\n packets.forEach((packet, i) => {\n // force base64 encoding for binary packets\n encodePacket(packet, false, encodedPacket => {\n encodedPackets[i] = encodedPacket;\n if (++count === length) {\n callback(encodedPackets.join(SEPARATOR));\n }\n });\n });\n};\nconst decodePayload = (encodedPayload, binaryType) => {\n const encodedPackets = encodedPayload.split(SEPARATOR);\n const packets = [];\n for (let i = 0; i < encodedPackets.length; i++) {\n const decodedPacket = decodePacket(encodedPackets[i], binaryType);\n packets.push(decodedPacket);\n if (decodedPacket.type === \"error\") {\n break;\n }\n }\n return packets;\n};\nexport function createPacketEncoderStream() {\n return new TransformStream({\n transform(packet, controller) {\n encodePacketToBinary(packet, encodedPacket => {\n const payloadLength = encodedPacket.length;\n let header;\n // inspired by the WebSocket format: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#decoding_payload_length\n if (payloadLength < 126) {\n header = new Uint8Array(1);\n new DataView(header.buffer).setUint8(0, payloadLength);\n }\n else if (payloadLength < 65536) {\n header = new Uint8Array(3);\n const view = new DataView(header.buffer);\n view.setUint8(0, 126);\n view.setUint16(1, payloadLength);\n }\n else {\n header = new Uint8Array(9);\n const view = new DataView(header.buffer);\n view.setUint8(0, 127);\n view.setBigUint64(1, BigInt(payloadLength));\n }\n // first bit indicates whether the payload is plain text (0) or binary (1)\n if (packet.data && typeof packet.data !== \"string\") {\n header[0] |= 0x80;\n }\n controller.enqueue(header);\n controller.enqueue(encodedPacket);\n });\n }\n });\n}\nlet TEXT_DECODER;\nfunction totalLength(chunks) {\n return chunks.reduce((acc, chunk) => acc + chunk.length, 0);\n}\nfunction concatChunks(chunks, size) {\n if (chunks[0].length === size) {\n return chunks.shift();\n }\n const buffer = new Uint8Array(size);\n let j = 0;\n for (let i = 0; i < size; i++) {\n buffer[i] = chunks[0][j++];\n if (j === chunks[0].length) {\n chunks.shift();\n j = 0;\n }\n }\n if (chunks.length && j < chunks[0].length) {\n chunks[0] = chunks[0].slice(j);\n }\n return buffer;\n}\nexport function createPacketDecoderStream(maxPayload, binaryType) {\n if (!TEXT_DECODER) {\n TEXT_DECODER = new TextDecoder();\n }\n const chunks = [];\n let state = 0 /* READ_HEADER */;\n let expectedLength = -1;\n let isBinary = false;\n return new TransformStream({\n transform(chunk, controller) {\n chunks.push(chunk);\n while (true) {\n if (state === 0 /* READ_HEADER */) {\n if (totalLength(chunks) < 1) {\n break;\n }\n const header = concatChunks(chunks, 1);\n isBinary = (header[0] & 0x80) === 0x80;\n expectedLength = header[0] & 0x7f;\n if (expectedLength < 126) {\n state = 3 /* READ_PAYLOAD */;\n }\n else if (expectedLength === 126) {\n state = 1 /* READ_EXTENDED_LENGTH_16 */;\n }\n else {\n state = 2 /* READ_EXTENDED_LENGTH_64 */;\n }\n }\n else if (state === 1 /* READ_EXTENDED_LENGTH_16 */) {\n if (totalLength(chunks) < 2) {\n break;\n }\n const headerArray = concatChunks(chunks, 2);\n expectedLength = new DataView(headerArray.buffer, headerArray.byteOffset, headerArray.length).getUint16(0);\n state = 3 /* READ_PAYLOAD */;\n }\n else if (state === 2 /* READ_EXTENDED_LENGTH_64 */) {\n if (totalLength(chunks) < 8) {\n break;\n }\n const headerArray = concatChunks(chunks, 8);\n const view = new DataView(headerArray.buffer, headerArray.byteOffset, headerArray.length);\n const n = view.getUint32(0);\n if (n > Math.pow(2, 53 - 32) - 1) {\n // the maximum safe integer in JavaScript is 2^53 - 1\n controller.enqueue(ERROR_PACKET);\n break;\n }\n expectedLength = n * Math.pow(2, 32) + view.getUint32(4);\n state = 3 /* READ_PAYLOAD */;\n }\n else {\n if (totalLength(chunks) < expectedLength) {\n break;\n }\n const data = concatChunks(chunks, expectedLength);\n controller.enqueue(decodePacket(isBinary ? data : TEXT_DECODER.decode(data), binaryType));\n state = 0 /* READ_HEADER */;\n }\n if (expectedLength === 0 || expectedLength > maxPayload) {\n controller.enqueue(ERROR_PACKET);\n break;\n }\n }\n }\n });\n}\nexport const protocol = 4;\nexport { encodePacket, encodePayload, decodePacket, decodePayload };\n","import { ERROR_PACKET, PACKET_TYPES_REVERSE } from \"./commons.js\";\nimport { decode } from \"./contrib/base64-arraybuffer.js\";\nconst withNativeArrayBuffer = typeof ArrayBuffer === \"function\";\nexport const decodePacket = (encodedPacket, binaryType) => {\n if (typeof encodedPacket !== \"string\") {\n return {\n type: \"message\",\n data: mapBinary(encodedPacket, binaryType)\n };\n }\n const type = encodedPacket.charAt(0);\n if (type === \"b\") {\n return {\n type: \"message\",\n data: decodeBase64Packet(encodedPacket.substring(1), binaryType)\n };\n }\n const packetType = PACKET_TYPES_REVERSE[type];\n if (!packetType) {\n return ERROR_PACKET;\n }\n return encodedPacket.length > 1\n ? {\n type: PACKET_TYPES_REVERSE[type],\n data: encodedPacket.substring(1)\n }\n : {\n type: PACKET_TYPES_REVERSE[type]\n };\n};\nconst decodeBase64Packet = (data, binaryType) => {\n if (withNativeArrayBuffer) {\n const decoded = decode(data);\n return mapBinary(decoded, binaryType);\n }\n else {\n return { base64: true, data }; // fallback for old browsers\n }\n};\nconst mapBinary = (data, binaryType) => {\n switch (binaryType) {\n case \"blob\":\n if (data instanceof Blob) {\n // from WebSocket + binaryType \"blob\"\n return data;\n }\n else {\n // from HTTP long-polling or WebTransport\n return new Blob([data]);\n }\n case \"arraybuffer\":\n default:\n if (data instanceof ArrayBuffer) {\n // from HTTP long-polling (base64) or WebSocket + binaryType \"arraybuffer\"\n return data;\n }\n else {\n // from WebTransport (Uint8Array)\n return data.buffer;\n }\n }\n};\n","/**\n * Initialize a new `Emitter`.\n *\n * @api public\n */\n\nexport function Emitter(obj) {\n if (obj) return mixin(obj);\n}\n\n/**\n * Mixin the emitter properties.\n *\n * @param {Object} obj\n * @return {Object}\n * @api private\n */\n\nfunction mixin(obj) {\n for (var key in Emitter.prototype) {\n obj[key] = Emitter.prototype[key];\n }\n return obj;\n}\n\n/**\n * Listen on the given `event` with `fn`.\n *\n * @param {String} event\n * @param {Function} fn\n * @return {Emitter}\n * @api public\n */\n\nEmitter.prototype.on =\nEmitter.prototype.addEventListener = function(event, fn){\n this._callbacks = this._callbacks || {};\n (this._callbacks['$' + event] = this._callbacks['$' + event] || [])\n .push(fn);\n return this;\n};\n\n/**\n * Adds an `event` listener that will be invoked a single\n * time then automatically removed.\n *\n * @param {String} event\n * @param {Function} fn\n * @return {Emitter}\n * @api public\n */\n\nEmitter.prototype.once = function(event, fn){\n function on() {\n this.off(event, on);\n fn.apply(this, arguments);\n }\n\n on.fn = fn;\n this.on(event, on);\n return this;\n};\n\n/**\n * Remove the given callback for `event` or all\n * registered callbacks.\n *\n * @param {String} event\n * @param {Function} fn\n * @return {Emitter}\n * @api public\n */\n\nEmitter.prototype.off =\nEmitter.prototype.removeListener =\nEmitter.prototype.removeAllListeners =\nEmitter.prototype.removeEventListener = function(event, fn){\n this._callbacks = this._callbacks || {};\n\n // all\n if (0 == arguments.length) {\n this._callbacks = {};\n return this;\n }\n\n // specific event\n var callbacks = this._callbacks['$' + event];\n if (!callbacks) return this;\n\n // remove all handlers\n if (1 == arguments.length) {\n delete this._callbacks['$' + event];\n return this;\n }\n\n // remove specific handler\n var cb;\n for (var i = 0; i < callbacks.length; i++) {\n cb = callbacks[i];\n if (cb === fn || cb.fn === fn) {\n callbacks.splice(i, 1);\n break;\n }\n }\n\n // Remove event specific arrays for event types that no\n // one is subscribed for to avoid memory leak.\n if (callbacks.length === 0) {\n delete this._callbacks['$' + event];\n }\n\n return this;\n};\n\n/**\n * Emit `event` with the given args.\n *\n * @param {String} event\n * @param {Mixed} ...\n * @return {Emitter}\n */\n\nEmitter.prototype.emit = function(event){\n this._callbacks = this._callbacks || {};\n\n var args = new Array(arguments.length - 1)\n , callbacks = this._callbacks['$' + event];\n\n for (var i = 1; i < arguments.length; i++) {\n args[i - 1] = arguments[i];\n }\n\n if (callbacks) {\n callbacks = callbacks.slice(0);\n for (var i = 0, len = callbacks.length; i < len; ++i) {\n callbacks[i].apply(this, args);\n }\n }\n\n return this;\n};\n\n// alias used for reserved events (protected method)\nEmitter.prototype.emitReserved = Emitter.prototype.emit;\n\n/**\n * Return array of callbacks for `event`.\n *\n * @param {String} event\n * @return {Array}\n * @api public\n */\n\nEmitter.prototype.listeners = function(event){\n this._callbacks = this._callbacks || {};\n return this._callbacks['$' + event] || [];\n};\n\n/**\n * Check if this emitter has `event` handlers.\n *\n * @param {String} event\n * @return {Boolean}\n * @api public\n */\n\nEmitter.prototype.hasListeners = function(event){\n return !! this.listeners(event).length;\n};\n","export const globalThisShim = (() => {\n if (typeof self !== \"undefined\") {\n return self;\n }\n else if (typeof window !== \"undefined\") {\n return window;\n }\n else {\n return Function(\"return this\")();\n }\n})();\n","import { globalThisShim as globalThis } from \"./globalThis.js\";\nexport function pick(obj, ...attr) {\n return attr.reduce((acc, k) => {\n if (obj.hasOwnProperty(k)) {\n acc[k] = obj[k];\n }\n return acc;\n }, {});\n}\n// Keep a reference to the real timeout functions so they can be used when overridden\nconst NATIVE_SET_TIMEOUT = globalThis.setTimeout;\nconst NATIVE_CLEAR_TIMEOUT = globalThis.clearTimeout;\nexport function installTimerFunctions(obj, opts) {\n if (opts.useNativeTimers) {\n obj.setTimeoutFn = NATIVE_SET_TIMEOUT.bind(globalThis);\n obj.clearTimeoutFn = NATIVE_CLEAR_TIMEOUT.bind(globalThis);\n }\n else {\n obj.setTimeoutFn = globalThis.setTimeout.bind(globalThis);\n obj.clearTimeoutFn = globalThis.clearTimeout.bind(globalThis);\n }\n}\n// base64 encoded buffers are about 33% bigger (https://en.wikipedia.org/wiki/Base64)\nconst BASE64_OVERHEAD = 1.33;\n// we could also have used `new Blob([obj]).size`, but it isn't supported in IE9\nexport function byteLength(obj) {\n if (typeof obj === \"string\") {\n return utf8Length(obj);\n }\n // arraybuffer or blob\n return Math.ceil((obj.byteLength || obj.size) * BASE64_OVERHEAD);\n}\nfunction utf8Length(str) {\n let c = 0, length = 0;\n for (let i = 0, l = str.length; i < l; i++) {\n c = str.charCodeAt(i);\n if (c < 0x80) {\n length += 1;\n }\n else if (c < 0x800) {\n length += 2;\n }\n else if (c < 0xd800 || c >= 0xe000) {\n length += 3;\n }\n else {\n i++;\n length += 4;\n }\n }\n return length;\n}\n","import { decodePacket } from \"engine.io-parser\";\nimport { Emitter } from \"@socket.io/component-emitter\";\nimport { installTimerFunctions } from \"./util.js\";\nimport { encode } from \"./contrib/parseqs.js\";\nclass TransportError extends Error {\n constructor(reason, description, context) {\n super(reason);\n this.description = description;\n this.context = context;\n this.type = \"TransportError\";\n }\n}\nexport class Transport extends Emitter {\n /**\n * Transport abstract constructor.\n *\n * @param {Object} opts - options\n * @protected\n */\n constructor(opts) {\n super();\n this.writable = false;\n installTimerFunctions(this, opts);\n this.opts = opts;\n this.query = opts.query;\n this.socket = opts.socket;\n }\n /**\n * Emits an error.\n *\n * @param {String} reason\n * @param description\n * @param context - the error context\n * @return {Transport} for chaining\n * @protected\n */\n onError(reason, description, context) {\n super.emitReserved(\"error\", new TransportError(reason, description, context));\n return this;\n }\n /**\n * Opens the transport.\n */\n open() {\n this.readyState = \"opening\";\n this.doOpen();\n return this;\n }\n /**\n * Closes the transport.\n */\n close() {\n if (this.readyState === \"opening\" || this.readyState === \"open\") {\n this.doClose();\n this.onClose();\n }\n return this;\n }\n /**\n * Sends multiple packets.\n *\n * @param {Array} packets\n */\n send(packets) {\n if (this.readyState === \"open\") {\n this.write(packets);\n }\n else {\n // this might happen if the transport was silently closed in the beforeunload event handler\n }\n }\n /**\n * Called upon open\n *\n * @protected\n */\n onOpen() {\n this.readyState = \"open\";\n this.writable = true;\n super.emitReserved(\"open\");\n }\n /**\n * Called with data.\n *\n * @param {String} data\n * @protected\n */\n onData(data) {\n const packet = decodePacket(data, this.socket.binaryType);\n this.onPacket(packet);\n }\n /**\n * Called with a decoded packet.\n *\n * @protected\n */\n onPacket(packet) {\n super.emitReserved(\"packet\", packet);\n }\n /**\n * Called upon close.\n *\n * @protected\n */\n onClose(details) {\n this.readyState = \"closed\";\n super.emitReserved(\"close\", details);\n }\n /**\n * Pauses the transport, in order not to lose packets during an upgrade.\n *\n * @param onPause\n */\n pause(onPause) { }\n createUri(schema, query = {}) {\n return (schema +\n \"://\" +\n this._hostname() +\n this._port() +\n this.opts.path +\n this._query(query));\n }\n _hostname() {\n const hostname = this.opts.hostname;\n return hostname.indexOf(\":\") === -1 ? hostname : \"[\" + hostname + \"]\";\n }\n _port() {\n if (this.opts.port &&\n ((this.opts.secure && Number(this.opts.port !== 443)) ||\n (!this.opts.secure && Number(this.opts.port) !== 80))) {\n return \":\" + this.opts.port;\n }\n else {\n return \"\";\n }\n }\n _query(query) {\n const encodedQuery = encode(query);\n return encodedQuery.length ? \"?\" + encodedQuery : \"\";\n }\n}\n","// imported from https://github.com/unshiftio/yeast\n'use strict';\nconst alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_'.split(''), length = 64, map = {};\nlet seed = 0, i = 0, prev;\n/**\n * Return a string representing the specified number.\n *\n * @param {Number} num The number to convert.\n * @returns {String} The string representation of the number.\n * @api public\n */\nexport function encode(num) {\n let encoded = '';\n do {\n encoded = alphabet[num % length] + encoded;\n num = Math.floor(num / length);\n } while (num > 0);\n return encoded;\n}\n/**\n * Return the integer value specified by the given string.\n *\n * @param {String} str The string to convert.\n * @returns {Number} The integer value represented by the string.\n * @api public\n */\nexport function decode(str) {\n let decoded = 0;\n for (i = 0; i < str.length; i++) {\n decoded = decoded * length + map[str.charAt(i)];\n }\n return decoded;\n}\n/**\n * Yeast: A tiny growing id generator.\n *\n * @returns {String} A unique id.\n * @api public\n */\nexport function yeast() {\n const now = encode(+new Date());\n if (now !== prev)\n return seed = 0, prev = now;\n return now + '.' + encode(seed++);\n}\n//\n// Map each character to its index.\n//\nfor (; i < length; i++)\n map[alphabet[i]] = i;\n","// imported from https://github.com/galkn/querystring\n/**\n * Compiles a querystring\n * Returns string representation of the object\n *\n * @param {Object}\n * @api private\n */\nexport function encode(obj) {\n let str = '';\n for (let i in obj) {\n if (obj.hasOwnProperty(i)) {\n if (str.length)\n str += '&';\n str += encodeURIComponent(i) + '=' + encodeURIComponent(obj[i]);\n }\n }\n return str;\n}\n/**\n * Parses a simple querystring into an object\n *\n * @param {String} qs\n * @api private\n */\nexport function decode(qs) {\n let qry = {};\n let pairs = qs.split('&');\n for (let i = 0, l = pairs.length; i < l; i++) {\n let pair = pairs[i].split('=');\n qry[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);\n }\n return qry;\n}\n","// imported from https://github.com/component/has-cors\nlet value = false;\ntry {\n value = typeof XMLHttpRequest !== 'undefined' &&\n 'withCredentials' in new XMLHttpRequest();\n}\ncatch (err) {\n // if XMLHttp support is disabled in IE then it will throw\n // when trying to create\n}\nexport const hasCORS = value;\n","// browser shim for xmlhttprequest module\nimport { hasCORS } from \"../contrib/has-cors.js\";\nimport { globalThisShim as globalThis } from \"../globalThis.js\";\nexport function XHR(opts) {\n const xdomain = opts.xdomain;\n // XMLHttpRequest can be disabled on IE\n try {\n if (\"undefined\" !== typeof XMLHttpRequest && (!xdomain || hasCORS)) {\n return new XMLHttpRequest();\n }\n }\n catch (e) { }\n if (!xdomain) {\n try {\n return new globalThis[[\"Active\"].concat(\"Object\").join(\"X\")](\"Microsoft.XMLHTTP\");\n }\n catch (e) { }\n }\n}\nexport function createCookieJar() { }\n","import { Transport } from \"../transport.js\";\nimport { yeast } from \"../contrib/yeast.js\";\nimport { encodePayload, decodePayload } from \"engine.io-parser\";\nimport { createCookieJar, XHR as XMLHttpRequest, } from \"./xmlhttprequest.js\";\nimport { Emitter } from \"@socket.io/component-emitter\";\nimport { installTimerFunctions, pick } from \"../util.js\";\nimport { globalThisShim as globalThis } from \"../globalThis.js\";\nfunction empty() { }\nconst hasXHR2 = (function () {\n const xhr = new XMLHttpRequest({\n xdomain: false,\n });\n return null != xhr.responseType;\n})();\nexport class Polling extends Transport {\n /**\n * XHR Polling constructor.\n *\n * @param {Object} opts\n * @package\n */\n constructor(opts) {\n super(opts);\n this.polling = false;\n if (typeof location !== \"undefined\") {\n const isSSL = \"https:\" === location.protocol;\n let port = location.port;\n // some user agents have empty `location.port`\n if (!port) {\n port = isSSL ? \"443\" : \"80\";\n }\n this.xd =\n (typeof location !== \"undefined\" &&\n opts.hostname !== location.hostname) ||\n port !== opts.port;\n }\n /**\n * XHR supports binary\n */\n const forceBase64 = opts && opts.forceBase64;\n this.supportsBinary = hasXHR2 && !forceBase64;\n if (this.opts.withCredentials) {\n this.cookieJar = createCookieJar();\n }\n }\n get name() {\n return \"polling\";\n }\n /**\n * Opens the socket (triggers polling). We write a PING message to determine\n * when the transport is open.\n *\n * @protected\n */\n doOpen() {\n this.poll();\n }\n /**\n * Pauses polling.\n *\n * @param {Function} onPause - callback upon buffers are flushed and transport is paused\n * @package\n */\n pause(onPause) {\n this.readyState = \"pausing\";\n const pause = () => {\n this.readyState = \"paused\";\n onPause();\n };\n if (this.polling || !this.writable) {\n let total = 0;\n if (this.polling) {\n total++;\n this.once(\"pollComplete\", function () {\n --total || pause();\n });\n }\n if (!this.writable) {\n total++;\n this.once(\"drain\", function () {\n --total || pause();\n });\n }\n }\n else {\n pause();\n }\n }\n /**\n * Starts polling cycle.\n *\n * @private\n */\n poll() {\n this.polling = true;\n this.doPoll();\n this.emitReserved(\"poll\");\n }\n /**\n * Overloads onData to detect payloads.\n *\n * @protected\n */\n onData(data) {\n const callback = (packet) => {\n // if its the first message we consider the transport open\n if (\"opening\" === this.readyState && packet.type === \"open\") {\n this.onOpen();\n }\n // if its a close packet, we close the ongoing requests\n if (\"close\" === packet.type) {\n this.onClose({ description: \"transport closed by the server\" });\n return false;\n }\n // otherwise bypass onData and handle the message\n this.onPacket(packet);\n };\n // decode payload\n decodePayload(data, this.socket.binaryType).forEach(callback);\n // if an event did not trigger closing\n if (\"closed\" !== this.readyState) {\n // if we got data we're not polling\n this.polling = false;\n this.emitReserved(\"pollComplete\");\n if (\"open\" === this.readyState) {\n this.poll();\n }\n else {\n }\n }\n }\n /**\n * For polling, send a close packet.\n *\n * @protected\n */\n doClose() {\n const close = () => {\n this.write([{ type: \"close\" }]);\n };\n if (\"open\" === this.readyState) {\n close();\n }\n else {\n // in case we're trying to close while\n // handshaking is in progress (GH-164)\n this.once(\"open\", close);\n }\n }\n /**\n * Writes a packets payload.\n *\n * @param {Array} packets - data packets\n * @protected\n */\n write(packets) {\n this.writable = false;\n encodePayload(packets, (data) => {\n this.doWrite(data, () => {\n this.writable = true;\n this.emitReserved(\"drain\");\n });\n });\n }\n /**\n * Generates uri for connection.\n *\n * @private\n */\n uri() {\n const schema = this.opts.secure ? \"https\" : \"http\";\n const query = this.query || {};\n // cache busting is forced\n if (false !== this.opts.timestampRequests) {\n query[this.opts.timestampParam] = yeast();\n }\n if (!this.supportsBinary && !query.sid) {\n query.b64 = 1;\n }\n return this.createUri(schema, query);\n }\n /**\n * Creates a request.\n *\n * @param {String} method\n * @private\n */\n request(opts = {}) {\n Object.assign(opts, { xd: this.xd, cookieJar: this.cookieJar }, this.opts);\n return new Request(this.uri(), opts);\n }\n /**\n * Sends data.\n *\n * @param {String} data to send.\n * @param {Function} called upon flush.\n * @private\n */\n doWrite(data, fn) {\n const req = this.request({\n method: \"POST\",\n data: data,\n });\n req.on(\"success\", fn);\n req.on(\"error\", (xhrStatus, context) => {\n this.onError(\"xhr post error\", xhrStatus, context);\n });\n }\n /**\n * Starts a poll cycle.\n *\n * @private\n */\n doPoll() {\n const req = this.request();\n req.on(\"data\", this.onData.bind(this));\n req.on(\"error\", (xhrStatus, context) => {\n this.onError(\"xhr poll error\", xhrStatus, context);\n });\n this.pollXhr = req;\n }\n}\nexport class Request extends Emitter {\n /**\n * Request constructor\n *\n * @param {Object} options\n * @package\n */\n constructor(uri, opts) {\n super();\n installTimerFunctions(this, opts);\n this.opts = opts;\n this.method = opts.method || \"GET\";\n this.uri = uri;\n this.data = undefined !== opts.data ? opts.data : null;\n this.create();\n }\n /**\n * Creates the XHR object and sends the request.\n *\n * @private\n */\n create() {\n var _a;\n const opts = pick(this.opts, \"agent\", \"pfx\", \"key\", \"passphrase\", \"cert\", \"ca\", \"ciphers\", \"rejectUnauthorized\", \"autoUnref\");\n opts.xdomain = !!this.opts.xd;\n const xhr = (this.xhr = new XMLHttpRequest(opts));\n try {\n xhr.open(this.method, this.uri, true);\n try {\n if (this.opts.extraHeaders) {\n xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true);\n for (let i in this.opts.extraHeaders) {\n if (this.opts.extraHeaders.hasOwnProperty(i)) {\n xhr.setRequestHeader(i, this.opts.extraHeaders[i]);\n }\n }\n }\n }\n catch (e) { }\n if (\"POST\" === this.method) {\n try {\n xhr.setRequestHeader(\"Content-type\", \"text/plain;charset=UTF-8\");\n }\n catch (e) { }\n }\n try {\n xhr.setRequestHeader(\"Accept\", \"*/*\");\n }\n catch (e) { }\n (_a = this.opts.cookieJar) === null || _a === void 0 ? void 0 : _a.addCookies(xhr);\n // ie6 check\n if (\"withCredentials\" in xhr) {\n xhr.withCredentials = this.opts.withCredentials;\n }\n if (this.opts.requestTimeout) {\n xhr.timeout = this.opts.requestTimeout;\n }\n xhr.onreadystatechange = () => {\n var _a;\n if (xhr.readyState === 3) {\n (_a = this.opts.cookieJar) === null || _a === void 0 ? void 0 : _a.parseCookies(xhr);\n }\n if (4 !== xhr.readyState)\n return;\n if (200 === xhr.status || 1223 === xhr.status) {\n this.onLoad();\n }\n else {\n // make sure the `error` event handler that's user-set\n // does not throw in the same tick and gets caught here\n this.setTimeoutFn(() => {\n this.onError(typeof xhr.status === \"number\" ? xhr.status : 0);\n }, 0);\n }\n };\n xhr.send(this.data);\n }\n catch (e) {\n // Need to defer since .create() is called directly from the constructor\n // and thus the 'error' event can only be only bound *after* this exception\n // occurs. Therefore, also, we cannot throw here at all.\n this.setTimeoutFn(() => {\n this.onError(e);\n }, 0);\n return;\n }\n if (typeof document !== \"undefined\") {\n this.index = Request.requestsCount++;\n Request.requests[this.index] = this;\n }\n }\n /**\n * Called upon error.\n *\n * @private\n */\n onError(err) {\n this.emitReserved(\"error\", err, this.xhr);\n this.cleanup(true);\n }\n /**\n * Cleans up house.\n *\n * @private\n */\n cleanup(fromError) {\n if (\"undefined\" === typeof this.xhr || null === this.xhr) {\n return;\n }\n this.xhr.onreadystatechange = empty;\n if (fromError) {\n try {\n this.xhr.abort();\n }\n catch (e) { }\n }\n if (typeof document !== \"undefined\") {\n delete Request.requests[this.index];\n }\n this.xhr = null;\n }\n /**\n * Called upon load.\n *\n * @private\n */\n onLoad() {\n const data = this.xhr.responseText;\n if (data !== null) {\n this.emitReserved(\"data\", data);\n this.emitReserved(\"success\");\n this.cleanup();\n }\n }\n /**\n * Aborts the request.\n *\n * @package\n */\n abort() {\n this.cleanup();\n }\n}\nRequest.requestsCount = 0;\nRequest.requests = {};\n/**\n * Aborts pending requests when unloading the window. This is needed to prevent\n * memory leaks (e.g. when using IE) and to ensure that no spurious error is\n * emitted.\n */\nif (typeof document !== \"undefined\") {\n // @ts-ignore\n if (typeof attachEvent === \"function\") {\n // @ts-ignore\n attachEvent(\"onunload\", unloadHandler);\n }\n else if (typeof addEventListener === \"function\") {\n const terminationEvent = \"onpagehide\" in globalThis ? \"pagehide\" : \"unload\";\n addEventListener(terminationEvent, unloadHandler, false);\n }\n}\nfunction unloadHandler() {\n for (let i in Request.requests) {\n if (Request.requests.hasOwnProperty(i)) {\n Request.requests[i].abort();\n }\n }\n}\n","import { globalThisShim as globalThis } from \"../globalThis.js\";\nexport const nextTick = (() => {\n const isPromiseAvailable = typeof Promise === \"function\" && typeof Promise.resolve === \"function\";\n if (isPromiseAvailable) {\n return (cb) => Promise.resolve().then(cb);\n }\n else {\n return (cb, setTimeoutFn) => setTimeoutFn(cb, 0);\n }\n})();\nexport const WebSocket = globalThis.WebSocket || globalThis.MozWebSocket;\nexport const usingBrowserWebSocket = true;\nexport const defaultBinaryType = \"arraybuffer\";\n","import { Transport } from \"../transport.js\";\nimport { yeast } from \"../contrib/yeast.js\";\nimport { pick } from \"../util.js\";\nimport { nextTick, usingBrowserWebSocket, WebSocket, } from \"./websocket-constructor.js\";\nimport { encodePacket } from \"engine.io-parser\";\n// detect ReactNative environment\nconst isReactNative = typeof navigator !== \"undefined\" &&\n typeof navigator.product === \"string\" &&\n navigator.product.toLowerCase() === \"reactnative\";\nexport class WS extends Transport {\n /**\n * WebSocket transport constructor.\n *\n * @param {Object} opts - connection options\n * @protected\n */\n constructor(opts) {\n super(opts);\n this.supportsBinary = !opts.forceBase64;\n }\n get name() {\n return \"websocket\";\n }\n doOpen() {\n if (!this.check()) {\n // let probe timeout\n return;\n }\n const uri = this.uri();\n const protocols = this.opts.protocols;\n // React Native only supports the 'headers' option, and will print a warning if anything else is passed\n const opts = isReactNative\n ? {}\n : pick(this.opts, \"agent\", \"perMessageDeflate\", \"pfx\", \"key\", \"passphrase\", \"cert\", \"ca\", \"ciphers\", \"rejectUnauthorized\", \"localAddress\", \"protocolVersion\", \"origin\", \"maxPayload\", \"family\", \"checkServerIdentity\");\n if (this.opts.extraHeaders) {\n opts.headers = this.opts.extraHeaders;\n }\n try {\n this.ws =\n usingBrowserWebSocket && !isReactNative\n ? protocols\n ? new WebSocket(uri, protocols)\n : new WebSocket(uri)\n : new WebSocket(uri, protocols, opts);\n }\n catch (err) {\n return this.emitReserved(\"error\", err);\n }\n this.ws.binaryType = this.socket.binaryType;\n this.addEventListeners();\n }\n /**\n * Adds event listeners to the socket\n *\n * @private\n */\n addEventListeners() {\n this.ws.onopen = () => {\n if (this.opts.autoUnref) {\n this.ws._socket.unref();\n }\n this.onOpen();\n };\n this.ws.onclose = (closeEvent) => this.onClose({\n description: \"websocket connection closed\",\n context: closeEvent,\n });\n this.ws.onmessage = (ev) => this.onData(ev.data);\n this.ws.onerror = (e) => this.onError(\"websocket error\", e);\n }\n write(packets) {\n this.writable = false;\n // encodePacket efficient as it uses WS framing\n // no need for encodePayload\n for (let i = 0; i < packets.length; i++) {\n const packet = packets[i];\n const lastPacket = i === packets.length - 1;\n encodePacket(packet, this.supportsBinary, (data) => {\n // always create a new object (GH-437)\n const opts = {};\n if (!usingBrowserWebSocket) {\n if (packet.options) {\n opts.compress = packet.options.compress;\n }\n if (this.opts.perMessageDeflate) {\n const len = \n // @ts-ignore\n \"string\" === typeof data ? Buffer.byteLength(data) : data.length;\n if (len < this.opts.perMessageDeflate.threshold) {\n opts.compress = false;\n }\n }\n }\n // Sometimes the websocket has already been closed but the browser didn't\n // have a chance of informing us about it yet, in that case send will\n // throw an error\n try {\n if (usingBrowserWebSocket) {\n // TypeError is thrown when passing the second argument on Safari\n this.ws.send(data);\n }\n else {\n this.ws.send(data, opts);\n }\n }\n catch (e) {\n }\n if (lastPacket) {\n // fake drain\n // defer to next tick to allow Socket to clear writeBuffer\n nextTick(() => {\n this.writable = true;\n this.emitReserved(\"drain\");\n }, this.setTimeoutFn);\n }\n });\n }\n }\n doClose() {\n if (typeof this.ws !== \"undefined\") {\n this.ws.close();\n this.ws = null;\n }\n }\n /**\n * Generates uri for connection.\n *\n * @private\n */\n uri() {\n const schema = this.opts.secure ? \"wss\" : \"ws\";\n const query = this.query || {};\n // append timestamp to URI\n if (this.opts.timestampRequests) {\n query[this.opts.timestampParam] = yeast();\n }\n // communicate binary support capabilities\n if (!this.supportsBinary) {\n query.b64 = 1;\n }\n return this.createUri(schema, query);\n }\n /**\n * Feature detection for WebSocket.\n *\n * @return {Boolean} whether this transport is available.\n * @private\n */\n check() {\n return !!WebSocket;\n }\n}\n","import { Transport } from \"../transport.js\";\nimport { nextTick } from \"./websocket-constructor.js\";\nimport { createPacketDecoderStream, createPacketEncoderStream, } from \"engine.io-parser\";\nexport class WT extends Transport {\n get name() {\n return \"webtransport\";\n }\n doOpen() {\n // @ts-ignore\n if (typeof WebTransport !== \"function\") {\n return;\n }\n // @ts-ignore\n this.transport = new WebTransport(this.createUri(\"https\"), this.opts.transportOptions[this.name]);\n this.transport.closed\n .then(() => {\n this.onClose();\n })\n .catch((err) => {\n this.onError(\"webtransport error\", err);\n });\n // note: we could have used async/await, but that would require some additional polyfills\n this.transport.ready.then(() => {\n this.transport.createBidirectionalStream().then((stream) => {\n const decoderStream = createPacketDecoderStream(Number.MAX_SAFE_INTEGER, this.socket.binaryType);\n const reader = stream.readable.pipeThrough(decoderStream).getReader();\n const encoderStream = createPacketEncoderStream();\n encoderStream.readable.pipeTo(stream.writable);\n this.writer = encoderStream.writable.getWriter();\n const read = () => {\n reader\n .read()\n .then(({ done, value }) => {\n if (done) {\n return;\n }\n this.onPacket(value);\n read();\n })\n .catch((err) => {\n });\n };\n read();\n const packet = { type: \"open\" };\n if (this.query.sid) {\n packet.data = `{\"sid\":\"${this.query.sid}\"}`;\n }\n this.writer.write(packet).then(() => this.onOpen());\n });\n });\n }\n write(packets) {\n this.writable = false;\n for (let i = 0; i < packets.length; i++) {\n const packet = packets[i];\n const lastPacket = i === packets.length - 1;\n this.writer.write(packet).then(() => {\n if (lastPacket) {\n nextTick(() => {\n this.writable = true;\n this.emitReserved(\"drain\");\n }, this.setTimeoutFn);\n }\n });\n }\n }\n doClose() {\n var _a;\n (_a = this.transport) === null || _a === void 0 ? void 0 : _a.close();\n }\n}\n","import { Polling } from \"./polling.js\";\nimport { WS } from \"./websocket.js\";\nimport { WT } from \"./webtransport.js\";\nexport const transports = {\n websocket: WS,\n webtransport: WT,\n polling: Polling,\n};\n","// imported from https://github.com/galkn/parseuri\n/**\n * Parses a URI\n *\n * Note: we could also have used the built-in URL object, but it isn't supported on all platforms.\n *\n * See:\n * - https://developer.mozilla.org/en-US/docs/Web/API/URL\n * - https://caniuse.com/url\n * - https://www.rfc-editor.org/rfc/rfc3986#appendix-B\n *\n * History of the parse() method:\n * - first commit: https://github.com/socketio/socket.io-client/commit/4ee1d5d94b3906a9c052b459f1a818b15f38f91c\n * - export into its own module: https://github.com/socketio/engine.io-client/commit/de2c561e4564efeb78f1bdb1ba39ef81b2822cb3\n * - reimport: https://github.com/socketio/engine.io-client/commit/df32277c3f6d622eec5ed09f493cae3f3391d242\n *\n * @author Steven Levithan (MIT license)\n * @api private\n */\nconst re = /^(?:(?![^:@\\/?#]+:[^:@\\/]*@)(http|https|ws|wss):\\/\\/)?((?:(([^:@\\/?#]*)(?::([^:@\\/?#]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\\/?#]*)(?::(\\d*))?)(((\\/(?:[^?#](?![^?#\\/]*\\.[^?#\\/.]+(?:[?#]|$)))*\\/?)?([^?#\\/]*))(?:\\?([^#]*))?(?:#(.*))?)/;\nconst parts = [\n 'source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor'\n];\nexport function parse(str) {\n const src = str, b = str.indexOf('['), e = str.indexOf(']');\n if (b != -1 && e != -1) {\n str = str.substring(0, b) + str.substring(b, e).replace(/:/g, ';') + str.substring(e, str.length);\n }\n let m = re.exec(str || ''), uri = {}, i = 14;\n while (i--) {\n uri[parts[i]] = m[i] || '';\n }\n if (b != -1 && e != -1) {\n uri.source = src;\n uri.host = uri.host.substring(1, uri.host.length - 1).replace(/;/g, ':');\n uri.authority = uri.authority.replace('[', '').replace(']', '').replace(/;/g, ':');\n uri.ipv6uri = true;\n }\n uri.pathNames = pathNames(uri, uri['path']);\n uri.queryKey = queryKey(uri, uri['query']);\n return uri;\n}\nfunction pathNames(obj, path) {\n const regx = /\\/{2,9}/g, names = path.replace(regx, \"/\").split(\"/\");\n if (path.slice(0, 1) == '/' || path.length === 0) {\n names.splice(0, 1);\n }\n if (path.slice(-1) == '/') {\n names.splice(names.length - 1, 1);\n }\n return names;\n}\nfunction queryKey(uri, query) {\n const data = {};\n query.replace(/(?:^|&)([^&=]*)=?([^&]*)/g, function ($0, $1, $2) {\n if ($1) {\n data[$1] = $2;\n }\n });\n return data;\n}\n","import { transports } from \"./transports/index.js\";\nimport { installTimerFunctions, byteLength } from \"./util.js\";\nimport { decode } from \"./contrib/parseqs.js\";\nimport { parse } from \"./contrib/parseuri.js\";\nimport { Emitter } from \"@socket.io/component-emitter\";\nimport { protocol } from \"engine.io-parser\";\nimport { defaultBinaryType } from \"./transports/websocket-constructor.js\";\nexport class Socket extends Emitter {\n /**\n * Socket constructor.\n *\n * @param {String|Object} uri - uri or options\n * @param {Object} opts - options\n */\n constructor(uri, opts = {}) {\n super();\n this.binaryType = defaultBinaryType;\n this.writeBuffer = [];\n if (uri && \"object\" === typeof uri) {\n opts = uri;\n uri = null;\n }\n if (uri) {\n uri = parse(uri);\n opts.hostname = uri.host;\n opts.secure = uri.protocol === \"https\" || uri.protocol === \"wss\";\n opts.port = uri.port;\n if (uri.query)\n opts.query = uri.query;\n }\n else if (opts.host) {\n opts.hostname = parse(opts.host).host;\n }\n installTimerFunctions(this, opts);\n this.secure =\n null != opts.secure\n ? opts.secure\n : typeof location !== \"undefined\" && \"https:\" === location.protocol;\n if (opts.hostname && !opts.port) {\n // if no port is specified manually, use the protocol default\n opts.port = this.secure ? \"443\" : \"80\";\n }\n this.hostname =\n opts.hostname ||\n (typeof location !== \"undefined\" ? location.hostname : \"localhost\");\n this.port =\n opts.port ||\n (typeof location !== \"undefined\" && location.port\n ? location.port\n : this.secure\n ? \"443\"\n : \"80\");\n this.transports = opts.transports || [\n \"polling\",\n \"websocket\",\n \"webtransport\",\n ];\n this.writeBuffer = [];\n this.prevBufferLen = 0;\n this.opts = Object.assign({\n path: \"/engine.io\",\n agent: false,\n withCredentials: false,\n upgrade: true,\n timestampParam: \"t\",\n rememberUpgrade: false,\n addTrailingSlash: true,\n rejectUnauthorized: true,\n perMessageDeflate: {\n threshold: 1024,\n },\n transportOptions: {},\n closeOnBeforeunload: false,\n }, opts);\n this.opts.path =\n this.opts.path.replace(/\\/$/, \"\") +\n (this.opts.addTrailingSlash ? \"/\" : \"\");\n if (typeof this.opts.query === \"string\") {\n this.opts.query = decode(this.opts.query);\n }\n // set on handshake\n this.id = null;\n this.upgrades = null;\n this.pingInterval = null;\n this.pingTimeout = null;\n // set on heartbeat\n this.pingTimeoutTimer = null;\n if (typeof addEventListener === \"function\") {\n if (this.opts.closeOnBeforeunload) {\n // Firefox closes the connection when the \"beforeunload\" event is emitted but not Chrome. This event listener\n // ensures every browser behaves the same (no \"disconnect\" event at the Socket.IO level when the page is\n // closed/reloaded)\n this.beforeunloadEventListener = () => {\n if (this.transport) {\n // silently close the transport\n this.transport.removeAllListeners();\n this.transport.close();\n }\n };\n addEventListener(\"beforeunload\", this.beforeunloadEventListener, false);\n }\n if (this.hostname !== \"localhost\") {\n this.offlineEventListener = () => {\n this.onClose(\"transport close\", {\n description: \"network connection lost\",\n });\n };\n addEventListener(\"offline\", this.offlineEventListener, false);\n }\n }\n this.open();\n }\n /**\n * Creates transport of the given type.\n *\n * @param {String} name - transport name\n * @return {Transport}\n * @private\n */\n createTransport(name) {\n const query = Object.assign({}, this.opts.query);\n // append engine.io protocol identifier\n query.EIO = protocol;\n // transport name\n query.transport = name;\n // session id if we already have one\n if (this.id)\n query.sid = this.id;\n const opts = Object.assign({}, this.opts, {\n query,\n socket: this,\n hostname: this.hostname,\n secure: this.secure,\n port: this.port,\n }, this.opts.transportOptions[name]);\n return new transports[name](opts);\n }\n /**\n * Initializes transport to use and starts probe.\n *\n * @private\n */\n open() {\n let transport;\n if (this.opts.rememberUpgrade &&\n Socket.priorWebsocketSuccess &&\n this.transports.indexOf(\"websocket\") !== -1) {\n transport = \"websocket\";\n }\n else if (0 === this.transports.length) {\n // Emit error on next tick so it can be listened to\n this.setTimeoutFn(() => {\n this.emitReserved(\"error\", \"No transports available\");\n }, 0);\n return;\n }\n else {\n transport = this.transports[0];\n }\n this.readyState = \"opening\";\n // Retry with the next transport if the transport is disabled (jsonp: false)\n try {\n transport = this.createTransport(transport);\n }\n catch (e) {\n this.transports.shift();\n this.open();\n return;\n }\n transport.open();\n this.setTransport(transport);\n }\n /**\n * Sets the current transport. Disables the existing one (if any).\n *\n * @private\n */\n setTransport(transport) {\n if (this.transport) {\n this.transport.removeAllListeners();\n }\n // set up transport\n this.transport = transport;\n // set up transport listeners\n transport\n .on(\"drain\", this.onDrain.bind(this))\n .on(\"packet\", this.onPacket.bind(this))\n .on(\"error\", this.onError.bind(this))\n .on(\"close\", (reason) => this.onClose(\"transport close\", reason));\n }\n /**\n * Probes a transport.\n *\n * @param {String} name - transport name\n * @private\n */\n probe(name) {\n let transport = this.createTransport(name);\n let failed = false;\n Socket.priorWebsocketSuccess = false;\n const onTransportOpen = () => {\n if (failed)\n return;\n transport.send([{ type: \"ping\", data: \"probe\" }]);\n transport.once(\"packet\", (msg) => {\n if (failed)\n return;\n if (\"pong\" === msg.type && \"probe\" === msg.data) {\n this.upgrading = true;\n this.emitReserved(\"upgrading\", transport);\n if (!transport)\n return;\n Socket.priorWebsocketSuccess = \"websocket\" === transport.name;\n this.transport.pause(() => {\n if (failed)\n return;\n if (\"closed\" === this.readyState)\n return;\n cleanup();\n this.setTransport(transport);\n transport.send([{ type: \"upgrade\" }]);\n this.emitReserved(\"upgrade\", transport);\n transport = null;\n this.upgrading = false;\n this.flush();\n });\n }\n else {\n const err = new Error(\"probe error\");\n // @ts-ignore\n err.transport = transport.name;\n this.emitReserved(\"upgradeError\", err);\n }\n });\n };\n function freezeTransport() {\n if (failed)\n return;\n // Any callback called by transport should be ignored since now\n failed = true;\n cleanup();\n transport.close();\n transport = null;\n }\n // Handle any error that happens while probing\n const onerror = (err) => {\n const error = new Error(\"probe error: \" + err);\n // @ts-ignore\n error.transport = transport.name;\n freezeTransport();\n this.emitReserved(\"upgradeError\", error);\n };\n function onTransportClose() {\n onerror(\"transport closed\");\n }\n // When the socket is closed while we're probing\n function onclose() {\n onerror(\"socket closed\");\n }\n // When the socket is upgraded while we're probing\n function onupgrade(to) {\n if (transport && to.name !== transport.name) {\n freezeTransport();\n }\n }\n // Remove all listeners on the transport and on self\n const cleanup = () => {\n transport.removeListener(\"open\", onTransportOpen);\n transport.removeListener(\"error\", onerror);\n transport.removeListener(\"close\", onTransportClose);\n this.off(\"close\", onclose);\n this.off(\"upgrading\", onupgrade);\n };\n transport.once(\"open\", onTransportOpen);\n transport.once(\"error\", onerror);\n transport.once(\"close\", onTransportClose);\n this.once(\"close\", onclose);\n this.once(\"upgrading\", onupgrade);\n if (this.upgrades.indexOf(\"webtransport\") !== -1 &&\n name !== \"webtransport\") {\n // favor WebTransport\n this.setTimeoutFn(() => {\n if (!failed) {\n transport.open();\n }\n }, 200);\n }\n else {\n transport.open();\n }\n }\n /**\n * Called when connection is deemed open.\n *\n * @private\n */\n onOpen() {\n this.readyState = \"open\";\n Socket.priorWebsocketSuccess = \"websocket\" === this.transport.name;\n this.emitReserved(\"open\");\n this.flush();\n // we check for `readyState` in case an `open`\n // listener already closed the socket\n if (\"open\" === this.readyState && this.opts.upgrade) {\n let i = 0;\n const l = this.upgrades.length;\n for (; i < l; i++) {\n this.probe(this.upgrades[i]);\n }\n }\n }\n /**\n * Handles a packet.\n *\n * @private\n */\n onPacket(packet) {\n if (\"opening\" === this.readyState ||\n \"open\" === this.readyState ||\n \"closing\" === this.readyState) {\n this.emitReserved(\"packet\", packet);\n // Socket is live - any packet counts\n this.emitReserved(\"heartbeat\");\n this.resetPingTimeout();\n switch (packet.type) {\n case \"open\":\n this.onHandshake(JSON.parse(packet.data));\n break;\n case \"ping\":\n this.sendPacket(\"pong\");\n this.emitReserved(\"ping\");\n this.emitReserved(\"pong\");\n break;\n case \"error\":\n const err = new Error(\"server error\");\n // @ts-ignore\n err.code = packet.data;\n this.onError(err);\n break;\n case \"message\":\n this.emitReserved(\"data\", packet.data);\n this.emitReserved(\"message\", packet.data);\n break;\n }\n }\n else {\n }\n }\n /**\n * Called upon handshake completion.\n *\n * @param {Object} data - handshake obj\n * @private\n */\n onHandshake(data) {\n this.emitReserved(\"handshake\", data);\n this.id = data.sid;\n this.transport.query.sid = data.sid;\n this.upgrades = this.filterUpgrades(data.upgrades);\n this.pingInterval = data.pingInterval;\n this.pingTimeout = data.pingTimeout;\n this.maxPayload = data.maxPayload;\n this.onOpen();\n // In case open handler closes socket\n if (\"closed\" === this.readyState)\n return;\n this.resetPingTimeout();\n }\n /**\n * Sets and resets ping timeout timer based on server pings.\n *\n * @private\n */\n resetPingTimeout() {\n this.clearTimeoutFn(this.pingTimeoutTimer);\n this.pingTimeoutTimer = this.setTimeoutFn(() => {\n this.onClose(\"ping timeout\");\n }, this.pingInterval + this.pingTimeout);\n if (this.opts.autoUnref) {\n this.pingTimeoutTimer.unref();\n }\n }\n /**\n * Called on `drain` event\n *\n * @private\n */\n onDrain() {\n this.writeBuffer.splice(0, this.prevBufferLen);\n // setting prevBufferLen = 0 is very important\n // for example, when upgrading, upgrade packet is sent over,\n // and a nonzero prevBufferLen could cause problems on `drain`\n this.prevBufferLen = 0;\n if (0 === this.writeBuffer.length) {\n this.emitReserved(\"drain\");\n }\n else {\n this.flush();\n }\n }\n /**\n * Flush write buffers.\n *\n * @private\n */\n flush() {\n if (\"closed\" !== this.readyState &&\n this.transport.writable &&\n !this.upgrading &&\n this.writeBuffer.length) {\n const packets = this.getWritablePackets();\n this.transport.send(packets);\n // keep track of current length of writeBuffer\n // splice writeBuffer and callbackBuffer on `drain`\n this.prevBufferLen = packets.length;\n this.emitReserved(\"flush\");\n }\n }\n /**\n * Ensure the encoded size of the writeBuffer is below the maxPayload value sent by the server (only for HTTP\n * long-polling)\n *\n * @private\n */\n getWritablePackets() {\n const shouldCheckPayloadSize = this.maxPayload &&\n this.transport.name === \"polling\" &&\n this.writeBuffer.length > 1;\n if (!shouldCheckPayloadSize) {\n return this.writeBuffer;\n }\n let payloadSize = 1; // first packet type\n for (let i = 0; i < this.writeBuffer.length; i++) {\n const data = this.writeBuffer[i].data;\n if (data) {\n payloadSize += byteLength(data);\n }\n if (i > 0 && payloadSize > this.maxPayload) {\n return this.writeBuffer.slice(0, i);\n }\n payloadSize += 2; // separator + packet type\n }\n return this.writeBuffer;\n }\n /**\n * Sends a message.\n *\n * @param {String} msg - message.\n * @param {Object} options.\n * @param {Function} callback function.\n * @return {Socket} for chaining.\n */\n write(msg, options, fn) {\n this.sendPacket(\"message\", msg, options, fn);\n return this;\n }\n send(msg, options, fn) {\n this.sendPacket(\"message\", msg, options, fn);\n return this;\n }\n /**\n * Sends a packet.\n *\n * @param {String} type: packet type.\n * @param {String} data.\n * @param {Object} options.\n * @param {Function} fn - callback function.\n * @private\n */\n sendPacket(type, data, options, fn) {\n if (\"function\" === typeof data) {\n fn = data;\n data = undefined;\n }\n if (\"function\" === typeof options) {\n fn = options;\n options = null;\n }\n if (\"closing\" === this.readyState || \"closed\" === this.readyState) {\n return;\n }\n options = options || {};\n options.compress = false !== options.compress;\n const packet = {\n type: type,\n data: data,\n options: options,\n };\n this.emitReserved(\"packetCreate\", packet);\n this.writeBuffer.push(packet);\n if (fn)\n this.once(\"flush\", fn);\n this.flush();\n }\n /**\n * Closes the connection.\n */\n close() {\n const close = () => {\n this.onClose(\"forced close\");\n this.transport.close();\n };\n const cleanupAndClose = () => {\n this.off(\"upgrade\", cleanupAndClose);\n this.off(\"upgradeError\", cleanupAndClose);\n close();\n };\n const waitForUpgrade = () => {\n // wait for upgrade to finish since we can't send packets while pausing a transport\n this.once(\"upgrade\", cleanupAndClose);\n this.once(\"upgradeError\", cleanupAndClose);\n };\n if (\"opening\" === this.readyState || \"open\" === this.readyState) {\n this.readyState = \"closing\";\n if (this.writeBuffer.length) {\n this.once(\"drain\", () => {\n if (this.upgrading) {\n waitForUpgrade();\n }\n else {\n close();\n }\n });\n }\n else if (this.upgrading) {\n waitForUpgrade();\n }\n else {\n close();\n }\n }\n return this;\n }\n /**\n * Called upon transport error\n *\n * @private\n */\n onError(err) {\n Socket.priorWebsocketSuccess = false;\n this.emitReserved(\"error\", err);\n this.onClose(\"transport error\", err);\n }\n /**\n * Called upon transport close.\n *\n * @private\n */\n onClose(reason, description) {\n if (\"opening\" === this.readyState ||\n \"open\" === this.readyState ||\n \"closing\" === this.readyState) {\n // clear timers\n this.clearTimeoutFn(this.pingTimeoutTimer);\n // stop event from firing again for transport\n this.transport.removeAllListeners(\"close\");\n // ensure transport won't stay open\n this.transport.close();\n // ignore further transport communication\n this.transport.removeAllListeners();\n if (typeof removeEventListener === \"function\") {\n removeEventListener(\"beforeunload\", this.beforeunloadEventListener, false);\n removeEventListener(\"offline\", this.offlineEventListener, false);\n }\n // set ready state\n this.readyState = \"closed\";\n // clear session id\n this.id = null;\n // emit close event\n this.emitReserved(\"close\", reason, description);\n // clean buffers after, so users can still\n // grab the buffers on `close` event\n this.writeBuffer = [];\n this.prevBufferLen = 0;\n }\n }\n /**\n * Filters upgrades, returning only those matching client transports.\n *\n * @param {Array} upgrades - server upgrades\n * @private\n */\n filterUpgrades(upgrades) {\n const filteredUpgrades = [];\n let i = 0;\n const j = upgrades.length;\n for (; i < j; i++) {\n if (~this.transports.indexOf(upgrades[i]))\n filteredUpgrades.push(upgrades[i]);\n }\n return filteredUpgrades;\n }\n}\nSocket.protocol = protocol;\n","import { Socket } from \"./socket.js\";\nexport { Socket };\nexport const protocol = Socket.protocol;\nexport { Transport } from \"./transport.js\";\nexport { transports } from \"./transports/index.js\";\nexport { installTimerFunctions } from \"./util.js\";\nexport { parse } from \"./contrib/parseuri.js\";\nexport { nextTick } from \"./transports/websocket-constructor.js\";\n","const withNativeArrayBuffer = typeof ArrayBuffer === \"function\";\nconst isView = (obj) => {\n return typeof ArrayBuffer.isView === \"function\"\n ? ArrayBuffer.isView(obj)\n : obj.buffer instanceof ArrayBuffer;\n};\nconst toString = Object.prototype.toString;\nconst withNativeBlob = typeof Blob === \"function\" ||\n (typeof Blob !== \"undefined\" &&\n toString.call(Blob) === \"[object BlobConstructor]\");\nconst withNativeFile = typeof File === \"function\" ||\n (typeof File !== \"undefined\" &&\n toString.call(File) === \"[object FileConstructor]\");\n/**\n * Returns true if obj is a Buffer, an ArrayBuffer, a Blob or a File.\n *\n * @private\n */\nexport function isBinary(obj) {\n return ((withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj))) ||\n (withNativeBlob && obj instanceof Blob) ||\n (withNativeFile && obj instanceof File));\n}\nexport function hasBinary(obj, toJSON) {\n if (!obj || typeof obj !== \"object\") {\n return false;\n }\n if (Array.isArray(obj)) {\n for (let i = 0, l = obj.length; i < l; i++) {\n if (hasBinary(obj[i])) {\n return true;\n }\n }\n return false;\n }\n if (isBinary(obj)) {\n return true;\n }\n if (obj.toJSON &&\n typeof obj.toJSON === \"function\" &&\n arguments.length === 1) {\n return hasBinary(obj.toJSON(), true);\n }\n for (const key in obj) {\n if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key])) {\n return true;\n }\n }\n return false;\n}\n","import { isBinary } from \"./is-binary.js\";\n/**\n * Replaces every Buffer | ArrayBuffer | Blob | File in packet with a numbered placeholder.\n *\n * @param {Object} packet - socket.io event packet\n * @return {Object} with deconstructed packet and list of buffers\n * @public\n */\nexport function deconstructPacket(packet) {\n const buffers = [];\n const packetData = packet.data;\n const pack = packet;\n pack.data = _deconstructPacket(packetData, buffers);\n pack.attachments = buffers.length; // number of binary 'attachments'\n return { packet: pack, buffers: buffers };\n}\nfunction _deconstructPacket(data, buffers) {\n if (!data)\n return data;\n if (isBinary(data)) {\n const placeholder = { _placeholder: true, num: buffers.length };\n buffers.push(data);\n return placeholder;\n }\n else if (Array.isArray(data)) {\n const newData = new Array(data.length);\n for (let i = 0; i < data.length; i++) {\n newData[i] = _deconstructPacket(data[i], buffers);\n }\n return newData;\n }\n else if (typeof data === \"object\" && !(data instanceof Date)) {\n const newData = {};\n for (const key in data) {\n if (Object.prototype.hasOwnProperty.call(data, key)) {\n newData[key] = _deconstructPacket(data[key], buffers);\n }\n }\n return newData;\n }\n return data;\n}\n/**\n * Reconstructs a binary packet from its placeholder packet and buffers\n *\n * @param {Object} packet - event packet with placeholders\n * @param {Array} buffers - binary buffers to put in placeholder positions\n * @return {Object} reconstructed packet\n * @public\n */\nexport function reconstructPacket(packet, buffers) {\n packet.data = _reconstructPacket(packet.data, buffers);\n delete packet.attachments; // no longer useful\n return packet;\n}\nfunction _reconstructPacket(data, buffers) {\n if (!data)\n return data;\n if (data && data._placeholder === true) {\n const isIndexValid = typeof data.num === \"number\" &&\n data.num >= 0 &&\n data.num < buffers.length;\n if (isIndexValid) {\n return buffers[data.num]; // appropriate buffer (should be natural order anyway)\n }\n else {\n throw new Error(\"illegal attachments\");\n }\n }\n else if (Array.isArray(data)) {\n for (let i = 0; i < data.length; i++) {\n data[i] = _reconstructPacket(data[i], buffers);\n }\n }\n else if (typeof data === \"object\") {\n for (const key in data) {\n if (Object.prototype.hasOwnProperty.call(data, key)) {\n data[key] = _reconstructPacket(data[key], buffers);\n }\n }\n }\n return data;\n}\n","import { Emitter } from \"@socket.io/component-emitter\";\nimport { deconstructPacket, reconstructPacket } from \"./binary.js\";\nimport { isBinary, hasBinary } from \"./is-binary.js\";\n/**\n * These strings must not be used as event names, as they have a special meaning.\n */\nconst RESERVED_EVENTS = [\n \"connect\",\n \"connect_error\",\n \"disconnect\",\n \"disconnecting\",\n \"newListener\",\n \"removeListener\", // used by the Node.js EventEmitter\n];\n/**\n * Protocol version.\n *\n * @public\n */\nexport const protocol = 5;\nexport var PacketType;\n(function (PacketType) {\n PacketType[PacketType[\"CONNECT\"] = 0] = \"CONNECT\";\n PacketType[PacketType[\"DISCONNECT\"] = 1] = \"DISCONNECT\";\n PacketType[PacketType[\"EVENT\"] = 2] = \"EVENT\";\n PacketType[PacketType[\"ACK\"] = 3] = \"ACK\";\n PacketType[PacketType[\"CONNECT_ERROR\"] = 4] = \"CONNECT_ERROR\";\n PacketType[PacketType[\"BINARY_EVENT\"] = 5] = \"BINARY_EVENT\";\n PacketType[PacketType[\"BINARY_ACK\"] = 6] = \"BINARY_ACK\";\n})(PacketType || (PacketType = {}));\n/**\n * A socket.io Encoder instance\n */\nexport class Encoder {\n /**\n * Encoder constructor\n *\n * @param {function} replacer - custom replacer to pass down to JSON.parse\n */\n constructor(replacer) {\n this.replacer = replacer;\n }\n /**\n * Encode a packet as a single string if non-binary, or as a\n * buffer sequence, depending on packet type.\n *\n * @param {Object} obj - packet object\n */\n encode(obj) {\n if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) {\n if (hasBinary(obj)) {\n return this.encodeAsBinary({\n type: obj.type === PacketType.EVENT\n ? PacketType.BINARY_EVENT\n : PacketType.BINARY_ACK,\n nsp: obj.nsp,\n data: obj.data,\n id: obj.id,\n });\n }\n }\n return [this.encodeAsString(obj)];\n }\n /**\n * Encode packet as string.\n */\n encodeAsString(obj) {\n // first is type\n let str = \"\" + obj.type;\n // attachments if we have them\n if (obj.type === PacketType.BINARY_EVENT ||\n obj.type === PacketType.BINARY_ACK) {\n str += obj.attachments + \"-\";\n }\n // if we have a namespace other than `/`\n // we append it followed by a comma `,`\n if (obj.nsp && \"/\" !== obj.nsp) {\n str += obj.nsp + \",\";\n }\n // immediately followed by the id\n if (null != obj.id) {\n str += obj.id;\n }\n // json data\n if (null != obj.data) {\n str += JSON.stringify(obj.data, this.replacer);\n }\n return str;\n }\n /**\n * Encode packet as 'buffer sequence' by removing blobs, and\n * deconstructing packet into object with placeholders and\n * a list of buffers.\n */\n encodeAsBinary(obj) {\n const deconstruction = deconstructPacket(obj);\n const pack = this.encodeAsString(deconstruction.packet);\n const buffers = deconstruction.buffers;\n buffers.unshift(pack); // add packet info to beginning of data list\n return buffers; // write all the buffers\n }\n}\n// see https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript\nfunction isObject(value) {\n return Object.prototype.toString.call(value) === \"[object Object]\";\n}\n/**\n * A socket.io Decoder instance\n *\n * @return {Object} decoder\n */\nexport class Decoder extends Emitter {\n /**\n * Decoder constructor\n *\n * @param {function} reviver - custom reviver to pass down to JSON.stringify\n */\n constructor(reviver) {\n super();\n this.reviver = reviver;\n }\n /**\n * Decodes an encoded packet string into packet JSON.\n *\n * @param {String} obj - encoded packet\n */\n add(obj) {\n let packet;\n if (typeof obj === \"string\") {\n if (this.reconstructor) {\n throw new Error(\"got plaintext data when reconstructing a packet\");\n }\n packet = this.decodeString(obj);\n const isBinaryEvent = packet.type === PacketType.BINARY_EVENT;\n if (isBinaryEvent || packet.type === PacketType.BINARY_ACK) {\n packet.type = isBinaryEvent ? PacketType.EVENT : PacketType.ACK;\n // binary packet's json\n this.reconstructor = new BinaryReconstructor(packet);\n // no attachments, labeled binary but no binary data to follow\n if (packet.attachments === 0) {\n super.emitReserved(\"decoded\", packet);\n }\n }\n else {\n // non-binary full packet\n super.emitReserved(\"decoded\", packet);\n }\n }\n else if (isBinary(obj) || obj.base64) {\n // raw binary data\n if (!this.reconstructor) {\n throw new Error(\"got binary data when not reconstructing a packet\");\n }\n else {\n packet = this.reconstructor.takeBinaryData(obj);\n if (packet) {\n // received final buffer\n this.reconstructor = null;\n super.emitReserved(\"decoded\", packet);\n }\n }\n }\n else {\n throw new Error(\"Unknown type: \" + obj);\n }\n }\n /**\n * Decode a packet String (JSON data)\n *\n * @param {String} str\n * @return {Object} packet\n */\n decodeString(str) {\n let i = 0;\n // look up type\n const p = {\n type: Number(str.charAt(0)),\n };\n if (PacketType[p.type] === undefined) {\n throw new Error(\"unknown packet type \" + p.type);\n }\n // look up attachments if type binary\n if (p.type === PacketType.BINARY_EVENT ||\n p.type === PacketType.BINARY_ACK) {\n const start = i + 1;\n while (str.charAt(++i) !== \"-\" && i != str.length) { }\n const buf = str.substring(start, i);\n if (buf != Number(buf) || str.charAt(i) !== \"-\") {\n throw new Error(\"Illegal attachments\");\n }\n p.attachments = Number(buf);\n }\n // look up namespace (if any)\n if (\"/\" === str.charAt(i + 1)) {\n const start = i + 1;\n while (++i) {\n const c = str.charAt(i);\n if (\",\" === c)\n break;\n if (i === str.length)\n break;\n }\n p.nsp = str.substring(start, i);\n }\n else {\n p.nsp = \"/\";\n }\n // look up id\n const next = str.charAt(i + 1);\n if (\"\" !== next && Number(next) == next) {\n const start = i + 1;\n while (++i) {\n const c = str.charAt(i);\n if (null == c || Number(c) != c) {\n --i;\n break;\n }\n if (i === str.length)\n break;\n }\n p.id = Number(str.substring(start, i + 1));\n }\n // look up json data\n if (str.charAt(++i)) {\n const payload = this.tryParse(str.substr(i));\n if (Decoder.isPayloadValid(p.type, payload)) {\n p.data = payload;\n }\n else {\n throw new Error(\"invalid payload\");\n }\n }\n return p;\n }\n tryParse(str) {\n try {\n return JSON.parse(str, this.reviver);\n }\n catch (e) {\n return false;\n }\n }\n static isPayloadValid(type, payload) {\n switch (type) {\n case PacketType.CONNECT:\n return isObject(payload);\n case PacketType.DISCONNECT:\n return payload === undefined;\n case PacketType.CONNECT_ERROR:\n return typeof payload === \"string\" || isObject(payload);\n case PacketType.EVENT:\n case PacketType.BINARY_EVENT:\n return (Array.isArray(payload) &&\n (typeof payload[0] === \"number\" ||\n (typeof payload[0] === \"string\" &&\n RESERVED_EVENTS.indexOf(payload[0]) === -1)));\n case PacketType.ACK:\n case PacketType.BINARY_ACK:\n return Array.isArray(payload);\n }\n }\n /**\n * Deallocates a parser's resources\n */\n destroy() {\n if (this.reconstructor) {\n this.reconstructor.finishedReconstruction();\n this.reconstructor = null;\n }\n }\n}\n/**\n * A manager of a binary event's 'buffer sequence'. Should\n * be constructed whenever a packet of type BINARY_EVENT is\n * decoded.\n *\n * @param {Object} packet\n * @return {BinaryReconstructor} initialized reconstructor\n */\nclass BinaryReconstructor {\n constructor(packet) {\n this.packet = packet;\n this.buffers = [];\n this.reconPack = packet;\n }\n /**\n * Method to be called when binary data received from connection\n * after a BINARY_EVENT packet.\n *\n * @param {Buffer | ArrayBuffer} binData - the raw binary data received\n * @return {null | Object} returns null if more binary data is expected or\n * a reconstructed packet object if all buffers have been received.\n */\n takeBinaryData(binData) {\n this.buffers.push(binData);\n if (this.buffers.length === this.reconPack.attachments) {\n // done with buffer list\n const packet = reconstructPacket(this.reconPack, this.buffers);\n this.finishedReconstruction();\n return packet;\n }\n return null;\n }\n /**\n * Cleans up binary packet reconstruction variables.\n */\n finishedReconstruction() {\n this.reconPack = null;\n this.buffers = [];\n }\n}\n","export function on(obj, ev, fn) {\n obj.on(ev, fn);\n return function subDestroy() {\n obj.off(ev, fn);\n };\n}\n","import { PacketType } from \"socket.io-parser\";\nimport { on } from \"./on.js\";\nimport { Emitter, } from \"@socket.io/component-emitter\";\n/**\n * Internal events.\n * These events can't be emitted by the user.\n */\nconst RESERVED_EVENTS = Object.freeze({\n connect: 1,\n connect_error: 1,\n disconnect: 1,\n disconnecting: 1,\n // EventEmitter reserved events: https://nodejs.org/api/events.html#events_event_newlistener\n newListener: 1,\n removeListener: 1,\n});\n/**\n * A Socket is the fundamental class for interacting with the server.\n *\n * A Socket belongs to a certain Namespace (by default /) and uses an underlying {@link Manager} to communicate.\n *\n * @example\n * const socket = io();\n *\n * socket.on(\"connect\", () => {\n * console.log(\"connected\");\n * });\n *\n * // send an event to the server\n * socket.emit(\"foo\", \"bar\");\n *\n * socket.on(\"foobar\", () => {\n * // an event was received from the server\n * });\n *\n * // upon disconnection\n * socket.on(\"disconnect\", (reason) => {\n * console.log(`disconnected due to ${reason}`);\n * });\n */\nexport class Socket extends Emitter {\n /**\n * `Socket` constructor.\n */\n constructor(io, nsp, opts) {\n super();\n /**\n * Whether the socket is currently connected to the server.\n *\n * @example\n * const socket = io();\n *\n * socket.on(\"connect\", () => {\n * console.log(socket.connected); // true\n * });\n *\n * socket.on(\"disconnect\", () => {\n * console.log(socket.connected); // false\n * });\n */\n this.connected = false;\n /**\n * Whether the connection state was recovered after a temporary disconnection. In that case, any missed packets will\n * be transmitted by the server.\n */\n this.recovered = false;\n /**\n * Buffer for packets received before the CONNECT packet\n */\n this.receiveBuffer = [];\n /**\n * Buffer for packets that will be sent once the socket is connected\n */\n this.sendBuffer = [];\n /**\n * The queue of packets to be sent with retry in case of failure.\n *\n * Packets are sent one by one, each waiting for the server acknowledgement, in order to guarantee the delivery order.\n * @private\n */\n this._queue = [];\n /**\n * A sequence to generate the ID of the {@link QueuedPacket}.\n * @private\n */\n this._queueSeq = 0;\n this.ids = 0;\n this.acks = {};\n this.flags = {};\n this.io = io;\n this.nsp = nsp;\n if (opts && opts.auth) {\n this.auth = opts.auth;\n }\n this._opts = Object.assign({}, opts);\n if (this.io._autoConnect)\n this.open();\n }\n /**\n * Whether the socket is currently disconnected\n *\n * @example\n * const socket = io();\n *\n * socket.on(\"connect\", () => {\n * console.log(socket.disconnected); // false\n * });\n *\n * socket.on(\"disconnect\", () => {\n * console.log(socket.disconnected); // true\n * });\n */\n get disconnected() {\n return !this.connected;\n }\n /**\n * Subscribe to open, close and packet events\n *\n * @private\n */\n subEvents() {\n if (this.subs)\n return;\n const io = this.io;\n this.subs = [\n on(io, \"open\", this.onopen.bind(this)),\n on(io, \"packet\", this.onpacket.bind(this)),\n on(io, \"error\", this.onerror.bind(this)),\n on(io, \"close\", this.onclose.bind(this)),\n ];\n }\n /**\n * Whether the Socket will try to reconnect when its Manager connects or reconnects.\n *\n * @example\n * const socket = io();\n *\n * console.log(socket.active); // true\n *\n * socket.on(\"disconnect\", (reason) => {\n * if (reason === \"io server disconnect\") {\n * // the disconnection was initiated by the server, you need to manually reconnect\n * console.log(socket.active); // false\n * }\n * // else the socket will automatically try to reconnect\n * console.log(socket.active); // true\n * });\n */\n get active() {\n return !!this.subs;\n }\n /**\n * \"Opens\" the socket.\n *\n * @example\n * const socket = io({\n * autoConnect: false\n * });\n *\n * socket.connect();\n */\n connect() {\n if (this.connected)\n return this;\n this.subEvents();\n if (!this.io[\"_reconnecting\"])\n this.io.open(); // ensure open\n if (\"open\" === this.io._readyState)\n this.onopen();\n return this;\n }\n /**\n * Alias for {@link connect()}.\n */\n open() {\n return this.connect();\n }\n /**\n * Sends a `message` event.\n *\n * This method mimics the WebSocket.send() method.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send\n *\n * @example\n * socket.send(\"hello\");\n *\n * // this is equivalent to\n * socket.emit(\"message\", \"hello\");\n *\n * @return self\n */\n send(...args) {\n args.unshift(\"message\");\n this.emit.apply(this, args);\n return this;\n }\n /**\n * Override `emit`.\n * If the event is in `events`, it's emitted normally.\n *\n * @example\n * socket.emit(\"hello\", \"world\");\n *\n * // all serializable datastructures are supported (no need to call JSON.stringify)\n * socket.emit(\"hello\", 1, \"2\", { 3: [\"4\"], 5: Uint8Array.from([6]) });\n *\n * // with an acknowledgement from the server\n * socket.emit(\"hello\", \"world\", (val) => {\n * // ...\n * });\n *\n * @return self\n */\n emit(ev, ...args) {\n if (RESERVED_EVENTS.hasOwnProperty(ev)) {\n throw new Error('\"' + ev.toString() + '\" is a reserved event name');\n }\n args.unshift(ev);\n if (this._opts.retries && !this.flags.fromQueue && !this.flags.volatile) {\n this._addToQueue(args);\n return this;\n }\n const packet = {\n type: PacketType.EVENT,\n data: args,\n };\n packet.options = {};\n packet.options.compress = this.flags.compress !== false;\n // event ack callback\n if (\"function\" === typeof args[args.length - 1]) {\n const id = this.ids++;\n const ack = args.pop();\n this._registerAckCallback(id, ack);\n packet.id = id;\n }\n const isTransportWritable = this.io.engine &&\n this.io.engine.transport &&\n this.io.engine.transport.writable;\n const discardPacket = this.flags.volatile && (!isTransportWritable || !this.connected);\n if (discardPacket) {\n }\n else if (this.connected) {\n this.notifyOutgoingListeners(packet);\n this.packet(packet);\n }\n else {\n this.sendBuffer.push(packet);\n }\n this.flags = {};\n return this;\n }\n /**\n * @private\n */\n _registerAckCallback(id, ack) {\n var _a;\n const timeout = (_a = this.flags.timeout) !== null && _a !== void 0 ? _a : this._opts.ackTimeout;\n if (timeout === undefined) {\n this.acks[id] = ack;\n return;\n }\n // @ts-ignore\n const timer = this.io.setTimeoutFn(() => {\n delete this.acks[id];\n for (let i = 0; i < this.sendBuffer.length; i++) {\n if (this.sendBuffer[i].id === id) {\n this.sendBuffer.splice(i, 1);\n }\n }\n ack.call(this, new Error(\"operation has timed out\"));\n }, timeout);\n this.acks[id] = (...args) => {\n // @ts-ignore\n this.io.clearTimeoutFn(timer);\n ack.apply(this, [null, ...args]);\n };\n }\n /**\n * Emits an event and waits for an acknowledgement\n *\n * @example\n * // without timeout\n * const response = await socket.emitWithAck(\"hello\", \"world\");\n *\n * // with a specific timeout\n * try {\n * const response = await socket.timeout(1000).emitWithAck(\"hello\", \"world\");\n * } catch (err) {\n * // the server did not acknowledge the event in the given delay\n * }\n *\n * @return a Promise that will be fulfilled when the server acknowledges the event\n */\n emitWithAck(ev, ...args) {\n // the timeout flag is optional\n const withErr = this.flags.timeout !== undefined || this._opts.ackTimeout !== undefined;\n return new Promise((resolve, reject) => {\n args.push((arg1, arg2) => {\n if (withErr) {\n return arg1 ? reject(arg1) : resolve(arg2);\n }\n else {\n return resolve(arg1);\n }\n });\n this.emit(ev, ...args);\n });\n }\n /**\n * Add the packet to the queue.\n * @param args\n * @private\n */\n _addToQueue(args) {\n let ack;\n if (typeof args[args.length - 1] === \"function\") {\n ack = args.pop();\n }\n const packet = {\n id: this._queueSeq++,\n tryCount: 0,\n pending: false,\n args,\n flags: Object.assign({ fromQueue: true }, this.flags),\n };\n args.push((err, ...responseArgs) => {\n if (packet !== this._queue[0]) {\n // the packet has already been acknowledged\n return;\n }\n const hasError = err !== null;\n if (hasError) {\n if (packet.tryCount > this._opts.retries) {\n this._queue.shift();\n if (ack) {\n ack(err);\n }\n }\n }\n else {\n this._queue.shift();\n if (ack) {\n ack(null, ...responseArgs);\n }\n }\n packet.pending = false;\n return this._drainQueue();\n });\n this._queue.push(packet);\n this._drainQueue();\n }\n /**\n * Send the first packet of the queue, and wait for an acknowledgement from the server.\n * @param force - whether to resend a packet that has not been acknowledged yet\n *\n * @private\n */\n _drainQueue(force = false) {\n if (!this.connected || this._queue.length === 0) {\n return;\n }\n const packet = this._queue[0];\n if (packet.pending && !force) {\n return;\n }\n packet.pending = true;\n packet.tryCount++;\n this.flags = packet.flags;\n this.emit.apply(this, packet.args);\n }\n /**\n * Sends a packet.\n *\n * @param packet\n * @private\n */\n packet(packet) {\n packet.nsp = this.nsp;\n this.io._packet(packet);\n }\n /**\n * Called upon engine `open`.\n *\n * @private\n */\n onopen() {\n if (typeof this.auth == \"function\") {\n this.auth((data) => {\n this._sendConnectPacket(data);\n });\n }\n else {\n this._sendConnectPacket(this.auth);\n }\n }\n /**\n * Sends a CONNECT packet to initiate the Socket.IO session.\n *\n * @param data\n * @private\n */\n _sendConnectPacket(data) {\n this.packet({\n type: PacketType.CONNECT,\n data: this._pid\n ? Object.assign({ pid: this._pid, offset: this._lastOffset }, data)\n : data,\n });\n }\n /**\n * Called upon engine or manager `error`.\n *\n * @param err\n * @private\n */\n onerror(err) {\n if (!this.connected) {\n this.emitReserved(\"connect_error\", err);\n }\n }\n /**\n * Called upon engine `close`.\n *\n * @param reason\n * @param description\n * @private\n */\n onclose(reason, description) {\n this.connected = false;\n delete this.id;\n this.emitReserved(\"disconnect\", reason, description);\n }\n /**\n * Called with socket packet.\n *\n * @param packet\n * @private\n */\n onpacket(packet) {\n const sameNamespace = packet.nsp === this.nsp;\n if (!sameNamespace)\n return;\n switch (packet.type) {\n case PacketType.CONNECT:\n if (packet.data && packet.data.sid) {\n this.onconnect(packet.data.sid, packet.data.pid);\n }\n else {\n this.emitReserved(\"connect_error\", new Error(\"It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)\"));\n }\n break;\n case PacketType.EVENT:\n case PacketType.BINARY_EVENT:\n this.onevent(packet);\n break;\n case PacketType.ACK:\n case PacketType.BINARY_ACK:\n this.onack(packet);\n break;\n case PacketType.DISCONNECT:\n this.ondisconnect();\n break;\n case PacketType.CONNECT_ERROR:\n this.destroy();\n const err = new Error(packet.data.message);\n // @ts-ignore\n err.data = packet.data.data;\n this.emitReserved(\"connect_error\", err);\n break;\n }\n }\n /**\n * Called upon a server event.\n *\n * @param packet\n * @private\n */\n onevent(packet) {\n const args = packet.data || [];\n if (null != packet.id) {\n args.push(this.ack(packet.id));\n }\n if (this.connected) {\n this.emitEvent(args);\n }\n else {\n this.receiveBuffer.push(Object.freeze(args));\n }\n }\n emitEvent(args) {\n if (this._anyListeners && this._anyListeners.length) {\n const listeners = this._anyListeners.slice();\n for (const listener of listeners) {\n listener.apply(this, args);\n }\n }\n super.emit.apply(this, args);\n if (this._pid && args.length && typeof args[args.length - 1] === \"string\") {\n this._lastOffset = args[args.length - 1];\n }\n }\n /**\n * Produces an ack callback to emit with an event.\n *\n * @private\n */\n ack(id) {\n const self = this;\n let sent = false;\n return function (...args) {\n // prevent double callbacks\n if (sent)\n return;\n sent = true;\n self.packet({\n type: PacketType.ACK,\n id: id,\n data: args,\n });\n };\n }\n /**\n * Called upon a server acknowlegement.\n *\n * @param packet\n * @private\n */\n onack(packet) {\n const ack = this.acks[packet.id];\n if (\"function\" === typeof ack) {\n ack.apply(this, packet.data);\n delete this.acks[packet.id];\n }\n else {\n }\n }\n /**\n * Called upon server connect.\n *\n * @private\n */\n onconnect(id, pid) {\n this.id = id;\n this.recovered = pid && this._pid === pid;\n this._pid = pid; // defined only if connection state recovery is enabled\n this.connected = true;\n this.emitBuffered();\n this.emitReserved(\"connect\");\n this._drainQueue(true);\n }\n /**\n * Emit buffered events (received and emitted).\n *\n * @private\n */\n emitBuffered() {\n this.receiveBuffer.forEach((args) => this.emitEvent(args));\n this.receiveBuffer = [];\n this.sendBuffer.forEach((packet) => {\n this.notifyOutgoingListeners(packet);\n this.packet(packet);\n });\n this.sendBuffer = [];\n }\n /**\n * Called upon server disconnect.\n *\n * @private\n */\n ondisconnect() {\n this.destroy();\n this.onclose(\"io server disconnect\");\n }\n /**\n * Called upon forced client/server side disconnections,\n * this method ensures the manager stops tracking us and\n * that reconnections don't get triggered for this.\n *\n * @private\n */\n destroy() {\n if (this.subs) {\n // clean subscriptions to avoid reconnections\n this.subs.forEach((subDestroy) => subDestroy());\n this.subs = undefined;\n }\n this.io[\"_destroy\"](this);\n }\n /**\n * Disconnects the socket manually. In that case, the socket will not try to reconnect.\n *\n * If this is the last active Socket instance of the {@link Manager}, the low-level connection will be closed.\n *\n * @example\n * const socket = io();\n *\n * socket.on(\"disconnect\", (reason) => {\n * // console.log(reason); prints \"io client disconnect\"\n * });\n *\n * socket.disconnect();\n *\n * @return self\n */\n disconnect() {\n if (this.connected) {\n this.packet({ type: PacketType.DISCONNECT });\n }\n // remove socket from pool\n this.destroy();\n if (this.connected) {\n // fire events\n this.onclose(\"io client disconnect\");\n }\n return this;\n }\n /**\n * Alias for {@link disconnect()}.\n *\n * @return self\n */\n close() {\n return this.disconnect();\n }\n /**\n * Sets the compress flag.\n *\n * @example\n * socket.compress(false).emit(\"hello\");\n *\n * @param compress - if `true`, compresses the sending data\n * @return self\n */\n compress(compress) {\n this.flags.compress = compress;\n return this;\n }\n /**\n * Sets a modifier for a subsequent event emission that the event message will be dropped when this socket is not\n * ready to send messages.\n *\n * @example\n * socket.volatile.emit(\"hello\"); // the server may or may not receive it\n *\n * @returns self\n */\n get volatile() {\n this.flags.volatile = true;\n return this;\n }\n /**\n * Sets a modifier for a subsequent event emission that the callback will be called with an error when the\n * given number of milliseconds have elapsed without an acknowledgement from the server:\n *\n * @example\n * socket.timeout(5000).emit(\"my-event\", (err) => {\n * if (err) {\n * // the server did not acknowledge the event in the given delay\n * }\n * });\n *\n * @returns self\n */\n timeout(timeout) {\n this.flags.timeout = timeout;\n return this;\n }\n /**\n * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the\n * callback.\n *\n * @example\n * socket.onAny((event, ...args) => {\n * console.log(`got ${event}`);\n * });\n *\n * @param listener\n */\n onAny(listener) {\n this._anyListeners = this._anyListeners || [];\n this._anyListeners.push(listener);\n return this;\n }\n /**\n * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the\n * callback. The listener is added to the beginning of the listeners array.\n *\n * @example\n * socket.prependAny((event, ...args) => {\n * console.log(`got event ${event}`);\n * });\n *\n * @param listener\n */\n prependAny(listener) {\n this._anyListeners = this._anyListeners || [];\n this._anyListeners.unshift(listener);\n return this;\n }\n /**\n * Removes the listener that will be fired when any event is emitted.\n *\n * @example\n * const catchAllListener = (event, ...args) => {\n * console.log(`got event ${event}`);\n * }\n *\n * socket.onAny(catchAllListener);\n *\n * // remove a specific listener\n * socket.offAny(catchAllListener);\n *\n * // or remove all listeners\n * socket.offAny();\n *\n * @param listener\n */\n offAny(listener) {\n if (!this._anyListeners) {\n return this;\n }\n if (listener) {\n const listeners = this._anyListeners;\n for (let i = 0; i < listeners.length; i++) {\n if (listener === listeners[i]) {\n listeners.splice(i, 1);\n return this;\n }\n }\n }\n else {\n this._anyListeners = [];\n }\n return this;\n }\n /**\n * Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,\n * e.g. to remove listeners.\n */\n listenersAny() {\n return this._anyListeners || [];\n }\n /**\n * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the\n * callback.\n *\n * Note: acknowledgements sent to the server are not included.\n *\n * @example\n * socket.onAnyOutgoing((event, ...args) => {\n * console.log(`sent event ${event}`);\n * });\n *\n * @param listener\n */\n onAnyOutgoing(listener) {\n this._anyOutgoingListeners = this._anyOutgoingListeners || [];\n this._anyOutgoingListeners.push(listener);\n return this;\n }\n /**\n * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the\n * callback. The listener is added to the beginning of the listeners array.\n *\n * Note: acknowledgements sent to the server are not included.\n *\n * @example\n * socket.prependAnyOutgoing((event, ...args) => {\n * console.log(`sent event ${event}`);\n * });\n *\n * @param listener\n */\n prependAnyOutgoing(listener) {\n this._anyOutgoingListeners = this._anyOutgoingListeners || [];\n this._anyOutgoingListeners.unshift(listener);\n return this;\n }\n /**\n * Removes the listener that will be fired when any event is emitted.\n *\n * @example\n * const catchAllListener = (event, ...args) => {\n * console.log(`sent event ${event}`);\n * }\n *\n * socket.onAnyOutgoing(catchAllListener);\n *\n * // remove a specific listener\n * socket.offAnyOutgoing(catchAllListener);\n *\n * // or remove all listeners\n * socket.offAnyOutgoing();\n *\n * @param [listener] - the catch-all listener (optional)\n */\n offAnyOutgoing(listener) {\n if (!this._anyOutgoingListeners) {\n return this;\n }\n if (listener) {\n const listeners = this._anyOutgoingListeners;\n for (let i = 0; i < listeners.length; i++) {\n if (listener === listeners[i]) {\n listeners.splice(i, 1);\n return this;\n }\n }\n }\n else {\n this._anyOutgoingListeners = [];\n }\n return this;\n }\n /**\n * Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,\n * e.g. to remove listeners.\n */\n listenersAnyOutgoing() {\n return this._anyOutgoingListeners || [];\n }\n /**\n * Notify the listeners for each packet sent\n *\n * @param packet\n *\n * @private\n */\n notifyOutgoingListeners(packet) {\n if (this._anyOutgoingListeners && this._anyOutgoingListeners.length) {\n const listeners = this._anyOutgoingListeners.slice();\n for (const listener of listeners) {\n listener.apply(this, packet.data);\n }\n }\n }\n}\n","/**\n * Initialize backoff timer with `opts`.\n *\n * - `min` initial timeout in milliseconds [100]\n * - `max` max timeout [10000]\n * - `jitter` [0]\n * - `factor` [2]\n *\n * @param {Object} opts\n * @api public\n */\nexport function Backoff(opts) {\n opts = opts || {};\n this.ms = opts.min || 100;\n this.max = opts.max || 10000;\n this.factor = opts.factor || 2;\n this.jitter = opts.jitter > 0 && opts.jitter <= 1 ? opts.jitter : 0;\n this.attempts = 0;\n}\n/**\n * Return the backoff duration.\n *\n * @return {Number}\n * @api public\n */\nBackoff.prototype.duration = function () {\n var ms = this.ms * Math.pow(this.factor, this.attempts++);\n if (this.jitter) {\n var rand = Math.random();\n var deviation = Math.floor(rand * this.jitter * ms);\n ms = (Math.floor(rand * 10) & 1) == 0 ? ms - deviation : ms + deviation;\n }\n return Math.min(ms, this.max) | 0;\n};\n/**\n * Reset the number of attempts.\n *\n * @api public\n */\nBackoff.prototype.reset = function () {\n this.attempts = 0;\n};\n/**\n * Set the minimum duration\n *\n * @api public\n */\nBackoff.prototype.setMin = function (min) {\n this.ms = min;\n};\n/**\n * Set the maximum duration\n *\n * @api public\n */\nBackoff.prototype.setMax = function (max) {\n this.max = max;\n};\n/**\n * Set the jitter\n *\n * @api public\n */\nBackoff.prototype.setJitter = function (jitter) {\n this.jitter = jitter;\n};\n","import { Socket as Engine, installTimerFunctions, nextTick, } from \"engine.io-client\";\nimport { Socket } from \"./socket.js\";\nimport * as parser from \"socket.io-parser\";\nimport { on } from \"./on.js\";\nimport { Backoff } from \"./contrib/backo2.js\";\nimport { Emitter, } from \"@socket.io/component-emitter\";\nexport class Manager extends Emitter {\n constructor(uri, opts) {\n var _a;\n super();\n this.nsps = {};\n this.subs = [];\n if (uri && \"object\" === typeof uri) {\n opts = uri;\n uri = undefined;\n }\n opts = opts || {};\n opts.path = opts.path || \"/socket.io\";\n this.opts = opts;\n installTimerFunctions(this, opts);\n this.reconnection(opts.reconnection !== false);\n this.reconnectionAttempts(opts.reconnectionAttempts || Infinity);\n this.reconnectionDelay(opts.reconnectionDelay || 1000);\n this.reconnectionDelayMax(opts.reconnectionDelayMax || 5000);\n this.randomizationFactor((_a = opts.randomizationFactor) !== null && _a !== void 0 ? _a : 0.5);\n this.backoff = new Backoff({\n min: this.reconnectionDelay(),\n max: this.reconnectionDelayMax(),\n jitter: this.randomizationFactor(),\n });\n this.timeout(null == opts.timeout ? 20000 : opts.timeout);\n this._readyState = \"closed\";\n this.uri = uri;\n const _parser = opts.parser || parser;\n this.encoder = new _parser.Encoder();\n this.decoder = new _parser.Decoder();\n this._autoConnect = opts.autoConnect !== false;\n if (this._autoConnect)\n this.open();\n }\n reconnection(v) {\n if (!arguments.length)\n return this._reconnection;\n this._reconnection = !!v;\n return this;\n }\n reconnectionAttempts(v) {\n if (v === undefined)\n return this._reconnectionAttempts;\n this._reconnectionAttempts = v;\n return this;\n }\n reconnectionDelay(v) {\n var _a;\n if (v === undefined)\n return this._reconnectionDelay;\n this._reconnectionDelay = v;\n (_a = this.backoff) === null || _a === void 0 ? void 0 : _a.setMin(v);\n return this;\n }\n randomizationFactor(v) {\n var _a;\n if (v === undefined)\n return this._randomizationFactor;\n this._randomizationFactor = v;\n (_a = this.backoff) === null || _a === void 0 ? void 0 : _a.setJitter(v);\n return this;\n }\n reconnectionDelayMax(v) {\n var _a;\n if (v === undefined)\n return this._reconnectionDelayMax;\n this._reconnectionDelayMax = v;\n (_a = this.backoff) === null || _a === void 0 ? void 0 : _a.setMax(v);\n return this;\n }\n timeout(v) {\n if (!arguments.length)\n return this._timeout;\n this._timeout = v;\n return this;\n }\n /**\n * Starts trying to reconnect if reconnection is enabled and we have not\n * started reconnecting yet\n *\n * @private\n */\n maybeReconnectOnOpen() {\n // Only try to reconnect if it's the first time we're connecting\n if (!this._reconnecting &&\n this._reconnection &&\n this.backoff.attempts === 0) {\n // keeps reconnection from firing twice for the same reconnection loop\n this.reconnect();\n }\n }\n /**\n * Sets the current transport `socket`.\n *\n * @param {Function} fn - optional, callback\n * @return self\n * @public\n */\n open(fn) {\n if (~this._readyState.indexOf(\"open\"))\n return this;\n this.engine = new Engine(this.uri, this.opts);\n const socket = this.engine;\n const self = this;\n this._readyState = \"opening\";\n this.skipReconnect = false;\n // emit `open`\n const openSubDestroy = on(socket, \"open\", function () {\n self.onopen();\n fn && fn();\n });\n const onError = (err) => {\n this.cleanup();\n this._readyState = \"closed\";\n this.emitReserved(\"error\", err);\n if (fn) {\n fn(err);\n }\n else {\n // Only do this if there is no fn to handle the error\n this.maybeReconnectOnOpen();\n }\n };\n // emit `error`\n const errorSub = on(socket, \"error\", onError);\n if (false !== this._timeout) {\n const timeout = this._timeout;\n // set timer\n const timer = this.setTimeoutFn(() => {\n openSubDestroy();\n onError(new Error(\"timeout\"));\n socket.close();\n }, timeout);\n if (this.opts.autoUnref) {\n timer.unref();\n }\n this.subs.push(() => {\n this.clearTimeoutFn(timer);\n });\n }\n this.subs.push(openSubDestroy);\n this.subs.push(errorSub);\n return this;\n }\n /**\n * Alias for open()\n *\n * @return self\n * @public\n */\n connect(fn) {\n return this.open(fn);\n }\n /**\n * Called upon transport open.\n *\n * @private\n */\n onopen() {\n // clear old subs\n this.cleanup();\n // mark as open\n this._readyState = \"open\";\n this.emitReserved(\"open\");\n // add new subs\n const socket = this.engine;\n this.subs.push(on(socket, \"ping\", this.onping.bind(this)), on(socket, \"data\", this.ondata.bind(this)), on(socket, \"error\", this.onerror.bind(this)), on(socket, \"close\", this.onclose.bind(this)), on(this.decoder, \"decoded\", this.ondecoded.bind(this)));\n }\n /**\n * Called upon a ping.\n *\n * @private\n */\n onping() {\n this.emitReserved(\"ping\");\n }\n /**\n * Called with data.\n *\n * @private\n */\n ondata(data) {\n try {\n this.decoder.add(data);\n }\n catch (e) {\n this.onclose(\"parse error\", e);\n }\n }\n /**\n * Called when parser fully decodes a packet.\n *\n * @private\n */\n ondecoded(packet) {\n // the nextTick call prevents an exception in a user-provided event listener from triggering a disconnection due to a \"parse error\"\n nextTick(() => {\n this.emitReserved(\"packet\", packet);\n }, this.setTimeoutFn);\n }\n /**\n * Called upon socket error.\n *\n * @private\n */\n onerror(err) {\n this.emitReserved(\"error\", err);\n }\n /**\n * Creates a new socket for the given `nsp`.\n *\n * @return {Socket}\n * @public\n */\n socket(nsp, opts) {\n let socket = this.nsps[nsp];\n if (!socket) {\n socket = new Socket(this, nsp, opts);\n this.nsps[nsp] = socket;\n }\n else if (this._autoConnect && !socket.active) {\n socket.connect();\n }\n return socket;\n }\n /**\n * Called upon a socket close.\n *\n * @param socket\n * @private\n */\n _destroy(socket) {\n const nsps = Object.keys(this.nsps);\n for (const nsp of nsps) {\n const socket = this.nsps[nsp];\n if (socket.active) {\n return;\n }\n }\n this._close();\n }\n /**\n * Writes a packet.\n *\n * @param packet\n * @private\n */\n _packet(packet) {\n const encodedPackets = this.encoder.encode(packet);\n for (let i = 0; i < encodedPackets.length; i++) {\n this.engine.write(encodedPackets[i], packet.options);\n }\n }\n /**\n * Clean up transport subscriptions and packet buffer.\n *\n * @private\n */\n cleanup() {\n this.subs.forEach((subDestroy) => subDestroy());\n this.subs.length = 0;\n this.decoder.destroy();\n }\n /**\n * Close the current socket.\n *\n * @private\n */\n _close() {\n this.skipReconnect = true;\n this._reconnecting = false;\n this.onclose(\"forced close\");\n if (this.engine)\n this.engine.close();\n }\n /**\n * Alias for close()\n *\n * @private\n */\n disconnect() {\n return this._close();\n }\n /**\n * Called upon engine close.\n *\n * @private\n */\n onclose(reason, description) {\n this.cleanup();\n this.backoff.reset();\n this._readyState = \"closed\";\n this.emitReserved(\"close\", reason, description);\n if (this._reconnection && !this.skipReconnect) {\n this.reconnect();\n }\n }\n /**\n * Attempt a reconnection.\n *\n * @private\n */\n reconnect() {\n if (this._reconnecting || this.skipReconnect)\n return this;\n const self = this;\n if (this.backoff.attempts >= this._reconnectionAttempts) {\n this.backoff.reset();\n this.emitReserved(\"reconnect_failed\");\n this._reconnecting = false;\n }\n else {\n const delay = this.backoff.duration();\n this._reconnecting = true;\n const timer = this.setTimeoutFn(() => {\n if (self.skipReconnect)\n return;\n this.emitReserved(\"reconnect_attempt\", self.backoff.attempts);\n // check again for the case socket closed in above events\n if (self.skipReconnect)\n return;\n self.open((err) => {\n if (err) {\n self._reconnecting = false;\n self.reconnect();\n this.emitReserved(\"reconnect_error\", err);\n }\n else {\n self.onreconnect();\n }\n });\n }, delay);\n if (this.opts.autoUnref) {\n timer.unref();\n }\n this.subs.push(() => {\n this.clearTimeoutFn(timer);\n });\n }\n }\n /**\n * Called upon successful reconnect.\n *\n * @private\n */\n onreconnect() {\n const attempt = this.backoff.attempts;\n this._reconnecting = false;\n this.backoff.reset();\n this.emitReserved(\"reconnect\", attempt);\n }\n}\n","import { url } from \"./url.js\";\nimport { Manager } from \"./manager.js\";\nimport { Socket } from \"./socket.js\";\n/**\n * Managers cache.\n */\nconst cache = {};\nfunction lookup(uri, opts) {\n if (typeof uri === \"object\") {\n opts = uri;\n uri = undefined;\n }\n opts = opts || {};\n const parsed = url(uri, opts.path || \"/socket.io\");\n const source = parsed.source;\n const id = parsed.id;\n const path = parsed.path;\n const sameNamespace = cache[id] && path in cache[id][\"nsps\"];\n const newConnection = opts.forceNew ||\n opts[\"force new connection\"] ||\n false === opts.multiplex ||\n sameNamespace;\n let io;\n if (newConnection) {\n io = new Manager(source, opts);\n }\n else {\n if (!cache[id]) {\n cache[id] = new Manager(source, opts);\n }\n io = cache[id];\n }\n if (parsed.query && !opts.query) {\n opts.query = parsed.queryKey;\n }\n return io.socket(parsed.path, opts);\n}\n// so that \"lookup\" can be used both as a function (e.g. `io(...)`) and as a\n// namespace (e.g. `io.connect(...)`), for backward compatibility\nObject.assign(lookup, {\n Manager,\n Socket,\n io: lookup,\n connect: lookup,\n});\n/**\n * Protocol version.\n *\n * @public\n */\nexport { protocol } from \"socket.io-parser\";\n/**\n * Expose constructors for standalone build.\n *\n * @public\n */\nexport { Manager, Socket, lookup as io, lookup as connect, lookup as default, };\n","import { parse } from \"engine.io-client\";\n/**\n * URL parser.\n *\n * @param uri - url\n * @param path - the request path of the connection\n * @param loc - An object meant to mimic window.location.\n * Defaults to window.location.\n * @public\n */\nexport function url(uri, path = \"\", loc) {\n let obj = uri;\n // default to window.location\n loc = loc || (typeof location !== \"undefined\" && location);\n if (null == uri)\n uri = loc.protocol + \"//\" + loc.host;\n // relative path support\n if (typeof uri === \"string\") {\n if (\"/\" === uri.charAt(0)) {\n if (\"/\" === uri.charAt(1)) {\n uri = loc.protocol + uri;\n }\n else {\n uri = loc.host + uri;\n }\n }\n if (!/^(https?|wss?):\\/\\//.test(uri)) {\n if (\"undefined\" !== typeof loc) {\n uri = loc.protocol + \"//\" + uri;\n }\n else {\n uri = \"https://\" + uri;\n }\n }\n // parse\n obj = parse(uri);\n }\n // make sure we treat `localhost:80` and `localhost` equally\n if (!obj.port) {\n if (/^(http|ws)$/.test(obj.protocol)) {\n obj.port = \"80\";\n }\n else if (/^(http|ws)s$/.test(obj.protocol)) {\n obj.port = \"443\";\n }\n }\n obj.path = obj.path || \"/\";\n const ipv6 = obj.host.indexOf(\":\") !== -1;\n const host = ipv6 ? \"[\" + obj.host + \"]\" : obj.host;\n // define unique id\n obj.id = obj.protocol + \"://\" + host + \":\" + obj.port + path;\n // define href\n obj.href =\n obj.protocol +\n \"://\" +\n host +\n (loc && loc.port === obj.port ? \"\" : \":\" + obj.port);\n return obj;\n}\n"],"names":["PACKET_TYPES","Object","create","PACKET_TYPES_REVERSE","keys","forEach","key","TEXT_ENCODER","ERROR_PACKET","type","data","withNativeBlob","Blob","prototype","toString","call","withNativeArrayBuffer","ArrayBuffer","isView","obj","buffer","encodePacket","_ref","supportsBinary","callback","encodeBlobAsBase64","fileReader","FileReader","onload","content","result","split","readAsDataURL","toArray","Uint8Array","byteOffset","byteLength","chars","lookup","i","charCodeAt","TEXT_DECODER","decodePacket","encodedPacket","binaryType","mapBinary","charAt","decodeBase64Packet","substring","length","decoded","base64","encoded1","encoded2","encoded3","encoded4","bufferLength","len","p","arraybuffer","bytes","decode","SEPARATOR","String","fromCharCode","createPacketEncoderStream","TransformStream","transform","packet","controller","arrayBuffer","then","encoded","TextEncoder","encode","encodePacketToBinary","header","payloadLength","DataView","setUint8","view","setUint16","setBigUint64","BigInt","enqueue","totalLength","chunks","reduce","acc","chunk","concatChunks","size","shift","j","slice","Emitter","mixin","on","addEventListener","event","fn","this","_callbacks","push","once","off","apply","arguments","removeListener","removeAllListeners","removeEventListener","cb","callbacks","splice","emit","args","Array","emitReserved","listeners","hasListeners","globalThisShim","self","window","Function","pick","_len","attr","_key","k","hasOwnProperty","NATIVE_SET_TIMEOUT","globalThis","setTimeout","NATIVE_CLEAR_TIMEOUT","clearTimeout","installTimerFunctions","opts","useNativeTimers","setTimeoutFn","bind","clearTimeoutFn","prev","TransportError","_Error","_inherits","_super","_createSuper","reason","description","context","_this","_classCallCheck","_createClass","_wrapNativeSuper","Error","Transport","_Emitter","_super2","_this2","writable","_assertThisInitialized","query","socket","value","_get","_getPrototypeOf","readyState","doOpen","doClose","onClose","packets","write","onPacket","details","onPause","schema","undefined","_hostname","_port","path","_query","hostname","indexOf","port","secure","Number","encodedQuery","str","encodeURIComponent","alphabet","map","seed","num","Math","floor","yeast","now","Date","XMLHttpRequest","err","hasCORS","XHR","xdomain","e","concat","join","empty","hasXHR2","responseType","Polling","_Transport","polling","location","isSSL","protocol","xd","forceBase64","withCredentials","cookieJar","createCookieJar","get","poll","pause","total","doPoll","_this3","encodedPayload","encodedPackets","decodedPacket","decodePayload","onOpen","_this4","close","_this5","count","encodePayload","doWrite","timestampRequests","timestampParam","sid","b64","createUri","_extends","Request","uri","_this6","req","request","method","xhrStatus","onError","_this7","onData","pollXhr","_this8","_a","_this9","xhr","open","extraHeaders","setDisableHeaderCheck","setRequestHeader","addCookies","requestTimeout","timeout","onreadystatechange","parseCookies","status","onLoad","send","document","index","requestsCount","requests","cleanup","fromError","abort","responseText","attachEvent","unloadHandler","nextTick","Promise","resolve","WebSocket","MozWebSocket","isReactNative","navigator","product","toLowerCase","WS","check","protocols","headers","ws","addEventListeners","onopen","autoUnref","_socket","unref","onclose","closeEvent","onmessage","ev","onerror","_loop","lastPacket","WT","WebTransport","transport","transportOptions","name","closed","ready","createBidirectionalStream","stream","decoderStream","maxPayload","TextDecoder","state","expectedLength","isBinary","headerArray","getUint16","n","getUint32","pow","createPacketDecoderStream","MAX_SAFE_INTEGER","reader","readable","pipeThrough","getReader","encoderStream","pipeTo","writer","getWriter","read","done","transports","websocket","webtransport","re","parts","parse","src","b","replace","m","exec","source","host","authority","ipv6uri","pathNames","regx","names","queryKey","$0","$1","$2","Socket","writeBuffer","_typeof","prevBufferLen","agent","upgrade","rememberUpgrade","addTrailingSlash","rejectUnauthorized","perMessageDeflate","threshold","closeOnBeforeunload","qs","qry","pairs","l","pair","decodeURIComponent","id","upgrades","pingInterval","pingTimeout","pingTimeoutTimer","beforeunloadEventListener","offlineEventListener","EIO","priorWebsocketSuccess","createTransport","setTransport","onDrain","failed","onTransportOpen","msg","upgrading","flush","freezeTransport","error","onTransportClose","onupgrade","to","probe","resetPingTimeout","onHandshake","JSON","sendPacket","code","filterUpgrades","getWritablePackets","payloadSize","c","utf8Length","ceil","options","compress","cleanupAndClose","waitForUpgrade","filteredUpgrades","Socket$1","withNativeFile","File","hasBinary","toJSON","isArray","deconstructPacket","buffers","packetData","pack","_deconstructPacket","attachments","placeholder","_placeholder","newData","reconstructPacket","_reconstructPacket","PacketType","RESERVED_EVENTS","Encoder","replacer","EVENT","ACK","encodeAsString","encodeAsBinary","BINARY_EVENT","BINARY_ACK","nsp","stringify","deconstruction","unshift","isObject","Decoder","reviver","reconstructor","isBinaryEvent","decodeString","BinaryReconstructor","takeBinaryData","start","buf","next","payload","tryParse","substr","isPayloadValid","finishedReconstruction","CONNECT","DISCONNECT","CONNECT_ERROR","reconPack","binData","freeze","connect","connect_error","disconnect","disconnecting","newListener","io","connected","recovered","receiveBuffer","sendBuffer","_queue","_queueSeq","ids","acks","flags","auth","_opts","_autoConnect","subs","onpacket","subEvents","_readyState","_len2","_key2","retries","fromQueue","_addToQueue","ack","pop","_registerAckCallback","isTransportWritable","engine","notifyOutgoingListeners","ackTimeout","timer","_len3","_key3","_len4","_key4","withErr","reject","arg1","arg2","tryCount","pending","_len5","responseArgs","_key5","_drainQueue","force","_packet","_sendConnectPacket","_pid","pid","offset","_lastOffset","onconnect","onevent","onack","ondisconnect","destroy","message","emitEvent","_anyListeners","_step","_iterator","_createForOfIteratorHelper","s","f","sent","_len6","_key6","emitBuffered","subDestroy","listener","_anyOutgoingListeners","_step2","_iterator2","Backoff","ms","min","max","factor","jitter","attempts","duration","rand","random","deviation","reset","setMin","setMax","setJitter","Manager","nsps","reconnection","reconnectionAttempts","Infinity","reconnectionDelay","reconnectionDelayMax","randomizationFactor","backoff","_parser","parser","encoder","decoder","autoConnect","v","_reconnection","_reconnectionAttempts","_reconnectionDelay","_randomizationFactor","_reconnectionDelayMax","_timeout","_reconnecting","reconnect","Engine","skipReconnect","openSubDestroy","maybeReconnectOnOpen","errorSub","onping","ondata","ondecoded","add","active","_i","_nsps","_close","delay","onreconnect","attempt","cache","parsed","loc","test","href","url","sameNamespace","forceNew","multiplex"],"mappings":";;;;;6lJAAA,IAAMA,EAAeC,OAAOC,OAAO,MACnCF,EAAmB,KAAI,IACvBA,EAAoB,MAAI,IACxBA,EAAmB,KAAI,IACvBA,EAAmB,KAAI,IACvBA,EAAsB,QAAI,IAC1BA,EAAsB,QAAI,IAC1BA,EAAmB,KAAI,IACvB,IAAMG,EAAuBF,OAAOC,OAAO,MAC3CD,OAAOG,KAAKJ,GAAcK,SAAQ,SAAAC,GAC9BH,EAAqBH,EAAaM,IAAQA,CAC9C,IACA,ICuCIC,EDvCEC,EAAe,CAAEC,KAAM,QAASC,KAAM,gBCXtCC,EAAiC,mBAATC,MACT,oBAATA,MACqC,6BAAzCX,OAAOY,UAAUC,SAASC,KAAKH,MACjCI,EAA+C,mBAAhBC,YAE/BC,EAAS,SAAAC,GACX,MAAqC,mBAAvBF,YAAYC,OACpBD,YAAYC,OAAOC,GACnBA,GAAOA,EAAIC,kBAAkBH,WACvC,EACMI,EAAe,SAAHC,EAAoBC,EAAgBC,GAAa,IAA3Cf,EAAIa,EAAJb,KAAMC,EAAIY,EAAJZ,KAC1B,OAAIC,GAAkBD,aAAgBE,KAC9BW,EACOC,EAASd,GAGTe,EAAmBf,EAAMc,GAG/BR,IACJN,aAAgBO,aAAeC,EAAOR,IACnCa,EACOC,EAASd,GAGTe,EAAmB,IAAIb,KAAK,CAACF,IAAQc,GAI7CA,EAASxB,EAAaS,IAASC,GAAQ,IAClD,EACMe,EAAqB,SAACf,EAAMc,GAC9B,IAAME,EAAa,IAAIC,WAKvB,OAJAD,EAAWE,OAAS,WAChB,IAAMC,EAAUH,EAAWI,OAAOC,MAAM,KAAK,GAC7CP,EAAS,KAAOK,GAAW,MAExBH,EAAWM,cAActB,EACpC,EACA,SAASuB,EAAQvB,GACb,OAAIA,aAAgBwB,WACTxB,EAEFA,aAAgBO,YACd,IAAIiB,WAAWxB,GAGf,IAAIwB,WAAWxB,EAAKU,OAAQV,EAAKyB,WAAYzB,EAAK0B,WAEjE,CC9CA,IAHA,IAAMC,EAAQ,mEAERC,EAA+B,oBAAfJ,WAA6B,GAAK,IAAIA,WAAW,KAC9DK,EAAI,EAAGA,EAAIF,GAAcE,IAC9BD,EAAOD,EAAMG,WAAWD,IAAMA,EAkB3B,ICyCHE,EC9DEzB,EAA+C,mBAAhBC,YACxByB,EAAe,SAACC,EAAeC,GACxC,GAA6B,iBAAlBD,EACP,MAAO,CACHlC,KAAM,UACNC,KAAMmC,EAAUF,EAAeC,IAGvC,IAAMnC,EAAOkC,EAAcG,OAAO,GAClC,MAAa,MAATrC,EACO,CACHA,KAAM,UACNC,KAAMqC,EAAmBJ,EAAcK,UAAU,GAAIJ,IAG1CzC,EAAqBM,GAIjCkC,EAAcM,OAAS,EACxB,CACExC,KAAMN,EAAqBM,GAC3BC,KAAMiC,EAAcK,UAAU,IAEhC,CACEvC,KAAMN,EAAqBM,IARxBD,CAUf,EACMuC,EAAqB,SAACrC,EAAMkC,GAC9B,GAAI5B,EAAuB,CACvB,IAAMkC,EFTQ,SAACC,GACnB,IAA8DZ,EAAUa,EAAUC,EAAUC,EAAUC,EAAlGC,EAA+B,IAAhBL,EAAOF,OAAeQ,EAAMN,EAAOF,OAAWS,EAAI,EACnC,MAA9BP,EAAOA,EAAOF,OAAS,KACvBO,IACkC,MAA9BL,EAAOA,EAAOF,OAAS,IACvBO,KAGR,IAAMG,EAAc,IAAI1C,YAAYuC,GAAeI,EAAQ,IAAI1B,WAAWyB,GAC1E,IAAKpB,EAAI,EAAGA,EAAIkB,EAAKlB,GAAK,EACtBa,EAAWd,EAAOa,EAAOX,WAAWD,IACpCc,EAAWf,EAAOa,EAAOX,WAAWD,EAAI,IACxCe,EAAWhB,EAAOa,EAAOX,WAAWD,EAAI,IACxCgB,EAAWjB,EAAOa,EAAOX,WAAWD,EAAI,IACxCqB,EAAMF,KAAQN,GAAY,EAAMC,GAAY,EAC5CO,EAAMF,MAAoB,GAAXL,IAAkB,EAAMC,GAAY,EACnDM,EAAMF,MAAoB,EAAXJ,IAAiB,EAAiB,GAAXC,EAE1C,OAAOI,CACX,CEVwBE,CAAOnD,GACvB,OAAOmC,EAAUK,EAASN,EAC9B,CAEI,MAAO,CAAEO,QAAQ,EAAMzC,KAAAA,EAE/B,EACMmC,EAAY,SAACnC,EAAMkC,GACrB,MACS,SADDA,EAEIlC,aAAgBE,KAETF,EAIA,IAAIE,KAAK,CAACF,IAIjBA,aAAgBO,YAETP,EAIAA,EAAKU,MAG5B,ED1DM0C,EAAYC,OAAOC,aAAa,IA4B/B,SAASC,IACZ,OAAO,IAAIC,gBAAgB,CACvBC,UAASA,SAACC,EAAQC,IFmBnB,SAA8BD,EAAQ5C,GACrCb,GAAkByD,EAAO1D,gBAAgBE,KAClCwD,EAAO1D,KACT4D,cACAC,KAAKtC,GACLsC,KAAK/C,GAELR,IACJoD,EAAO1D,gBAAgBO,aAAeC,EAAOkD,EAAO1D,OAC9Cc,EAASS,EAAQmC,EAAO1D,OAEnCW,EAAa+C,GAAQ,GAAO,SAAAI,GACnBjE,IACDA,EAAe,IAAIkE,aAEvBjD,EAASjB,EAAamE,OAAOF,GACjC,GACJ,CEnCYG,CAAqBP,GAAQ,SAAAzB,GACzB,IACIiC,EADEC,EAAgBlC,EAAcM,OAGpC,GAAI4B,EAAgB,IAChBD,EAAS,IAAI1C,WAAW,GACxB,IAAI4C,SAASF,EAAOxD,QAAQ2D,SAAS,EAAGF,QAEvC,GAAIA,EAAgB,MAAO,CAC5BD,EAAS,IAAI1C,WAAW,GACxB,IAAM8C,EAAO,IAAIF,SAASF,EAAOxD,QACjC4D,EAAKD,SAAS,EAAG,KACjBC,EAAKC,UAAU,EAAGJ,EACtB,KACK,CACDD,EAAS,IAAI1C,WAAW,GACxB,IAAM8C,EAAO,IAAIF,SAASF,EAAOxD,QACjC4D,EAAKD,SAAS,EAAG,KACjBC,EAAKE,aAAa,EAAGC,OAAON,GAChC,CAEIT,EAAO1D,MAA+B,iBAAhB0D,EAAO1D,OAC7BkE,EAAO,IAAM,KAEjBP,EAAWe,QAAQR,GACnBP,EAAWe,QAAQzC,EACvB,GACJ,GAER,CAEA,SAAS0C,EAAYC,GACjB,OAAOA,EAAOC,QAAO,SAACC,EAAKC,GAAK,OAAKD,EAAMC,EAAMxC,MAAM,GAAE,EAC7D,CACA,SAASyC,EAAaJ,EAAQK,GAC1B,GAAIL,EAAO,GAAGrC,SAAW0C,EACrB,OAAOL,EAAOM,QAIlB,IAFA,IAAMxE,EAAS,IAAIc,WAAWyD,GAC1BE,EAAI,EACCtD,EAAI,EAAGA,EAAIoD,EAAMpD,IACtBnB,EAAOmB,GAAK+C,EAAO,GAAGO,KAClBA,IAAMP,EAAO,GAAGrC,SAChBqC,EAAOM,QACPC,EAAI,GAMZ,OAHIP,EAAOrC,QAAU4C,EAAIP,EAAO,GAAGrC,SAC/BqC,EAAO,GAAKA,EAAO,GAAGQ,MAAMD,IAEzBzE,CACX,CE/EO,SAAS2E,EAAQ5E,GACtB,GAAIA,EAAK,OAWX,SAAeA,GACb,IAAK,IAAIb,KAAOyF,EAAQlF,UACtBM,EAAIb,GAAOyF,EAAQlF,UAAUP,GAE/B,OAAOa,CACT,CAhBkB6E,CAAM7E,EACxB,CA0BA4E,EAAQlF,UAAUoF,GAClBF,EAAQlF,UAAUqF,iBAAmB,SAASC,EAAOC,GAInD,OAHAC,KAAKC,WAAaD,KAAKC,YAAc,CAAA,GACpCD,KAAKC,WAAW,IAAMH,GAASE,KAAKC,WAAW,IAAMH,IAAU,IAC7DI,KAAKH,GACDC,IACT,EAYAN,EAAQlF,UAAU2F,KAAO,SAASL,EAAOC,GACvC,SAASH,IACPI,KAAKI,IAAIN,EAAOF,GAChBG,EAAGM,MAAML,KAAMM,UACjB,CAIA,OAFAV,EAAGG,GAAKA,EACRC,KAAKJ,GAAGE,EAAOF,GACRI,IACT,EAYAN,EAAQlF,UAAU4F,IAClBV,EAAQlF,UAAU+F,eAClBb,EAAQlF,UAAUgG,mBAClBd,EAAQlF,UAAUiG,oBAAsB,SAASX,EAAOC,GAItD,GAHAC,KAAKC,WAAaD,KAAKC,YAAc,CAAA,EAGjC,GAAKK,UAAU1D,OAEjB,OADAoD,KAAKC,WAAa,GACXD,KAIT,IAUIU,EAVAC,EAAYX,KAAKC,WAAW,IAAMH,GACtC,IAAKa,EAAW,OAAOX,KAGvB,GAAI,GAAKM,UAAU1D,OAEjB,cADOoD,KAAKC,WAAW,IAAMH,GACtBE,KAKT,IAAK,IAAI9D,EAAI,EAAGA,EAAIyE,EAAU/D,OAAQV,IAEpC,IADAwE,EAAKC,EAAUzE,MACJ6D,GAAMW,EAAGX,KAAOA,EAAI,CAC7BY,EAAUC,OAAO1E,EAAG,GACpB,KACF,CASF,OAJyB,IAArByE,EAAU/D,eACLoD,KAAKC,WAAW,IAAMH,GAGxBE,IACT,EAUAN,EAAQlF,UAAUqG,KAAO,SAASf,GAChCE,KAAKC,WAAaD,KAAKC,YAAc,CAAA,EAKrC,IAHA,IAAIa,EAAO,IAAIC,MAAMT,UAAU1D,OAAS,GACpC+D,EAAYX,KAAKC,WAAW,IAAMH,GAE7B5D,EAAI,EAAGA,EAAIoE,UAAU1D,OAAQV,IACpC4E,EAAK5E,EAAI,GAAKoE,UAAUpE,GAG1B,GAAIyE,EAEG,CAAIzE,EAAI,EAAb,IAAK,IAAWkB,GADhBuD,EAAYA,EAAUlB,MAAM,IACI7C,OAAQV,EAAIkB,IAAOlB,EACjDyE,EAAUzE,GAAGmE,MAAML,KAAMc,EADKlE,CAKlC,OAAOoD,IACT,EAGAN,EAAQlF,UAAUwG,aAAetB,EAAQlF,UAAUqG,KAUnDnB,EAAQlF,UAAUyG,UAAY,SAASnB,GAErC,OADAE,KAAKC,WAAaD,KAAKC,YAAc,CAAA,EAC9BD,KAAKC,WAAW,IAAMH,IAAU,EACzC,EAUAJ,EAAQlF,UAAU0G,aAAe,SAASpB,GACxC,QAAUE,KAAKiB,UAAUnB,GAAOlD,MAClC,ECxKO,IAAMuE,EACW,oBAATC,KACAA,KAEgB,oBAAXC,OACLA,OAGAC,SAAS,cAATA,GCPR,SAASC,EAAKzG,GAAc,IAAA0G,IAAAA,EAAAlB,UAAA1D,OAAN6E,MAAIV,MAAAS,EAAAA,EAAAA,OAAAE,EAAA,EAAAA,EAAAF,EAAAE,IAAJD,EAAIC,EAAApB,GAAAA,UAAAoB,GAC7B,OAAOD,EAAKvC,QAAO,SAACC,EAAKwC,GAIrB,OAHI7G,EAAI8G,eAAeD,KACnBxC,EAAIwC,GAAK7G,EAAI6G,IAEVxC,CACV,GAAE,CAAE,EACT,CAEA,IAAM0C,EAAqBC,EAAWC,WAChCC,EAAuBF,EAAWG,aACjC,SAASC,EAAsBpH,EAAKqH,GACnCA,EAAKC,iBACLtH,EAAIuH,aAAeR,EAAmBS,KAAKR,GAC3ChH,EAAIyH,eAAiBP,EAAqBM,KAAKR,KAG/ChH,EAAIuH,aAAeP,EAAWC,WAAWO,KAAKR,GAC9ChH,EAAIyH,eAAiBT,EAAWG,aAAaK,KAAKR,GAE1D,CClB8C,ICAzBU,EDCfC,WAAcC,GAAAC,EAAAF,EAAAC,GAAA,IAAAE,EAAAC,EAAAJ,GAChB,SAAAA,EAAYK,EAAQC,EAAaC,GAAS,IAAAC,EAIT,OAJSC,OAAAT,IACtCQ,EAAAL,EAAAlI,UAAMoI,IACDC,YAAcA,EACnBE,EAAKD,QAAUA,EACfC,EAAK7I,KAAO,iBAAiB6I,CACjC,CAAC,OAAAE,EAAAV,EAAA,EAAAW,EANwBC,QAQhBC,WAASC,GAAAZ,EAAAW,EAAAC,GAAA,IAAAC,EAAAX,EAAAS,GAOlB,SAAAA,EAAYnB,GAAM,IAAAsB,EAMY,OANZP,OAAAI,IACdG,EAAAD,EAAA9I,KAAAsF,OACK0D,UAAW,EAChBxB,EAAqByB,EAAAF,GAAOtB,GAC5BsB,EAAKtB,KAAOA,EACZsB,EAAKG,MAAQzB,EAAKyB,MAClBH,EAAKI,OAAS1B,EAAK0B,OAAOJ,CAC9B,CAiHC,OAhHDN,EAAAG,EAAA,CAAA,CAAArJ,IAAA,UAAA6J,MASA,SAAQhB,EAAQC,EAAaC,GAEzB,OADAe,EAAAC,EAAAV,EAAA9I,gCAAAE,KAAAsF,KAAmB,QAAS,IAAIyC,EAAeK,EAAQC,EAAaC,IAC7DhD,IACX,GACA,CAAA/F,IAAA,OAAA6J,MAGA,WAGI,OAFA9D,KAAKiE,WAAa,UAClBjE,KAAKkE,SACElE,IACX,GACA,CAAA/F,IAAA,QAAA6J,MAGA,WAKI,MAJwB,YAApB9D,KAAKiE,YAAgD,SAApBjE,KAAKiE,aACtCjE,KAAKmE,UACLnE,KAAKoE,WAEFpE,IACX,GACA,CAAA/F,IAAA,OAAA6J,MAKA,SAAKO,GACuB,SAApBrE,KAAKiE,YACLjE,KAAKsE,MAAMD,EAKnB,GACA,CAAApK,IAAA,SAAA6J,MAKA,WACI9D,KAAKiE,WAAa,OAClBjE,KAAK0D,UAAW,EAChBK,EAAAC,EAAAV,EAAA9I,WAAA,eAAAwF,MAAAtF,KAAAsF,KAAmB,OACvB,GACA,CAAA/F,IAAA,SAAA6J,MAMA,SAAOzJ,GACH,IAAM0D,EAAS1B,EAAahC,EAAM2F,KAAK6D,OAAOtH,YAC9CyD,KAAKuE,SAASxG,EAClB,GACA,CAAA9D,IAAA,WAAA6J,MAKA,SAAS/F,GACLgG,EAAAC,EAAAV,EAAA9I,WAAA,eAAAwF,MAAAtF,KAAAsF,KAAmB,SAAUjC,EACjC,GACA,CAAA9D,IAAA,UAAA6J,MAKA,SAAQU,GACJxE,KAAKiE,WAAa,SAClBF,EAAAC,EAAAV,EAAA9I,WAAA,eAAAwF,MAAAtF,KAAAsF,KAAmB,QAASwE,EAChC,GACA,CAAAvK,IAAA,QAAA6J,MAKA,SAAMW,GAAW,GAAC,CAAAxK,IAAA,YAAA6J,MAClB,SAAUY,GAAoB,IAAZd,EAAKtD,UAAA1D,OAAA,QAAA+H,IAAArE,UAAA,GAAAA,UAAA,GAAG,CAAA,EACtB,OAAQoE,EACJ,MACA1E,KAAK4E,YACL5E,KAAK6E,QACL7E,KAAKmC,KAAK2C,KACV9E,KAAK+E,OAAOnB,EACpB,GAAC,CAAA3J,IAAA,YAAA6J,MACD,WACI,IAAMkB,EAAWhF,KAAKmC,KAAK6C,SAC3B,OAAkC,IAA3BA,EAASC,QAAQ,KAAcD,EAAW,IAAMA,EAAW,GACtE,GAAC,CAAA/K,IAAA,QAAA6J,MACD,WACI,OAAI9D,KAAKmC,KAAK+C,OACRlF,KAAKmC,KAAKgD,QAAUC,OAA0B,MAAnBpF,KAAKmC,KAAK+C,QACjClF,KAAKmC,KAAKgD,QAAqC,KAA3BC,OAAOpF,KAAKmC,KAAK+C,OACpC,IAAMlF,KAAKmC,KAAK+C,KAGhB,EAEf,GAAC,CAAAjL,IAAA,SAAA6J,MACD,SAAOF,GACH,IAAMyB,EEjIP,SAAgBvK,GACnB,IAAIwK,EAAM,GACV,IAAK,IAAIpJ,KAAKpB,EACNA,EAAI8G,eAAe1F,KACfoJ,EAAI1I,SACJ0I,GAAO,KACXA,GAAOC,mBAAmBrJ,GAAK,IAAMqJ,mBAAmBzK,EAAIoB,KAGpE,OAAOoJ,CACX,CFuH6BjH,CAAOuF,GAC5B,OAAOyB,EAAazI,OAAS,IAAMyI,EAAe,EACtD,KAAC/B,CAAA,EA/H0B5D,GCVzB8F,EAAW,mEAAmE9J,MAAM,IAAKkB,EAAS,GAAI6I,EAAM,CAAA,EAC9GC,EAAO,EAAGxJ,EAAI,EAQX,SAASmC,EAAOsH,GACnB,IAAIxH,EAAU,GACd,GACIA,EAAUqH,EAASG,EAAM/I,GAAUuB,EACnCwH,EAAMC,KAAKC,MAAMF,EAAM/I,SAClB+I,EAAM,GACf,OAAOxH,CACX,CAqBO,SAAS2H,IACZ,IAAMC,EAAM1H,GAAQ,IAAI2H,MACxB,OAAID,IAAQvD,GACDkD,EAAO,EAAGlD,EAAOuD,GACrBA,EAAM,IAAM1H,EAAOqH,IAC9B,CAIA,KAAOxJ,EAAIU,EAAQV,IACfuJ,EAAID,EAAStJ,IAAMA,EEhDvB,IAAI4H,IAAQ,EACZ,IACIA,GAAkC,oBAAnBmC,gBACX,oBAAqB,IAAIA,cACjC,CACA,MAAOC,GAEH,CAEG,IAAMC,GAAUrC,GCPhB,SAASsC,GAAIjE,GAChB,IAAMkE,EAAUlE,EAAKkE,QAErB,IACI,GAAI,oBAAuBJ,kBAAoBI,GAAWF,IACtD,OAAO,IAAIF,cAEnB,CACA,MAAOK,GAAK,CACZ,IAAKD,EACD,IACI,OAAO,IAAIvE,EAAW,CAAC,UAAUyE,OAAO,UAAUC,KAAK,OAAM,oBACjE,CACA,MAAOF,GAAK,CAEpB,CCXA,SAASG,KAAU,CACnB,IAAMC,GAIK,MAHK,IAAIT,GAAe,CAC3BI,SAAS,IAEMM,aAEVC,YAAOC,GAAAlE,EAAAiE,EAAAC,GAAA,IAAAjE,EAAAC,EAAA+D,GAOhB,SAAAA,EAAYzE,GAAM,IAAAc,EAGd,GAHcC,OAAA0D,IACd3D,EAAAL,EAAAlI,UAAMyH,IACD2E,SAAU,EACS,oBAAbC,SAA0B,CACjC,IAAMC,EAAQ,WAAaD,SAASE,SAChC/B,EAAO6B,SAAS7B,KAEfA,IACDA,EAAO8B,EAAQ,MAAQ,MAE3B/D,EAAKiE,GACoB,oBAAbH,UACJ5E,EAAK6C,WAAa+B,SAAS/B,UAC3BE,IAAS/C,EAAK+C,IAC1B,CAIA,IAAMiC,EAAchF,GAAQA,EAAKgF,YAIhC,OAHDlE,EAAK/H,eAAiBwL,KAAYS,EAC9BlE,EAAKd,KAAKiF,kBACVnE,EAAKoE,eAAYC,GACpBrE,CACL,CAgLC,OAhLAE,EAAAyD,EAAA,CAAA,CAAA3M,IAAA,OAAAsN,IACD,WACI,MAAO,SACX,GACA,CAAAtN,IAAA,SAAA6J,MAMA,WACI9D,KAAKwH,MACT,GACA,CAAAvN,IAAA,QAAA6J,MAMA,SAAMW,GAAS,IAAAhB,EAAAzD,KACXA,KAAKiE,WAAa,UAClB,IAAMwD,EAAQ,WACVhE,EAAKQ,WAAa,SAClBQ,KAEJ,GAAIzE,KAAK8G,UAAY9G,KAAK0D,SAAU,CAChC,IAAIgE,EAAQ,EACR1H,KAAK8G,UACLY,IACA1H,KAAKG,KAAK,gBAAgB,aACpBuH,GAASD,GACf,KAECzH,KAAK0D,WACNgE,IACA1H,KAAKG,KAAK,SAAS,aACbuH,GAASD,GACf,IAER,MAEIA,GAER,GACA,CAAAxN,IAAA,OAAA6J,MAKA,WACI9D,KAAK8G,SAAU,EACf9G,KAAK2H,SACL3H,KAAKgB,aAAa,OACtB,GACA,CAAA/G,IAAA,SAAA6J,MAKA,SAAOzJ,GAAM,IAAAuN,EAAA5H,MVpFK,SAAC6H,EAAgBtL,GAGnC,IAFA,IAAMuL,EAAiBD,EAAenM,MAAM+B,GACtC4G,EAAU,GACPnI,EAAI,EAAGA,EAAI4L,EAAelL,OAAQV,IAAK,CAC5C,IAAM6L,EAAgB1L,EAAayL,EAAe5L,GAAIK,GAEtD,GADA8H,EAAQnE,KAAK6H,GACc,UAAvBA,EAAc3N,KACd,KAER,CACA,OAAOiK,CACX,EUwFQ2D,CAAc3N,EAAM2F,KAAK6D,OAAOtH,YAAYvC,SAd3B,SAAC+D,GAMd,GAJI,YAAc6J,EAAK3D,YAA8B,SAAhBlG,EAAO3D,MACxCwN,EAAKK,SAGL,UAAYlK,EAAO3D,KAEnB,OADAwN,EAAKxD,QAAQ,CAAErB,YAAa,oCACrB,EAGX6E,EAAKrD,SAASxG,MAKd,WAAaiC,KAAKiE,aAElBjE,KAAK8G,SAAU,EACf9G,KAAKgB,aAAa,gBACd,SAAWhB,KAAKiE,YAChBjE,KAAKwH,OAKjB,GACA,CAAAvN,IAAA,UAAA6J,MAKA,WAAU,IAAAoE,EAAAlI,KACAmI,EAAQ,WACVD,EAAK5D,MAAM,CAAC,CAAElK,KAAM,YAEpB,SAAW4F,KAAKiE,WAChBkE,IAKAnI,KAAKG,KAAK,OAAQgI,EAE1B,GACA,CAAAlO,IAAA,QAAA6J,MAMA,SAAMO,GAAS,IAAA+D,EAAApI,KACXA,KAAK0D,UAAW,EVxJF,SAACW,EAASlJ,GAE5B,IAAMyB,EAASyH,EAAQzH,OACjBkL,EAAiB,IAAI/G,MAAMnE,GAC7ByL,EAAQ,EACZhE,EAAQrK,SAAQ,SAAC+D,EAAQ7B,GAErBlB,EAAa+C,GAAQ,GAAO,SAAAzB,GACxBwL,EAAe5L,GAAKI,IACd+L,IAAUzL,GACZzB,EAAS2M,EAAetB,KAAK/I,GAErC,GACJ,GACJ,CU2IQ6K,CAAcjE,GAAS,SAAChK,GACpB+N,EAAKG,QAAQlO,GAAM,WACf+N,EAAK1E,UAAW,EAChB0E,EAAKpH,aAAa,QACtB,GACJ,GACJ,GACA,CAAA/G,IAAA,MAAA6J,MAKA,WACI,IAAMY,EAAS1E,KAAKmC,KAAKgD,OAAS,QAAU,OACtCvB,EAAQ5D,KAAK4D,OAAS,GAQ5B,OANI,IAAU5D,KAAKmC,KAAKqG,oBACpB5E,EAAM5D,KAAKmC,KAAKsG,gBAAkB3C,KAEjC9F,KAAK9E,gBAAmB0I,EAAM8E,MAC/B9E,EAAM+E,IAAM,GAET3I,KAAK4I,UAAUlE,EAAQd,EAClC,GACA,CAAA3J,IAAA,UAAA6J,MAMA,WAAmB,IAAX3B,EAAI7B,UAAA1D,OAAA,QAAA+H,IAAArE,UAAA,GAAAA,UAAA,GAAG,CAAA,EAEX,OADAuI,EAAc1G,EAAM,CAAE+E,GAAIlH,KAAKkH,GAAIG,UAAWrH,KAAKqH,WAAarH,KAAKmC,MAC9D,IAAI2G,GAAQ9I,KAAK+I,MAAO5G,EACnC,GACA,CAAAlI,IAAA,UAAA6J,MAOA,SAAQzJ,EAAM0F,GAAI,IAAAiJ,EAAAhJ,KACRiJ,EAAMjJ,KAAKkJ,QAAQ,CACrBC,OAAQ,OACR9O,KAAMA,IAEV4O,EAAIrJ,GAAG,UAAWG,GAClBkJ,EAAIrJ,GAAG,SAAS,SAACwJ,EAAWpG,GACxBgG,EAAKK,QAAQ,iBAAkBD,EAAWpG,EAC9C,GACJ,GACA,CAAA/I,IAAA,SAAA6J,MAKA,WAAS,IAAAwF,EAAAtJ,KACCiJ,EAAMjJ,KAAKkJ,UACjBD,EAAIrJ,GAAG,OAAQI,KAAKuJ,OAAOjH,KAAKtC,OAChCiJ,EAAIrJ,GAAG,SAAS,SAACwJ,EAAWpG,GACxBsG,EAAKD,QAAQ,iBAAkBD,EAAWpG,EAC9C,IACAhD,KAAKwJ,QAAUP,CACnB,KAACrC,CAAA,EA9MwBtD,GAgNhBwF,YAAOvF,GAAAZ,EAAAmG,EAAAvF,GAAA,IAAAC,EAAAX,EAAAiG,GAOhB,SAAAA,EAAYC,EAAK5G,GAAM,IAAAsH,EAOL,OAPKvG,OAAA4F,GAEnB5G,EAAqByB,EADrB8F,EAAAjG,EAAA9I,KAAAsF,OAC4BmC,GAC5BsH,EAAKtH,KAAOA,EACZsH,EAAKN,OAAShH,EAAKgH,QAAU,MAC7BM,EAAKV,IAAMA,EACXU,EAAKpP,UAAOsK,IAAcxC,EAAK9H,KAAO8H,EAAK9H,KAAO,KAClDoP,EAAK5P,SAAS4P,CAClB,CA8HC,OA7HDtG,EAAA2F,EAAA,CAAA,CAAA7O,IAAA,SAAA6J,MAKA,WAAS,IACD4F,EADCC,EAAA3J,KAECmC,EAAOZ,EAAKvB,KAAKmC,KAAM,QAAS,MAAO,MAAO,aAAc,OAAQ,KAAM,UAAW,qBAAsB,aACjHA,EAAKkE,UAAYrG,KAAKmC,KAAK+E,GAC3B,IAAM0C,EAAO5J,KAAK4J,IAAM,IAAI3D,GAAe9D,GAC3C,IACIyH,EAAIC,KAAK7J,KAAKmJ,OAAQnJ,KAAK+I,KAAK,GAChC,IACI,GAAI/I,KAAKmC,KAAK2H,aAEV,IAAK,IAAI5N,KADT0N,EAAIG,uBAAyBH,EAAIG,uBAAsB,GACzC/J,KAAKmC,KAAK2H,aAChB9J,KAAKmC,KAAK2H,aAAalI,eAAe1F,IACtC0N,EAAII,iBAAiB9N,EAAG8D,KAAKmC,KAAK2H,aAAa5N,GAI/D,CACA,MAAOoK,GAAK,CACZ,GAAI,SAAWtG,KAAKmJ,OAChB,IACIS,EAAII,iBAAiB,eAAgB,2BACzC,CACA,MAAO1D,GAAK,CAEhB,IACIsD,EAAII,iBAAiB,SAAU,MACnC,CACA,MAAO1D,GAAK,CACmB,QAA9BoD,EAAK1J,KAAKmC,KAAKkF,iBAA8B,IAAPqC,GAAyBA,EAAGO,WAAWL,GAE1E,oBAAqBA,IACrBA,EAAIxC,gBAAkBpH,KAAKmC,KAAKiF,iBAEhCpH,KAAKmC,KAAK+H,iBACVN,EAAIO,QAAUnK,KAAKmC,KAAK+H,gBAE5BN,EAAIQ,mBAAqB,WACrB,IAAIV,EACmB,IAAnBE,EAAI3F,aAC2B,QAA9ByF,EAAKC,EAAKxH,KAAKkF,iBAA8B,IAAPqC,GAAyBA,EAAGW,aAAaT,IAEhF,IAAMA,EAAI3F,aAEV,MAAQ2F,EAAIU,QAAU,OAASV,EAAIU,OACnCX,EAAKY,SAKLZ,EAAKtH,cAAa,WACdsH,EAAKN,QAA8B,iBAAfO,EAAIU,OAAsBV,EAAIU,OAAS,EAC9D,GAAE,KAGXV,EAAIY,KAAKxK,KAAK3F,KACjB,CACD,MAAOiM,GAOH,YAHAtG,KAAKqC,cAAa,WACdsH,EAAKN,QAAQ/C,EAChB,GAAE,EAEP,CACwB,oBAAbmE,WACPzK,KAAK0K,MAAQ5B,EAAQ6B,gBACrB7B,EAAQ8B,SAAS5K,KAAK0K,OAAS1K,KAEvC,GACA,CAAA/F,IAAA,UAAA6J,MAKA,SAAQoC,GACJlG,KAAKgB,aAAa,QAASkF,EAAKlG,KAAK4J,KACrC5J,KAAK6K,SAAQ,EACjB,GACA,CAAA5Q,IAAA,UAAA6J,MAKA,SAAQgH,GACJ,QAAI,IAAuB9K,KAAK4J,KAAO,OAAS5J,KAAK4J,IAArD,CAIA,GADA5J,KAAK4J,IAAIQ,mBAAqB3D,GAC1BqE,EACA,IACI9K,KAAK4J,IAAImB,OACb,CACA,MAAOzE,GAAK,CAEQ,oBAAbmE,iBACA3B,EAAQ8B,SAAS5K,KAAK0K,OAEjC1K,KAAK4J,IAAM,IAXX,CAYJ,GACA,CAAA3P,IAAA,SAAA6J,MAKA,WACI,IAAMzJ,EAAO2F,KAAK4J,IAAIoB,aACT,OAAT3Q,IACA2F,KAAKgB,aAAa,OAAQ3G,GAC1B2F,KAAKgB,aAAa,WAClBhB,KAAK6K,UAEb,GACA,CAAA5Q,IAAA,QAAA6J,MAKA,WACI9D,KAAK6K,SACT,KAAC/B,CAAA,EA7IwBpJ,GAsJ7B,GAPAoJ,GAAQ6B,cAAgB,EACxB7B,GAAQ8B,SAAW,CAAA,EAMK,oBAAbH,SAEP,GAA2B,mBAAhBQ,YAEPA,YAAY,WAAYC,SAEvB,GAAgC,mBAArBrL,iBAAiC,CAE7CA,iBADyB,eAAgBiC,EAAa,WAAa,SAChCoJ,IAAe,EACtD,CAEJ,SAASA,KACL,IAAK,IAAIhP,KAAK4M,GAAQ8B,SACd9B,GAAQ8B,SAAShJ,eAAe1F,IAChC4M,GAAQ8B,SAAS1O,GAAG6O,OAGhC,CCpYO,IAAMI,GACqC,mBAAZC,SAAqD,mBAApBA,QAAQC,QAEhE,SAAC3K,GAAE,OAAK0K,QAAQC,UAAUnN,KAAKwC,EAAG,EAGlC,SAACA,EAAI2B,GAAY,OAAKA,EAAa3B,EAAI,EAAE,EAG3C4K,GAAYxJ,EAAWwJ,WAAaxJ,EAAWyJ,aCJtDC,GAAqC,oBAAdC,WACI,iBAAtBA,UAAUC,SACmB,gBAApCD,UAAUC,QAAQC,cACTC,YAAE/E,GAAAlE,EAAAiJ,EAAA/E,GAAA,IAAAjE,EAAAC,EAAA+I,GAOX,SAAAA,EAAYzJ,GAAM,IAAAc,EAE0B,OAF1BC,OAAA0I,IACd3I,EAAAL,EAAAlI,UAAMyH,IACDjH,gBAAkBiH,EAAKgF,YAAYlE,CAC5C,CAmIC,OAnIAE,EAAAyI,EAAA,CAAA,CAAA3R,IAAA,OAAAsN,IACD,WACI,MAAO,WACX,GAAC,CAAAtN,IAAA,SAAA6J,MACD,WACI,GAAK9D,KAAK6L,QAAV,CAIA,IAAM9C,EAAM/I,KAAK+I,MACX+C,EAAY9L,KAAKmC,KAAK2J,UAEtB3J,EAAOqJ,GACP,CAAA,EACAjK,EAAKvB,KAAKmC,KAAM,QAAS,oBAAqB,MAAO,MAAO,aAAc,OAAQ,KAAM,UAAW,qBAAsB,eAAgB,kBAAmB,SAAU,aAAc,SAAU,uBAChMnC,KAAKmC,KAAK2H,eACV3H,EAAK4J,QAAU/L,KAAKmC,KAAK2H,cAE7B,IACI9J,KAAKgM,GACyBR,GAIpB,IAAIF,GAAUvC,EAAK+C,EAAW3J,GAH9B2J,EACI,IAAIR,GAAUvC,EAAK+C,GACnB,IAAIR,GAAUvC,EAE/B,CACD,MAAO7C,GACH,OAAOlG,KAAKgB,aAAa,QAASkF,EACtC,CACAlG,KAAKgM,GAAGzP,WAAayD,KAAK6D,OAAOtH,WACjCyD,KAAKiM,mBAtBL,CAuBJ,GACA,CAAAhS,IAAA,oBAAA6J,MAKA,WAAoB,IAAAL,EAAAzD,KAChBA,KAAKgM,GAAGE,OAAS,WACTzI,EAAKtB,KAAKgK,WACV1I,EAAKuI,GAAGI,QAAQC,QAEpB5I,EAAKwE,UAETjI,KAAKgM,GAAGM,QAAU,SAACC,GAAU,OAAK9I,EAAKW,QAAQ,CAC3CrB,YAAa,8BACbC,QAASuJ,GACX,EACFvM,KAAKgM,GAAGQ,UAAY,SAACC,GAAE,OAAKhJ,EAAK8F,OAAOkD,EAAGpS,KAAK,EAChD2F,KAAKgM,GAAGU,QAAU,SAACpG,GAAC,OAAK7C,EAAK4F,QAAQ,kBAAmB/C,EAAE,CAC/D,GAAC,CAAArM,IAAA,QAAA6J,MACD,SAAMO,GAAS,IAAAuD,EAAA5H,KACXA,KAAK0D,UAAW,EAGhB,IADA,IAAAiJ,EAAAA,WAEI,IAAM5O,EAASsG,EAAQnI,GACjB0Q,EAAa1Q,IAAMmI,EAAQzH,OAAS,EAC1C5B,EAAa+C,EAAQ6J,EAAK1M,gBAAgB,SAACb,GAmBvC,IAGQuN,EAAKoE,GAAGxB,KAAKnQ,EAKrB,CACA,MAAOiM,GACP,CACIsG,GAGAzB,IAAS,WACLvD,EAAKlE,UAAW,EAChBkE,EAAK5G,aAAa,QACtB,GAAG4G,EAAKvF,aAEhB,KAzCKnG,EAAI,EAAGA,EAAImI,EAAQzH,OAAQV,IAAGyQ,GA2C3C,GAAC,CAAA1S,IAAA,UAAA6J,MACD,gBAC2B,IAAZ9D,KAAKgM,KACZhM,KAAKgM,GAAG7D,QACRnI,KAAKgM,GAAK,KAElB,GACA,CAAA/R,IAAA,MAAA6J,MAKA,WACI,IAAMY,EAAS1E,KAAKmC,KAAKgD,OAAS,MAAQ,KACpCvB,EAAQ5D,KAAK4D,OAAS,GAS5B,OAPI5D,KAAKmC,KAAKqG,oBACV5E,EAAM5D,KAAKmC,KAAKsG,gBAAkB3C,KAGjC9F,KAAK9E,iBACN0I,EAAM+E,IAAM,GAET3I,KAAK4I,UAAUlE,EAAQd,EAClC,GACA,CAAA3J,IAAA,QAAA6J,MAMA,WACI,QAASwH,EACb,KAACM,CAAA,EA7ImBtI,GCNXuJ,YAAEhG,GAAAlE,EAAAkK,EAAAhG,GAAA,IAAAjE,EAAAC,EAAAgK,GAAA,SAAAA,IAAA,OAAA3J,OAAA2J,GAAAjK,EAAAvC,MAAAL,KAAAM,UAAA,CAkEV,OAlEU6C,EAAA0J,EAAA,CAAA,CAAA5S,IAAA,OAAAsN,IACX,WACI,MAAO,cACX,GAAC,CAAAtN,IAAA,SAAA6J,MACD,WAAS,IAAAb,EAAAjD,KAEuB,mBAAjB8M,eAIX9M,KAAK+M,UAAY,IAAID,aAAa9M,KAAK4I,UAAU,SAAU5I,KAAKmC,KAAK6K,iBAAiBhN,KAAKiN,OAC3FjN,KAAK+M,UAAUG,OACVhP,MAAK,WACN+E,EAAKmB,SACT,IAAE,OACS,SAAC8B,GACRjD,EAAKoG,QAAQ,qBAAsBnD,EACvC,IAEAlG,KAAK+M,UAAUI,MAAMjP,MAAK,WACtB+E,EAAK8J,UAAUK,4BAA4BlP,MAAK,SAACmP,GAC7C,IAAMC,Eb8Df,SAAmCC,EAAYhR,GAC7CH,IACDA,EAAe,IAAIoR,aAEvB,IAAMvO,EAAS,GACXwO,EAAQ,EACRC,GAAkB,EAClBC,GAAW,EACf,OAAO,IAAI9P,gBAAgB,CACvBC,UAASA,SAACsB,EAAOpB,GAEb,IADAiB,EAAOiB,KAAKd,KACC,CACT,GAAc,IAAVqO,EAA+B,CAC/B,GAAIzO,EAAYC,GAAU,EACtB,MAEJ,IAAMV,EAASc,EAAaJ,EAAQ,GACpC0O,EAAkC,MAAV,IAAZpP,EAAO,IACnBmP,EAA6B,IAAZnP,EAAO,GAEpBkP,EADAC,EAAiB,IACT,EAEgB,MAAnBA,EACG,EAGA,CAEhB,MACK,GAAc,IAAVD,EAA2C,CAChD,GAAIzO,EAAYC,GAAU,EACtB,MAEJ,IAAM2O,EAAcvO,EAAaJ,EAAQ,GACzCyO,EAAiB,IAAIjP,SAASmP,EAAY7S,OAAQ6S,EAAY9R,WAAY8R,EAAYhR,QAAQiR,UAAU,GACxGJ,EAAQ,CACZ,MACK,GAAc,IAAVA,EAA2C,CAChD,GAAIzO,EAAYC,GAAU,EACtB,MAEJ,IAAM2O,EAAcvO,EAAaJ,EAAQ,GACnCN,EAAO,IAAIF,SAASmP,EAAY7S,OAAQ6S,EAAY9R,WAAY8R,EAAYhR,QAC5EkR,EAAInP,EAAKoP,UAAU,GACzB,GAAID,EAAIlI,KAAKoI,IAAI,EAAG,IAAW,EAAG,CAE9BhQ,EAAWe,QAAQ5E,GACnB,KACJ,CACAuT,EAAiBI,EAAIlI,KAAKoI,IAAI,EAAG,IAAMrP,EAAKoP,UAAU,GACtDN,EAAQ,CACZ,KACK,CACD,GAAIzO,EAAYC,GAAUyO,EACtB,MAEJ,IAAMrT,EAAOgF,EAAaJ,EAAQyO,GAClC1P,EAAWe,QAAQ1C,EAAasR,EAAWtT,EAAO+B,EAAaoB,OAAOnD,GAAOkC,IAC7EkR,EAAQ,CACZ,CACA,GAAuB,IAAnBC,GAAwBA,EAAiBH,EAAY,CACrDvP,EAAWe,QAAQ5E,GACnB,KACJ,CACJ,CACJ,GAER,CajIsC8T,CAA0B7I,OAAO8I,iBAAkBjL,EAAKY,OAAOtH,YAC/E4R,EAASd,EAAOe,SAASC,YAAYf,GAAegB,YACpDC,EAAgB3Q,IACtB2Q,EAAcH,SAASI,OAAOnB,EAAO3J,UACrCT,EAAKwL,OAASF,EAAc7K,SAASgL,aACxB,SAAPC,IACFR,EACKQ,OACAzQ,MAAK,SAAAjD,GAAqB,IAAlB2T,EAAI3T,EAAJ2T,KAAM9K,EAAK7I,EAAL6I,MACX8K,IAGJ3L,EAAKsB,SAAST,GACd6K,IACH,WACU,SAACzI,GACX,IAELyI,GACA,IAAM5Q,EAAS,CAAE3D,KAAM,QACnB6I,EAAKW,MAAM8E,MACX3K,EAAO1D,KAAI,WAAAkM,OAActD,EAAKW,MAAM8E,IAAO,OAE/CzF,EAAKwL,OAAOnK,MAAMvG,GAAQG,MAAK,WAAA,OAAM+E,EAAKgF,WAC9C,GACJ,IACJ,GAAC,CAAAhO,IAAA,QAAA6J,MACD,SAAMO,GAAS,IAAAZ,EAAAzD,KACXA,KAAK0D,UAAW,EAChB,IADsB,IAAAiJ,EAAAA,WAElB,IAAM5O,EAASsG,EAAQnI,GACjB0Q,EAAa1Q,IAAMmI,EAAQzH,OAAS,EAC1C6G,EAAKgL,OAAOnK,MAAMvG,GAAQG,MAAK,WACvB0O,GACAzB,IAAS,WACL1H,EAAKC,UAAW,EAChBD,EAAKzC,aAAa,QACtB,GAAGyC,EAAKpB,aAEhB,KAVKnG,EAAI,EAAGA,EAAImI,EAAQzH,OAAQV,IAAGyQ,GAY3C,GAAC,CAAA1S,IAAA,UAAA6J,MACD,WACI,IAAI4F,EACsB,QAAzBA,EAAK1J,KAAK+M,iBAA8B,IAAPrD,GAAyBA,EAAGvB,OAClE,KAAC0E,CAAA,EAlEmBvJ,GCAXuL,GAAa,CACtBC,UAAWlD,GACXmD,aAAclC,GACd/F,QAASF,ICaPoI,GAAK,sPACLC,GAAQ,CACV,SAAU,WAAY,YAAa,WAAY,OAAQ,WAAY,OAAQ,OAAQ,WAAY,OAAQ,YAAa,OAAQ,QAAS,UAElI,SAASC,GAAM5J,GAClB,IAAM6J,EAAM7J,EAAK8J,EAAI9J,EAAIL,QAAQ,KAAMqB,EAAIhB,EAAIL,QAAQ,MAC7C,GAANmK,IAAiB,GAAN9I,IACXhB,EAAMA,EAAI3I,UAAU,EAAGyS,GAAK9J,EAAI3I,UAAUyS,EAAG9I,GAAG+I,QAAQ,KAAM,KAAO/J,EAAI3I,UAAU2J,EAAGhB,EAAI1I,SAG9F,IADA,IAwBmBgH,EACbvJ,EAzBFiV,EAAIN,GAAGO,KAAKjK,GAAO,IAAKyD,EAAM,CAAE,EAAE7M,EAAI,GACnCA,KACH6M,EAAIkG,GAAM/S,IAAMoT,EAAEpT,IAAM,GAU5B,OARU,GAANkT,IAAiB,GAAN9I,IACXyC,EAAIyG,OAASL,EACbpG,EAAI0G,KAAO1G,EAAI0G,KAAK9S,UAAU,EAAGoM,EAAI0G,KAAK7S,OAAS,GAAGyS,QAAQ,KAAM,KACpEtG,EAAI2G,UAAY3G,EAAI2G,UAAUL,QAAQ,IAAK,IAAIA,QAAQ,IAAK,IAAIA,QAAQ,KAAM,KAC9EtG,EAAI4G,SAAU,GAElB5G,EAAI6G,UAIR,SAAmB9U,EAAKgK,GACpB,IAAM+K,EAAO,WAAYC,EAAQhL,EAAKuK,QAAQQ,EAAM,KAAKnU,MAAM,KACvC,KAApBoJ,EAAKrF,MAAM,EAAG,IAA6B,IAAhBqF,EAAKlI,QAChCkT,EAAMlP,OAAO,EAAG,GAEE,KAAlBkE,EAAKrF,OAAO,IACZqQ,EAAMlP,OAAOkP,EAAMlT,OAAS,EAAG,GAEnC,OAAOkT,CACX,CAboBF,CAAU7G,EAAKA,EAAU,MACzCA,EAAIgH,UAaenM,EAbUmF,EAAW,MAclC1O,EAAO,CAAA,EACbuJ,EAAMyL,QAAQ,6BAA6B,SAAUW,EAAIC,EAAIC,GACrDD,IACA5V,EAAK4V,GAAMC,EAEnB,IACO7V,GAnBA0O,CACX,CClCaoH,IAAAA,YAAM5M,GAAAZ,EAAAwN,EAAA5M,GAAA,IAAAX,EAAAC,EAAAsN,GAOf,SAAAA,EAAYpH,GAAgB,IAAA9F,EAAXd,EAAI7B,UAAA1D,OAAA,QAAA+H,IAAArE,UAAA,GAAAA,UAAA,GAAG,CAAA,EAgGR,OAhGU4C,OAAAiN,IACtBlN,EAAAL,EAAAlI,KAAAsF,OACKzD,WLJoB,cKKzB0G,EAAKmN,YAAc,GACfrH,GAAO,WAAQsH,EAAYtH,KAC3B5G,EAAO4G,EACPA,EAAM,MAENA,GACAA,EAAMmG,GAAMnG,GACZ5G,EAAK6C,SAAW+D,EAAI0G,KACpBtN,EAAKgD,OAA0B,UAAjB4D,EAAI9B,UAAyC,QAAjB8B,EAAI9B,SAC9C9E,EAAK+C,KAAO6D,EAAI7D,KACZ6D,EAAInF,QACJzB,EAAKyB,MAAQmF,EAAInF,QAEhBzB,EAAKsN,OACVtN,EAAK6C,SAAWkK,GAAM/M,EAAKsN,MAAMA,MAErCvN,EAAqByB,EAAAV,GAAOd,GAC5Bc,EAAKkC,OACD,MAAQhD,EAAKgD,OACPhD,EAAKgD,OACe,oBAAb4B,UAA4B,WAAaA,SAASE,SAC/D9E,EAAK6C,WAAa7C,EAAK+C,OAEvB/C,EAAK+C,KAAOjC,EAAKkC,OAAS,MAAQ,MAEtClC,EAAK+B,SACD7C,EAAK6C,WACoB,oBAAb+B,SAA2BA,SAAS/B,SAAW,aAC/D/B,EAAKiC,KACD/C,EAAK+C,OACoB,oBAAb6B,UAA4BA,SAAS7B,KACvC6B,SAAS7B,KACTjC,EAAKkC,OACD,MACA,MAClBlC,EAAK4L,WAAa1M,EAAK0M,YAAc,CACjC,UACA,YACA,gBAEJ5L,EAAKmN,YAAc,GACnBnN,EAAKqN,cAAgB,EACrBrN,EAAKd,KAAO0G,EAAc,CACtB/D,KAAM,aACNyL,OAAO,EACPnJ,iBAAiB,EACjBoJ,SAAS,EACT/H,eAAgB,IAChBgI,iBAAiB,EACjBC,kBAAkB,EAClBC,oBAAoB,EACpBC,kBAAmB,CACfC,UAAW,MAEf7D,iBAAkB,CAAE,EACpB8D,qBAAqB,GACtB3O,GACHc,EAAKd,KAAK2C,KACN7B,EAAKd,KAAK2C,KAAKuK,QAAQ,MAAO,KACzBpM,EAAKd,KAAKuO,iBAAmB,IAAM,IACb,iBAApBzN,EAAKd,KAAKyB,QACjBX,EAAKd,KAAKyB,MTrDf,SAAgBmN,GAGnB,IAFA,IAAIC,EAAM,CAAA,EACNC,EAAQF,EAAGrV,MAAM,KACZQ,EAAI,EAAGgV,EAAID,EAAMrU,OAAQV,EAAIgV,EAAGhV,IAAK,CAC1C,IAAIiV,EAAOF,EAAM/U,GAAGR,MAAM,KAC1BsV,EAAII,mBAAmBD,EAAK,KAAOC,mBAAmBD,EAAK,GAC/D,CACA,OAAOH,CACX,CS6C8BxT,CAAOyF,EAAKd,KAAKyB,QAGvCX,EAAKoO,GAAK,KACVpO,EAAKqO,SAAW,KAChBrO,EAAKsO,aAAe,KACpBtO,EAAKuO,YAAc,KAEnBvO,EAAKwO,iBAAmB,KACQ,mBAArB5R,mBACHoD,EAAKd,KAAK2O,sBAIV7N,EAAKyO,0BAA4B,WACzBzO,EAAK8J,YAEL9J,EAAK8J,UAAUvM,qBACfyC,EAAK8J,UAAU5E,UAGvBtI,iBAAiB,eAAgBoD,EAAKyO,2BAA2B,IAE/C,cAAlBzO,EAAK+B,WACL/B,EAAK0O,qBAAuB,WACxB1O,EAAKmB,QAAQ,kBAAmB,CAC5BrB,YAAa,6BAGrBlD,iBAAiB,UAAWoD,EAAK0O,sBAAsB,KAG/D1O,EAAK4G,OAAO5G,CAChB,CAgeC,OA/dDE,EAAAgN,EAAA,CAAA,CAAAlW,IAAA,kBAAA6J,MAOA,SAAgBmJ,GACZ,IAAMrJ,EAAQiF,EAAc,CAAA,EAAI7I,KAAKmC,KAAKyB,OAE1CA,EAAMgO,IhBgCU,EgB9BhBhO,EAAMmJ,UAAYE,EAEdjN,KAAKqR,KACLzN,EAAM8E,IAAM1I,KAAKqR,IACrB,IAAMlP,EAAO0G,EAAc,GAAI7I,KAAKmC,KAAM,CACtCyB,MAAAA,EACAC,OAAQ7D,KACRgF,SAAUhF,KAAKgF,SACfG,OAAQnF,KAAKmF,OACbD,KAAMlF,KAAKkF,MACZlF,KAAKmC,KAAK6K,iBAAiBC,IAC9B,OAAO,IAAI4B,GAAW5B,GAAM9K,EAChC,GACA,CAAAlI,IAAA,OAAA6J,MAKA,WAAO,IACCiJ,EADDtJ,EAAAzD,KAEH,GAAIA,KAAKmC,KAAKsO,iBACVN,EAAO0B,wBACmC,IAA1C7R,KAAK6O,WAAW5J,QAAQ,aACxB8H,EAAY,gBAEX,IAAI,IAAM/M,KAAK6O,WAAWjS,OAK3B,YAHAoD,KAAKqC,cAAa,WACdoB,EAAKzC,aAAa,QAAS,0BAC9B,GAAE,GAIH+L,EAAY/M,KAAK6O,WAAW,EAChC,CACA7O,KAAKiE,WAAa,UAElB,IACI8I,EAAY/M,KAAK8R,gBAAgB/E,EACpC,CACD,MAAOzG,GAGH,OAFAtG,KAAK6O,WAAWtP,aAChBS,KAAK6J,MAET,CACAkD,EAAUlD,OACV7J,KAAK+R,aAAahF,EACtB,GACA,CAAA9S,IAAA,eAAA6J,MAKA,SAAaiJ,GAAW,IAAAnF,EAAA5H,KAChBA,KAAK+M,WACL/M,KAAK+M,UAAUvM,qBAGnBR,KAAK+M,UAAYA,EAEjBA,EACKnN,GAAG,QAASI,KAAKgS,QAAQ1P,KAAKtC,OAC9BJ,GAAG,SAAUI,KAAKuE,SAASjC,KAAKtC,OAChCJ,GAAG,QAASI,KAAKqJ,QAAQ/G,KAAKtC,OAC9BJ,GAAG,SAAS,SAACkD,GAAM,OAAK8E,EAAKxD,QAAQ,kBAAmBtB,KACjE,GACA,CAAA7I,IAAA,QAAA6J,MAMA,SAAMmJ,GAAM,IAAA/E,EAAAlI,KACJ+M,EAAY/M,KAAK8R,gBAAgB7E,GACjCgF,GAAS,EACb9B,EAAO0B,uBAAwB,EAC/B,IAAMK,EAAkB,WAChBD,IAEJlF,EAAUvC,KAAK,CAAC,CAAEpQ,KAAM,OAAQC,KAAM,WACtC0S,EAAU5M,KAAK,UAAU,SAACgS,GACtB,IAAIF,EAEJ,GAAI,SAAWE,EAAI/X,MAAQ,UAAY+X,EAAI9X,KAAM,CAG7C,GAFA6N,EAAKkK,WAAY,EACjBlK,EAAKlH,aAAa,YAAa+L,IAC1BA,EACD,OACJoD,EAAO0B,sBAAwB,cAAgB9E,EAAUE,KACzD/E,EAAK6E,UAAUtF,OAAM,WACbwK,GAEA,WAAa/J,EAAKjE,aAEtB4G,IACA3C,EAAK6J,aAAahF,GAClBA,EAAUvC,KAAK,CAAC,CAAEpQ,KAAM,aACxB8N,EAAKlH,aAAa,UAAW+L,GAC7BA,EAAY,KACZ7E,EAAKkK,WAAY,EACjBlK,EAAKmK,QACT,GACJ,KACK,CACD,IAAMnM,EAAM,IAAI7C,MAAM,eAEtB6C,EAAI6G,UAAYA,EAAUE,KAC1B/E,EAAKlH,aAAa,eAAgBkF,EACtC,CACJ,MAEJ,SAASoM,IACDL,IAGJA,GAAS,EACTpH,IACAkC,EAAU5E,QACV4E,EAAY,KAChB,CAEA,IAAML,EAAU,SAACxG,GACb,IAAMqM,EAAQ,IAAIlP,MAAM,gBAAkB6C,GAE1CqM,EAAMxF,UAAYA,EAAUE,KAC5BqF,IACApK,EAAKlH,aAAa,eAAgBuR,IAEtC,SAASC,IACL9F,EAAQ,mBACZ,CAEA,SAASJ,IACLI,EAAQ,gBACZ,CAEA,SAAS+F,EAAUC,GACX3F,GAAa2F,EAAGzF,OAASF,EAAUE,MACnCqF,GAER,CAEA,IAAMzH,EAAU,WACZkC,EAAUxM,eAAe,OAAQ2R,GACjCnF,EAAUxM,eAAe,QAASmM,GAClCK,EAAUxM,eAAe,QAASiS,GAClCtK,EAAK9H,IAAI,QAASkM,GAClBpE,EAAK9H,IAAI,YAAaqS,IAE1B1F,EAAU5M,KAAK,OAAQ+R,GACvBnF,EAAU5M,KAAK,QAASuM,GACxBK,EAAU5M,KAAK,QAASqS,GACxBxS,KAAKG,KAAK,QAASmM,GACnBtM,KAAKG,KAAK,YAAasS,IACwB,IAA3CzS,KAAKsR,SAASrM,QAAQ,iBACb,iBAATgI,EAEAjN,KAAKqC,cAAa,WACT4P,GACDlF,EAAUlD,MAEjB,GAAE,KAGHkD,EAAUlD,MAElB,GACA,CAAA5P,IAAA,SAAA6J,MAKA,WAOI,GANA9D,KAAKiE,WAAa,OAClBkM,EAAO0B,sBAAwB,cAAgB7R,KAAK+M,UAAUE,KAC9DjN,KAAKgB,aAAa,QAClBhB,KAAKqS,QAGD,SAAWrS,KAAKiE,YAAcjE,KAAKmC,KAAKqO,QAGxC,IAFA,IAAItU,EAAI,EACFgV,EAAIlR,KAAKsR,SAAS1U,OACjBV,EAAIgV,EAAGhV,IACV8D,KAAK2S,MAAM3S,KAAKsR,SAASpV,GAGrC,GACA,CAAAjC,IAAA,WAAA6J,MAKA,SAAS/F,GACL,GAAI,YAAciC,KAAKiE,YACnB,SAAWjE,KAAKiE,YAChB,YAAcjE,KAAKiE,WAKnB,OAJAjE,KAAKgB,aAAa,SAAUjD,GAE5BiC,KAAKgB,aAAa,aAClBhB,KAAK4S,mBACG7U,EAAO3D,MACX,IAAK,OACD4F,KAAK6S,YAAYC,KAAK5D,MAAMnR,EAAO1D,OACnC,MACJ,IAAK,OACD2F,KAAK+S,WAAW,QAChB/S,KAAKgB,aAAa,QAClBhB,KAAKgB,aAAa,QAClB,MACJ,IAAK,QACD,IAAMkF,EAAM,IAAI7C,MAAM,gBAEtB6C,EAAI8M,KAAOjV,EAAO1D,KAClB2F,KAAKqJ,QAAQnD,GACb,MACJ,IAAK,UACDlG,KAAKgB,aAAa,OAAQjD,EAAO1D,MACjC2F,KAAKgB,aAAa,UAAWjD,EAAO1D,MAMpD,GACA,CAAAJ,IAAA,cAAA6J,MAMA,SAAYzJ,GACR2F,KAAKgB,aAAa,YAAa3G,GAC/B2F,KAAKqR,GAAKhX,EAAKqO,IACf1I,KAAK+M,UAAUnJ,MAAM8E,IAAMrO,EAAKqO,IAChC1I,KAAKsR,SAAWtR,KAAKiT,eAAe5Y,EAAKiX,UACzCtR,KAAKuR,aAAelX,EAAKkX,aACzBvR,KAAKwR,YAAcnX,EAAKmX,YACxBxR,KAAKuN,WAAalT,EAAKkT,WACvBvN,KAAKiI,SAED,WAAajI,KAAKiE,YAEtBjE,KAAK4S,kBACT,GACA,CAAA3Y,IAAA,mBAAA6J,MAKA,WAAmB,IAAAsE,EAAApI,KACfA,KAAKuC,eAAevC,KAAKyR,kBACzBzR,KAAKyR,iBAAmBzR,KAAKqC,cAAa,WACtC+F,EAAKhE,QAAQ,eAChB,GAAEpE,KAAKuR,aAAevR,KAAKwR,aACxBxR,KAAKmC,KAAKgK,WACVnM,KAAKyR,iBAAiBpF,OAE9B,GACA,CAAApS,IAAA,UAAA6J,MAKA,WACI9D,KAAKoQ,YAAYxP,OAAO,EAAGZ,KAAKsQ,eAIhCtQ,KAAKsQ,cAAgB,EACjB,IAAMtQ,KAAKoQ,YAAYxT,OACvBoD,KAAKgB,aAAa,SAGlBhB,KAAKqS,OAEb,GACA,CAAApY,IAAA,QAAA6J,MAKA,WACI,GAAI,WAAa9D,KAAKiE,YAClBjE,KAAK+M,UAAUrJ,WACd1D,KAAKoS,WACNpS,KAAKoQ,YAAYxT,OAAQ,CACzB,IAAMyH,EAAUrE,KAAKkT,qBACrBlT,KAAK+M,UAAUvC,KAAKnG,GAGpBrE,KAAKsQ,cAAgBjM,EAAQzH,OAC7BoD,KAAKgB,aAAa,QACtB,CACJ,GACA,CAAA/G,IAAA,qBAAA6J,MAMA,WAII,KAH+B9D,KAAKuN,YACR,YAAxBvN,KAAK+M,UAAUE,MACfjN,KAAKoQ,YAAYxT,OAAS,GAE1B,OAAOoD,KAAKoQ,YAGhB,IADA,IZtZmBtV,EYsZfqY,EAAc,EACTjX,EAAI,EAAGA,EAAI8D,KAAKoQ,YAAYxT,OAAQV,IAAK,CAC9C,IAAM7B,EAAO2F,KAAKoQ,YAAYlU,GAAG7B,KAIjC,GAHIA,IACA8Y,GZzZO,iBADIrY,EY0ZeT,GZnZ1C,SAAoBiL,GAEhB,IADA,IAAI8N,EAAI,EAAGxW,EAAS,EACXV,EAAI,EAAGgV,EAAI5L,EAAI1I,OAAQV,EAAIgV,EAAGhV,KACnCkX,EAAI9N,EAAInJ,WAAWD,IACX,IACJU,GAAU,EAELwW,EAAI,KACTxW,GAAU,EAELwW,EAAI,OAAUA,GAAK,MACxBxW,GAAU,GAGVV,IACAU,GAAU,GAGlB,OAAOA,CACX,CAxBeyW,CAAWvY,GAGf8K,KAAK0N,KAPQ,MAOFxY,EAAIiB,YAAcjB,EAAIwE,QYuZ5BpD,EAAI,GAAKiX,EAAcnT,KAAKuN,WAC5B,OAAOvN,KAAKoQ,YAAY3Q,MAAM,EAAGvD,GAErCiX,GAAe,CACnB,CACA,OAAOnT,KAAKoQ,WAChB,GACA,CAAAnW,IAAA,QAAA6J,MAQA,SAAMqO,EAAKoB,EAASxT,GAEhB,OADAC,KAAK+S,WAAW,UAAWZ,EAAKoB,EAASxT,GAClCC,IACX,GAAC,CAAA/F,IAAA,OAAA6J,MACD,SAAKqO,EAAKoB,EAASxT,GAEf,OADAC,KAAK+S,WAAW,UAAWZ,EAAKoB,EAASxT,GAClCC,IACX,GACA,CAAA/F,IAAA,aAAA6J,MASA,SAAW1J,EAAMC,EAAMkZ,EAASxT,GAS5B,GARI,mBAAsB1F,IACtB0F,EAAK1F,EACLA,OAAOsK,GAEP,mBAAsB4O,IACtBxT,EAAKwT,EACLA,EAAU,MAEV,YAAcvT,KAAKiE,YAAc,WAAajE,KAAKiE,WAAvD,EAGAsP,EAAUA,GAAW,IACbC,UAAW,IAAUD,EAAQC,SACrC,IAAMzV,EAAS,CACX3D,KAAMA,EACNC,KAAMA,EACNkZ,QAASA,GAEbvT,KAAKgB,aAAa,eAAgBjD,GAClCiC,KAAKoQ,YAAYlQ,KAAKnC,GAClBgC,GACAC,KAAKG,KAAK,QAASJ,GACvBC,KAAKqS,OAZL,CAaJ,GACA,CAAApY,IAAA,QAAA6J,MAGA,WAAQ,IAAAkF,EAAAhJ,KACEmI,EAAQ,WACVa,EAAK5E,QAAQ,gBACb4E,EAAK+D,UAAU5E,SAEbsL,EAAkB,SAAlBA,IACFzK,EAAK5I,IAAI,UAAWqT,GACpBzK,EAAK5I,IAAI,eAAgBqT,GACzBtL,KAEEuL,EAAiB,WAEnB1K,EAAK7I,KAAK,UAAWsT,GACrBzK,EAAK7I,KAAK,eAAgBsT,IAqB9B,MAnBI,YAAczT,KAAKiE,YAAc,SAAWjE,KAAKiE,aACjDjE,KAAKiE,WAAa,UACdjE,KAAKoQ,YAAYxT,OACjBoD,KAAKG,KAAK,SAAS,WACX6I,EAAKoJ,UACLsB,IAGAvL,GAER,IAEKnI,KAAKoS,UACVsB,IAGAvL,KAGDnI,IACX,GACA,CAAA/F,IAAA,UAAA6J,MAKA,SAAQoC,GACJiK,EAAO0B,uBAAwB,EAC/B7R,KAAKgB,aAAa,QAASkF,GAC3BlG,KAAKoE,QAAQ,kBAAmB8B,EACpC,GACA,CAAAjM,IAAA,UAAA6J,MAKA,SAAQhB,EAAQC,GACR,YAAc/C,KAAKiE,YACnB,SAAWjE,KAAKiE,YAChB,YAAcjE,KAAKiE,aAEnBjE,KAAKuC,eAAevC,KAAKyR,kBAEzBzR,KAAK+M,UAAUvM,mBAAmB,SAElCR,KAAK+M,UAAU5E,QAEfnI,KAAK+M,UAAUvM,qBACoB,mBAAxBC,sBACPA,oBAAoB,eAAgBT,KAAK0R,2BAA2B,GACpEjR,oBAAoB,UAAWT,KAAK2R,sBAAsB,IAG9D3R,KAAKiE,WAAa,SAElBjE,KAAKqR,GAAK,KAEVrR,KAAKgB,aAAa,QAAS8B,EAAQC,GAGnC/C,KAAKoQ,YAAc,GACnBpQ,KAAKsQ,cAAgB,EAE7B,GACA,CAAArW,IAAA,iBAAA6J,MAMA,SAAewN,GAIX,IAHA,IAAMqC,EAAmB,GACrBzX,EAAI,EACFsD,EAAI8R,EAAS1U,OACZV,EAAIsD,EAAGtD,KACL8D,KAAK6O,WAAW5J,QAAQqM,EAASpV,KAClCyX,EAAiBzT,KAAKoR,EAASpV,IAEvC,OAAOyX,CACX,KAACxD,CAAA,EAxkBuBzQ,GA0kBtBkU,GAAC3M,ShBvbiB,EiBxJAkJ,GAAOlJ,SCF/B,IAAMtM,GAA+C,mBAAhBC,YAC/BC,GAAS,SAACC,GACZ,MAAqC,mBAAvBF,YAAYC,OACpBD,YAAYC,OAAOC,GACnBA,EAAIC,kBAAkBH,WAChC,EACMH,GAAWb,OAAOY,UAAUC,SAC5BH,GAAiC,mBAATC,MACT,oBAATA,MACoB,6BAAxBE,GAASC,KAAKH,MAChBsZ,GAAiC,mBAATC,MACT,oBAATA,MACoB,6BAAxBrZ,GAASC,KAAKoZ,MAMf,SAASnG,GAAS7S,GACrB,OAASH,KAA0BG,aAAeF,aAAeC,GAAOC,KACnER,IAAkBQ,aAAeP,MACjCsZ,IAAkB/Y,aAAegZ,IAC1C,CACO,SAASC,GAAUjZ,EAAKkZ,GAC3B,IAAKlZ,GAAsB,WAAfuV,EAAOvV,GACf,OAAO,EAEX,GAAIiG,MAAMkT,QAAQnZ,GAAM,CACpB,IAAK,IAAIoB,EAAI,EAAGgV,EAAIpW,EAAI8B,OAAQV,EAAIgV,EAAGhV,IACnC,GAAI6X,GAAUjZ,EAAIoB,IACd,OAAO,EAGf,OAAO,CACX,CACA,GAAIyR,GAAS7S,GACT,OAAO,EAEX,GAAIA,EAAIkZ,QACkB,mBAAflZ,EAAIkZ,QACU,IAArB1T,UAAU1D,OACV,OAAOmX,GAAUjZ,EAAIkZ,UAAU,GAEnC,IAAK,IAAM/Z,KAAOa,EACd,GAAIlB,OAAOY,UAAUoH,eAAelH,KAAKI,EAAKb,IAAQ8Z,GAAUjZ,EAAIb,IAChE,OAAO,EAGf,OAAO,CACX,CCzCO,SAASia,GAAkBnW,GAC9B,IAAMoW,EAAU,GACVC,EAAarW,EAAO1D,KACpBga,EAAOtW,EAGb,OAFAsW,EAAKha,KAAOia,GAAmBF,EAAYD,GAC3CE,EAAKE,YAAcJ,EAAQvX,OACpB,CAAEmB,OAAQsW,EAAMF,QAASA,EACpC,CACA,SAASG,GAAmBja,EAAM8Z,GAC9B,IAAK9Z,EACD,OAAOA,EACX,GAAIsT,GAAStT,GAAO,CAChB,IAAMma,EAAc,CAAEC,cAAc,EAAM9O,IAAKwO,EAAQvX,QAEvD,OADAuX,EAAQjU,KAAK7F,GACNma,CACV,CACI,GAAIzT,MAAMkT,QAAQ5Z,GAAO,CAE1B,IADA,IAAMqa,EAAU,IAAI3T,MAAM1G,EAAKuC,QACtBV,EAAI,EAAGA,EAAI7B,EAAKuC,OAAQV,IAC7BwY,EAAQxY,GAAKoY,GAAmBja,EAAK6B,GAAIiY,GAE7C,OAAOO,CACX,CACK,GAAoB,WAAhBrE,EAAOhW,MAAuBA,aAAgB2L,MAAO,CAC1D,IAAM0O,EAAU,CAAA,EAChB,IAAK,IAAMza,KAAOI,EACVT,OAAOY,UAAUoH,eAAelH,KAAKL,EAAMJ,KAC3Cya,EAAQza,GAAOqa,GAAmBja,EAAKJ,GAAMka,IAGrD,OAAOO,CACX,CACA,OAAOra,CACX,CASO,SAASsa,GAAkB5W,EAAQoW,GAGtC,OAFApW,EAAO1D,KAAOua,GAAmB7W,EAAO1D,KAAM8Z,UACvCpW,EAAOwW,YACPxW,CACX,CACA,SAAS6W,GAAmBva,EAAM8Z,GAC9B,IAAK9Z,EACD,OAAOA,EACX,GAAIA,IAA8B,IAAtBA,EAAKoa,aAAuB,CAIpC,GAHyC,iBAAbpa,EAAKsL,KAC7BtL,EAAKsL,KAAO,GACZtL,EAAKsL,IAAMwO,EAAQvX,OAEnB,OAAOuX,EAAQ9Z,EAAKsL,KAGpB,MAAM,IAAItC,MAAM,sBAEvB,CACI,GAAItC,MAAMkT,QAAQ5Z,GACnB,IAAK,IAAI6B,EAAI,EAAGA,EAAI7B,EAAKuC,OAAQV,IAC7B7B,EAAK6B,GAAK0Y,GAAmBva,EAAK6B,GAAIiY,QAGzC,GAAoB,WAAhB9D,EAAOhW,GACZ,IAAK,IAAMJ,KAAOI,EACVT,OAAOY,UAAUoH,eAAelH,KAAKL,EAAMJ,KAC3CI,EAAKJ,GAAO2a,GAAmBva,EAAKJ,GAAMka,IAItD,OAAO9Z,CACX,CC5EA,IAcWwa,GAdLC,GAAkB,CACpB,UACA,gBACA,aACA,gBACA,cACA,mBASJ,SAAWD,GACPA,EAAWA,EAAoB,QAAI,GAAK,UACxCA,EAAWA,EAAuB,WAAI,GAAK,aAC3CA,EAAWA,EAAkB,MAAI,GAAK,QACtCA,EAAWA,EAAgB,IAAI,GAAK,MACpCA,EAAWA,EAA0B,cAAI,GAAK,gBAC9CA,EAAWA,EAAyB,aAAI,GAAK,eAC7CA,EAAWA,EAAuB,WAAI,GAAK,YAC9C,CARD,CAQGA,KAAeA,GAAa,CAAE,IAIjC,IAAaE,GAAO,WAMhB,SAAAA,EAAYC,GAAU9R,OAAA6R,GAClB/U,KAAKgV,SAAWA,CACpB,CA2DC,OA1DD7R,EAAA4R,EAAA,CAAA,CAAA9a,IAAA,SAAA6J,MAMA,SAAOhJ,GACH,OAAIA,EAAIV,OAASya,GAAWI,OAASna,EAAIV,OAASya,GAAWK,MACrDnB,GAAUjZ,GAWX,CAACkF,KAAKmV,eAAera,IAVbkF,KAAKoV,eAAe,CACvBhb,KAAMU,EAAIV,OAASya,GAAWI,MACxBJ,GAAWQ,aACXR,GAAWS,WACjBC,IAAKza,EAAIya,IACTlb,KAAMS,EAAIT,KACVgX,GAAIvW,EAAIuW,IAKxB,GACA,CAAApX,IAAA,iBAAA6J,MAGA,SAAehJ,GAEX,IAAIwK,EAAM,GAAKxK,EAAIV,KAmBnB,OAjBIU,EAAIV,OAASya,GAAWQ,cACxBva,EAAIV,OAASya,GAAWS,aACxBhQ,GAAOxK,EAAIyZ,YAAc,KAIzBzZ,EAAIya,KAAO,MAAQza,EAAIya,MACvBjQ,GAAOxK,EAAIya,IAAM,KAGjB,MAAQza,EAAIuW,KACZ/L,GAAOxK,EAAIuW,IAGX,MAAQvW,EAAIT,OACZiL,GAAOwN,KAAK0C,UAAU1a,EAAIT,KAAM2F,KAAKgV,WAElC1P,CACX,GACA,CAAArL,IAAA,iBAAA6J,MAKA,SAAehJ,GACX,IAAM2a,EAAiBvB,GAAkBpZ,GACnCuZ,EAAOrU,KAAKmV,eAAeM,EAAe1X,QAC1CoW,EAAUsB,EAAetB,QAE/B,OADAA,EAAQuB,QAAQrB,GACTF,CACX,KAACY,CAAA,CAnEe,GAsEpB,SAASY,GAAS7R,GACd,MAAiD,oBAA1ClK,OAAOY,UAAUC,SAASC,KAAKoJ,EAC1C,CAMa8R,IAAAA,YAAOrS,GAAAZ,EAAAiT,EAAArS,GAAA,IAAAX,EAAAC,EAAA+S,GAMhB,SAAAA,EAAYC,GAAS,IAAA5S,EAEM,OAFNC,OAAA0S,IACjB3S,EAAAL,EAAAlI,KAAAsF,OACK6V,QAAUA,EAAQ5S,CAC3B,CA4IC,OA3IDE,EAAAyS,EAAA,CAAA,CAAA3b,IAAA,MAAA6J,MAKA,SAAIhJ,GACA,IAAIiD,EACJ,GAAmB,iBAARjD,EAAkB,CACzB,GAAIkF,KAAK8V,cACL,MAAM,IAAIzS,MAAM,mDAGpB,IAAM0S,GADNhY,EAASiC,KAAKgW,aAAalb,IACEV,OAASya,GAAWQ,aAC7CU,GAAiBhY,EAAO3D,OAASya,GAAWS,YAC5CvX,EAAO3D,KAAO2b,EAAgBlB,GAAWI,MAAQJ,GAAWK,IAE5DlV,KAAK8V,cAAgB,IAAIG,GAAoBlY,GAElB,IAAvBA,EAAOwW,aACPxQ,EAAAC,EAAA4R,EAAApb,WAAA,eAAAwF,MAAAtF,KAAAsF,KAAmB,UAAWjC,IAKlCgG,EAAAC,EAAA4R,EAAApb,WAAA,eAAAwF,MAAAtF,KAAAsF,KAAmB,UAAWjC,EAErC,KACI,KAAI4P,GAAS7S,KAAQA,EAAIgC,OAe1B,MAAM,IAAIuG,MAAM,iBAAmBvI,GAbnC,IAAKkF,KAAK8V,cACN,MAAM,IAAIzS,MAAM,qDAGhBtF,EAASiC,KAAK8V,cAAcI,eAAepb,MAGvCkF,KAAK8V,cAAgB,KACrB/R,EAAAC,EAAA4R,EAAApb,WAAA,eAAAwF,MAAAtF,KAAAsF,KAAmB,UAAWjC,GAM1C,CACJ,GACA,CAAA9D,IAAA,eAAA6J,MAMA,SAAawB,GACT,IAAIpJ,EAAI,EAEFmB,EAAI,CACNjD,KAAMgL,OAAOE,EAAI7I,OAAO,KAE5B,QAA2BkI,IAAvBkQ,GAAWxX,EAAEjD,MACb,MAAM,IAAIiJ,MAAM,uBAAyBhG,EAAEjD,MAG/C,GAAIiD,EAAEjD,OAASya,GAAWQ,cACtBhY,EAAEjD,OAASya,GAAWS,WAAY,CAElC,IADA,IAAMa,EAAQja,EAAI,EACS,MAApBoJ,EAAI7I,SAASP,IAAcA,GAAKoJ,EAAI1I,SAC3C,IAAMwZ,EAAM9Q,EAAI3I,UAAUwZ,EAAOja,GACjC,GAAIka,GAAOhR,OAAOgR,IAA0B,MAAlB9Q,EAAI7I,OAAOP,GACjC,MAAM,IAAImH,MAAM,uBAEpBhG,EAAEkX,YAAcnP,OAAOgR,EAC3B,CAEA,GAAI,MAAQ9Q,EAAI7I,OAAOP,EAAI,GAAI,CAE3B,IADA,IAAMia,EAAQja,EAAI,IACTA,GAAG,CAER,GAAI,MADMoJ,EAAI7I,OAAOP,GAEjB,MACJ,GAAIA,IAAMoJ,EAAI1I,OACV,KACR,CACAS,EAAEkY,IAAMjQ,EAAI3I,UAAUwZ,EAAOja,EACjC,MAEImB,EAAEkY,IAAM,IAGZ,IAAMc,EAAO/Q,EAAI7I,OAAOP,EAAI,GAC5B,GAAI,KAAOma,GAAQjR,OAAOiR,IAASA,EAAM,CAErC,IADA,IAAMF,EAAQja,EAAI,IACTA,GAAG,CACR,IAAMkX,EAAI9N,EAAI7I,OAAOP,GACrB,GAAI,MAAQkX,GAAKhO,OAAOgO,IAAMA,EAAG,GAC3BlX,EACF,KACJ,CACA,GAAIA,IAAMoJ,EAAI1I,OACV,KACR,CACAS,EAAEgU,GAAKjM,OAAOE,EAAI3I,UAAUwZ,EAAOja,EAAI,GAC3C,CAEA,GAAIoJ,EAAI7I,SAASP,GAAI,CACjB,IAAMoa,EAAUtW,KAAKuW,SAASjR,EAAIkR,OAAOta,IACzC,IAAI0Z,EAAQa,eAAepZ,EAAEjD,KAAMkc,GAI/B,MAAM,IAAIjT,MAAM,mBAHhBhG,EAAEhD,KAAOic,CAKjB,CACA,OAAOjZ,CACX,GAAC,CAAApD,IAAA,WAAA6J,MACD,SAASwB,GACL,IACI,OAAOwN,KAAK5D,MAAM5J,EAAKtF,KAAK6V,QAC/B,CACD,MAAOvP,GACH,OAAO,CACX,CACJ,GAAC,CAAArM,IAAA,UAAA6J,MAuBD,WACQ9D,KAAK8V,gBACL9V,KAAK8V,cAAcY,yBACnB1W,KAAK8V,cAAgB,KAE7B,IAAC,CAAA,CAAA7b,IAAA,iBAAA6J,MA3BD,SAAsB1J,EAAMkc,GACxB,OAAQlc,GACJ,KAAKya,GAAW8B,QACZ,OAAOhB,GAASW,GACpB,KAAKzB,GAAW+B,WACZ,YAAmBjS,IAAZ2R,EACX,KAAKzB,GAAWgC,cACZ,MAA0B,iBAAZP,GAAwBX,GAASW,GACnD,KAAKzB,GAAWI,MAChB,KAAKJ,GAAWQ,aACZ,OAAQtU,MAAMkT,QAAQqC,KACK,iBAAfA,EAAQ,IACW,iBAAfA,EAAQ,KAC6B,IAAzCxB,GAAgB7P,QAAQqR,EAAQ,KAChD,KAAKzB,GAAWK,IAChB,KAAKL,GAAWS,WACZ,OAAOvU,MAAMkT,QAAQqC,GAEjC,KAACV,CAAA,EArJwBlW,GAwKvBuW,GAAmB,WACrB,SAAAA,EAAYlY,GAAQmF,OAAA+S,GAChBjW,KAAKjC,OAASA,EACdiC,KAAKmU,QAAU,GACfnU,KAAK8W,UAAY/Y,CACrB,CAyBC,OAxBDoF,EAAA8S,EAAA,CAAA,CAAAhc,IAAA,iBAAA6J,MAQA,SAAeiT,GAEX,GADA/W,KAAKmU,QAAQjU,KAAK6W,GACd/W,KAAKmU,QAAQvX,SAAWoD,KAAK8W,UAAUvC,YAAa,CAEpD,IAAMxW,EAAS4W,GAAkB3U,KAAK8W,UAAW9W,KAAKmU,SAEtD,OADAnU,KAAK0W,yBACE3Y,CACX,CACA,OAAO,IACX,GACA,CAAA9D,IAAA,yBAAA6J,MAGA,WACI9D,KAAK8W,UAAY,KACjB9W,KAAKmU,QAAU,EACnB,KAAC8B,CAAA,CA9BoB,6CApQD,sDCnBjB,SAASrW,GAAG9E,EAAK2R,EAAI1M,GAExB,OADAjF,EAAI8E,GAAG6M,EAAI1M,GACJ,WACHjF,EAAIsF,IAAIqM,EAAI1M,GAEpB,CCEA,IAAM+U,GAAkBlb,OAAOod,OAAO,CAClCC,QAAS,EACTC,cAAe,EACfC,WAAY,EACZC,cAAe,EAEfC,YAAa,EACb9W,eAAgB,IA0BP4P,YAAM5M,GAAAZ,EAAAwN,EAAA5M,GAAA,IAAAX,EAAAC,EAAAsN,GAIf,SAAAA,EAAYmH,EAAI/B,EAAKpT,GAAM,IAAAc,EAoDP,OApDOC,OAAAiN,IACvBlN,EAAAL,EAAAlI,KAAAsF,OAeKuX,WAAY,EAKjBtU,EAAKuU,WAAY,EAIjBvU,EAAKwU,cAAgB,GAIrBxU,EAAKyU,WAAa,GAOlBzU,EAAK0U,OAAS,GAKd1U,EAAK2U,UAAY,EACjB3U,EAAK4U,IAAM,EACX5U,EAAK6U,KAAO,GACZ7U,EAAK8U,MAAQ,GACb9U,EAAKqU,GAAKA,EACVrU,EAAKsS,IAAMA,EACPpT,GAAQA,EAAK6V,OACb/U,EAAK+U,KAAO7V,EAAK6V,MAErB/U,EAAKgV,MAAQpP,EAAc,CAAE,EAAE1G,GAC3Bc,EAAKqU,GAAGY,cACRjV,EAAK4G,OAAO5G,CACpB,CAmuBC,OAluBDE,EAAAgN,EAAA,CAAA,CAAAlW,IAAA,eAAAsN,IAcA,WACI,OAAQvH,KAAKuX,SACjB,GACA,CAAAtd,IAAA,YAAA6J,MAKA,WACI,IAAI9D,KAAKmY,KAAT,CAEA,IAAMb,EAAKtX,KAAKsX,GAChBtX,KAAKmY,KAAO,CACRvY,GAAG0X,EAAI,OAAQtX,KAAKkM,OAAO5J,KAAKtC,OAChCJ,GAAG0X,EAAI,SAAUtX,KAAKoY,SAAS9V,KAAKtC,OACpCJ,GAAG0X,EAAI,QAAStX,KAAK0M,QAAQpK,KAAKtC,OAClCJ,GAAG0X,EAAI,QAAStX,KAAKsM,QAAQhK,KAAKtC,OANlC,CAQR,GACA,CAAA/F,IAAA,SAAAsN,IAiBA,WACI,QAASvH,KAAKmY,IAClB,GACA,CAAAle,IAAA,UAAA6J,MAUA,WACI,OAAI9D,KAAKuX,YAETvX,KAAKqY,YACArY,KAAKsX,GAAkB,eACxBtX,KAAKsX,GAAGzN,OACR,SAAW7J,KAAKsX,GAAGgB,aACnBtY,KAAKkM,UALElM,IAOf,GACA,CAAA/F,IAAA,OAAA6J,MAGA,WACI,OAAO9D,KAAKiX,SAChB,GACA,CAAAhd,IAAA,OAAA6J,MAeA,WAAc,IAAA,IAAAtC,EAAAlB,UAAA1D,OAANkE,EAAIC,IAAAA,MAAAS,GAAAE,EAAA,EAAAA,EAAAF,EAAAE,IAAJZ,EAAIY,GAAApB,UAAAoB,GAGR,OAFAZ,EAAK4U,QAAQ,WACb1V,KAAKa,KAAKR,MAAML,KAAMc,GACfd,IACX,GACA,CAAA/F,IAAA,OAAA6J,MAiBA,SAAK2I,GACD,GAAIqI,GAAgBlT,eAAe6K,GAC/B,MAAM,IAAIpJ,MAAM,IAAMoJ,EAAGhS,WAAa,8BACzC,IAAA8d,IAAAA,EAAAjY,UAAA1D,OAHOkE,MAAIC,MAAAwX,EAAAA,EAAAA,OAAAC,EAAA,EAAAA,EAAAD,EAAAC,IAAJ1X,EAAI0X,EAAAlY,GAAAA,UAAAkY,GAKZ,GADA1X,EAAK4U,QAAQjJ,GACTzM,KAAKiY,MAAMQ,UAAYzY,KAAK+X,MAAMW,YAAc1Y,KAAK+X,eAErD,OADA/X,KAAK2Y,YAAY7X,GACVd,KAEX,IAAMjC,EAAS,CACX3D,KAAMya,GAAWI,MACjB5a,KAAMyG,EAEV/C,QAAiB,IAGjB,GAFAA,EAAOwV,QAAQC,UAAmC,IAAxBxT,KAAK+X,MAAMvE,SAEjC,mBAAsB1S,EAAKA,EAAKlE,OAAS,GAAI,CAC7C,IAAMyU,EAAKrR,KAAK6X,MACVe,EAAM9X,EAAK+X,MACjB7Y,KAAK8Y,qBAAqBzH,EAAIuH,GAC9B7a,EAAOsT,GAAKA,CAChB,CACA,IAAM0H,EAAsB/Y,KAAKsX,GAAG0B,QAChChZ,KAAKsX,GAAG0B,OAAOjM,WACf/M,KAAKsX,GAAG0B,OAAOjM,UAAUrJ,SAY7B,OAXsB1D,KAAK+X,MAAc,YAAMgB,IAAwB/Y,KAAKuX,aAGnEvX,KAAKuX,WACVvX,KAAKiZ,wBAAwBlb,GAC7BiC,KAAKjC,OAAOA,IAGZiC,KAAK0X,WAAWxX,KAAKnC,IAEzBiC,KAAK+X,MAAQ,GACN/X,IACX,GACA,CAAA/F,IAAA,uBAAA6J,MAGA,SAAqBuN,EAAIuH,GAAK,IACtBlP,EADsBjG,EAAAzD,KAEpBmK,EAAwC,QAA7BT,EAAK1J,KAAK+X,MAAM5N,eAA4B,IAAPT,EAAgBA,EAAK1J,KAAKiY,MAAMiB,WACtF,QAAgBvU,IAAZwF,EAAJ,CAKA,IAAMgP,EAAQnZ,KAAKsX,GAAGjV,cAAa,kBACxBoB,EAAKqU,KAAKzG,GACjB,IAAK,IAAInV,EAAI,EAAGA,EAAIuH,EAAKiU,WAAW9a,OAAQV,IACpCuH,EAAKiU,WAAWxb,GAAGmV,KAAOA,GAC1B5N,EAAKiU,WAAW9W,OAAO1E,EAAG,GAGlC0c,EAAIle,KAAK+I,EAAM,IAAIJ,MAAM,2BAC5B,GAAE8G,GACHnK,KAAK8X,KAAKzG,GAAM,WAEZ5N,EAAK6T,GAAG/U,eAAe4W,GAAO,IAAA,IAAAC,EAAA9Y,UAAA1D,OAFdkE,EAAIC,IAAAA,MAAAqY,GAAAC,EAAA,EAAAA,EAAAD,EAAAC,IAAJvY,EAAIuY,GAAA/Y,UAAA+Y,GAGpBT,EAAIvY,MAAMoD,EAAI,CAAG,MAAI8C,OAAKzF,IAd9B,MAFId,KAAK8X,KAAKzG,GAAMuH,CAkBxB,GACA,CAAA3e,IAAA,cAAA6J,MAgBA,SAAY2I,GAAa,IAAA,IAAA7E,EAAA5H,KAAAsZ,EAAAhZ,UAAA1D,OAANkE,MAAIC,MAAAuY,EAAAA,EAAAA,OAAAC,EAAA,EAAAA,EAAAD,EAAAC,IAAJzY,EAAIyY,EAAAjZ,GAAAA,UAAAiZ,GAEnB,IAAMC,OAAiC7U,IAAvB3E,KAAK+X,MAAM5N,cAAmDxF,IAA1B3E,KAAKiY,MAAMiB,WAC/D,OAAO,IAAI9N,SAAQ,SAACC,EAASoO,GACzB3Y,EAAKZ,MAAK,SAACwZ,EAAMC,GACb,OAAIH,EACOE,EAAOD,EAAOC,GAAQrO,EAAQsO,GAG9BtO,EAAQqO,EAEvB,IACA9R,EAAK/G,KAAIR,MAATuH,EAAU6E,CAAAA,GAAElG,OAAKzF,GACrB,GACJ,GACA,CAAA7G,IAAA,cAAA6J,MAKA,SAAYhD,GAAM,IACV8X,EADU1Q,EAAAlI,KAEuB,mBAA1Bc,EAAKA,EAAKlE,OAAS,KAC1Bgc,EAAM9X,EAAK+X,OAEf,IAAM9a,EAAS,CACXsT,GAAIrR,KAAK4X,YACTgC,SAAU,EACVC,SAAS,EACT/Y,KAAAA,EACAiX,MAAOlP,EAAc,CAAE6P,WAAW,GAAQ1Y,KAAK+X,QAEnDjX,EAAKZ,MAAK,SAACgG,GACP,GAAInI,IAAWmK,EAAKyP,OAAO,GAA3B,CAKA,GADyB,OAARzR,EAETnI,EAAO6b,SAAW1R,EAAK+P,MAAMQ,UAC7BvQ,EAAKyP,OAAOpY,QACRqZ,GACAA,EAAI1S,SAMZ,GADAgC,EAAKyP,OAAOpY,QACRqZ,EAAK,CAAA,IAAAkB,IAAAA,EAAAxZ,UAAA1D,OAhBEmd,MAAYhZ,MAAA+Y,EAAAA,EAAAA,OAAAE,EAAA,EAAAA,EAAAF,EAAAE,IAAZD,EAAYC,EAAA1Z,GAAAA,UAAA0Z,GAiBnBpB,EAAGvY,WAAC,EAAA,CAAA,MAAIkG,OAAKwT,GACjB,CAGJ,OADAhc,EAAO8b,SAAU,EACV3R,EAAK+R,aAjBZ,CAkBJ,IACAja,KAAK2X,OAAOzX,KAAKnC,GACjBiC,KAAKia,aACT,GACA,CAAAhgB,IAAA,cAAA6J,MAMA,WAA2B,IAAfoW,EAAK5Z,UAAA1D,OAAA,QAAA+H,IAAArE,UAAA,IAAAA,UAAA,GACb,GAAKN,KAAKuX,WAAoC,IAAvBvX,KAAK2X,OAAO/a,OAAnC,CAGA,IAAMmB,EAASiC,KAAK2X,OAAO,GACvB5Z,EAAO8b,UAAYK,IAGvBnc,EAAO8b,SAAU,EACjB9b,EAAO6b,WACP5Z,KAAK+X,MAAQha,EAAOga,MACpB/X,KAAKa,KAAKR,MAAML,KAAMjC,EAAO+C,MAR7B,CASJ,GACA,CAAA7G,IAAA,SAAA6J,MAMA,SAAO/F,GACHA,EAAOwX,IAAMvV,KAAKuV,IAClBvV,KAAKsX,GAAG6C,QAAQpc,EACpB,GACA,CAAA9D,IAAA,SAAA6J,MAKA,WAAS,IAAAsE,EAAApI,KACmB,mBAAbA,KAAKgY,KACZhY,KAAKgY,MAAK,SAAC3d,GACP+N,EAAKgS,mBAAmB/f,EAC5B,IAGA2F,KAAKoa,mBAAmBpa,KAAKgY,KAErC,GACA,CAAA/d,IAAA,qBAAA6J,MAMA,SAAmBzJ,GACf2F,KAAKjC,OAAO,CACR3D,KAAMya,GAAW8B,QACjBtc,KAAM2F,KAAKqa,KACLxR,EAAc,CAAEyR,IAAKta,KAAKqa,KAAME,OAAQva,KAAKwa,aAAengB,GAC5DA,GAEd,GACA,CAAAJ,IAAA,UAAA6J,MAMA,SAAQoC,GACClG,KAAKuX,WACNvX,KAAKgB,aAAa,gBAAiBkF,EAE3C,GACA,CAAAjM,IAAA,UAAA6J,MAOA,SAAQhB,EAAQC,GACZ/C,KAAKuX,WAAY,SACVvX,KAAKqR,GACZrR,KAAKgB,aAAa,aAAc8B,EAAQC,EAC5C,GACA,CAAA9I,IAAA,WAAA6J,MAMA,SAAS/F,GAEL,GADsBA,EAAOwX,MAAQvV,KAAKuV,IAG1C,OAAQxX,EAAO3D,MACX,KAAKya,GAAW8B,QACR5Y,EAAO1D,MAAQ0D,EAAO1D,KAAKqO,IAC3B1I,KAAKya,UAAU1c,EAAO1D,KAAKqO,IAAK3K,EAAO1D,KAAKigB,KAG5Cta,KAAKgB,aAAa,gBAAiB,IAAIqC,MAAM,8LAEjD,MACJ,KAAKwR,GAAWI,MAChB,KAAKJ,GAAWQ,aACZrV,KAAK0a,QAAQ3c,GACb,MACJ,KAAK8W,GAAWK,IAChB,KAAKL,GAAWS,WACZtV,KAAK2a,MAAM5c,GACX,MACJ,KAAK8W,GAAW+B,WACZ5W,KAAK4a,eACL,MACJ,KAAK/F,GAAWgC,cACZ7W,KAAK6a,UACL,IAAM3U,EAAM,IAAI7C,MAAMtF,EAAO1D,KAAKygB,SAElC5U,EAAI7L,KAAO0D,EAAO1D,KAAKA,KACvB2F,KAAKgB,aAAa,gBAAiBkF,GAG/C,GACA,CAAAjM,IAAA,UAAA6J,MAMA,SAAQ/F,GACJ,IAAM+C,EAAO/C,EAAO1D,MAAQ,GACxB,MAAQ0D,EAAOsT,IACfvQ,EAAKZ,KAAKF,KAAK4Y,IAAI7a,EAAOsT,KAE1BrR,KAAKuX,UACLvX,KAAK+a,UAAUja,GAGfd,KAAKyX,cAAcvX,KAAKtG,OAAOod,OAAOlW,GAE9C,GAAC,CAAA7G,IAAA,YAAA6J,MACD,SAAUhD,GACN,GAAId,KAAKgb,eAAiBhb,KAAKgb,cAAcpe,OAAQ,CACjD,IACgCqe,EADaC,EAAAC,EAA3Bnb,KAAKgb,cAAcvb,SACL,IAAhC,IAAAyb,EAAAE,MAAAH,EAAAC,EAAApN,KAAAc,MAAkC,CAAfqM,EAAAnX,MACNzD,MAAML,KAAMc,EACzB,CAAC,CAAA,MAAAoF,GAAAgV,EAAA5U,EAAAJ,EAAA,CAAA,QAAAgV,EAAAG,GAAA,CACL,CACAtX,EAAAC,EAAAmM,EAAA3V,WAAW6F,OAAAA,MAAAA,MAAML,KAAMc,GACnBd,KAAKqa,MAAQvZ,EAAKlE,QAA2C,iBAA1BkE,EAAKA,EAAKlE,OAAS,KACtDoD,KAAKwa,YAAc1Z,EAAKA,EAAKlE,OAAS,GAE9C,GACA,CAAA3C,IAAA,MAAA6J,MAKA,SAAIuN,GACA,IAAMjQ,EAAOpB,KACTsb,GAAO,EACX,OAAO,WAEH,IAAIA,EAAJ,CAEAA,GAAO,EAAK,IAAA,IAAAC,EAAAjb,UAAA1D,OAJIkE,EAAIC,IAAAA,MAAAwa,GAAAC,EAAA,EAAAA,EAAAD,EAAAC,IAAJ1a,EAAI0a,GAAAlb,UAAAkb,GAKpBpa,EAAKrD,OAAO,CACR3D,KAAMya,GAAWK,IACjB7D,GAAIA,EACJhX,KAAMyG,GALN,EAQZ,GACA,CAAA7G,IAAA,QAAA6J,MAMA,SAAM/F,GACF,IAAM6a,EAAM5Y,KAAK8X,KAAK/Z,EAAOsT,IACzB,mBAAsBuH,IACtBA,EAAIvY,MAAML,KAAMjC,EAAO1D,aAChB2F,KAAK8X,KAAK/Z,EAAOsT,IAIhC,GACA,CAAApX,IAAA,YAAA6J,MAKA,SAAUuN,EAAIiJ,GACVta,KAAKqR,GAAKA,EACVrR,KAAKwX,UAAY8C,GAAOta,KAAKqa,OAASC,EACtCta,KAAKqa,KAAOC,EACZta,KAAKuX,WAAY,EACjBvX,KAAKyb,eACLzb,KAAKgB,aAAa,WAClBhB,KAAKia,aAAY,EACrB,GACA,CAAAhgB,IAAA,eAAA6J,MAKA,WAAe,IAAAkF,EAAAhJ,KACXA,KAAKyX,cAAczd,SAAQ,SAAC8G,GAAI,OAAKkI,EAAK+R,UAAUja,MACpDd,KAAKyX,cAAgB,GACrBzX,KAAK0X,WAAW1d,SAAQ,SAAC+D,GACrBiL,EAAKiQ,wBAAwBlb,GAC7BiL,EAAKjL,OAAOA,EAChB,IACAiC,KAAK0X,WAAa,EACtB,GACA,CAAAzd,IAAA,eAAA6J,MAKA,WACI9D,KAAK6a,UACL7a,KAAKsM,QAAQ,uBACjB,GACA,CAAArS,IAAA,UAAA6J,MAOA,WACQ9D,KAAKmY,OAELnY,KAAKmY,KAAKne,SAAQ,SAAC0hB,GAAU,OAAKA,OAClC1b,KAAKmY,UAAOxT,GAEhB3E,KAAKsX,GAAa,SAAEtX,KACxB,GACA,CAAA/F,IAAA,aAAA6J,MAgBA,WAUI,OATI9D,KAAKuX,WACLvX,KAAKjC,OAAO,CAAE3D,KAAMya,GAAW+B,aAGnC5W,KAAK6a,UACD7a,KAAKuX,WAELvX,KAAKsM,QAAQ,wBAEVtM,IACX,GACA,CAAA/F,IAAA,QAAA6J,MAKA,WACI,OAAO9D,KAAKmX,YAChB,GACA,CAAAld,IAAA,WAAA6J,MASA,SAAS0P,GAEL,OADAxT,KAAK+X,MAAMvE,SAAWA,EACfxT,IACX,GACA,CAAA/F,IAAA,WAAAsN,IASA,WAEI,OADAvH,KAAK+X,MAAc,UAAG,EACf/X,IACX,GACA,CAAA/F,IAAA,UAAA6J,MAaA,SAAQqG,GAEJ,OADAnK,KAAK+X,MAAM5N,QAAUA,EACdnK,IACX,GACA,CAAA/F,IAAA,QAAA6J,MAWA,SAAM6X,GAGF,OAFA3b,KAAKgb,cAAgBhb,KAAKgb,eAAiB,GAC3Chb,KAAKgb,cAAc9a,KAAKyb,GACjB3b,IACX,GACA,CAAA/F,IAAA,aAAA6J,MAWA,SAAW6X,GAGP,OAFA3b,KAAKgb,cAAgBhb,KAAKgb,eAAiB,GAC3Chb,KAAKgb,cAActF,QAAQiG,GACpB3b,IACX,GACA,CAAA/F,IAAA,SAAA6J,MAkBA,SAAO6X,GACH,IAAK3b,KAAKgb,cACN,OAAOhb,KAEX,GAAI2b,GAEA,IADA,IAAM1a,EAAYjB,KAAKgb,cACd9e,EAAI,EAAGA,EAAI+E,EAAUrE,OAAQV,IAClC,GAAIyf,IAAa1a,EAAU/E,GAEvB,OADA+E,EAAUL,OAAO1E,EAAG,GACb8D,UAKfA,KAAKgb,cAAgB,GAEzB,OAAOhb,IACX,GACA,CAAA/F,IAAA,eAAA6J,MAIA,WACI,OAAO9D,KAAKgb,eAAiB,EACjC,GACA,CAAA/gB,IAAA,gBAAA6J,MAaA,SAAc6X,GAGV,OAFA3b,KAAK4b,sBAAwB5b,KAAK4b,uBAAyB,GAC3D5b,KAAK4b,sBAAsB1b,KAAKyb,GACzB3b,IACX,GACA,CAAA/F,IAAA,qBAAA6J,MAaA,SAAmB6X,GAGf,OAFA3b,KAAK4b,sBAAwB5b,KAAK4b,uBAAyB,GAC3D5b,KAAK4b,sBAAsBlG,QAAQiG,GAC5B3b,IACX,GACA,CAAA/F,IAAA,iBAAA6J,MAkBA,SAAe6X,GACX,IAAK3b,KAAK4b,sBACN,OAAO5b,KAEX,GAAI2b,GAEA,IADA,IAAM1a,EAAYjB,KAAK4b,sBACd1f,EAAI,EAAGA,EAAI+E,EAAUrE,OAAQV,IAClC,GAAIyf,IAAa1a,EAAU/E,GAEvB,OADA+E,EAAUL,OAAO1E,EAAG,GACb8D,UAKfA,KAAK4b,sBAAwB,GAEjC,OAAO5b,IACX,GACA,CAAA/F,IAAA,uBAAA6J,MAIA,WACI,OAAO9D,KAAK4b,uBAAyB,EACzC,GACA,CAAA3hB,IAAA,0BAAA6J,MAOA,SAAwB/F,GACpB,GAAIiC,KAAK4b,uBAAyB5b,KAAK4b,sBAAsBhf,OAAQ,CACjE,IACgCif,EADqBC,EAAAX,EAAnCnb,KAAK4b,sBAAsBnc,SACb,IAAhC,IAAAqc,EAAAV,MAAAS,EAAAC,EAAAhO,KAAAc,MAAkC,CAAfiN,EAAA/X,MACNzD,MAAML,KAAMjC,EAAO1D,KAChC,CAAC,CAAA,MAAA6L,GAAA4V,EAAAxV,EAAAJ,EAAA,CAAA,QAAA4V,EAAAT,GAAA,CACL,CACJ,KAAClL,CAAA,EA5xBuBzQ,GC7BrB,SAASqc,GAAQ5Z,GACpBA,EAAOA,GAAQ,GACfnC,KAAKgc,GAAK7Z,EAAK8Z,KAAO,IACtBjc,KAAKkc,IAAM/Z,EAAK+Z,KAAO,IACvBlc,KAAKmc,OAASha,EAAKga,QAAU,EAC7Bnc,KAAKoc,OAASja,EAAKia,OAAS,GAAKja,EAAKia,QAAU,EAAIja,EAAKia,OAAS,EAClEpc,KAAKqc,SAAW,CACpB,CAOAN,GAAQvhB,UAAU8hB,SAAW,WACzB,IAAIN,EAAKhc,KAAKgc,GAAKpW,KAAKoI,IAAIhO,KAAKmc,OAAQnc,KAAKqc,YAC9C,GAAIrc,KAAKoc,OAAQ,CACb,IAAIG,EAAO3W,KAAK4W,SACZC,EAAY7W,KAAKC,MAAM0W,EAAOvc,KAAKoc,OAASJ,GAChDA,EAAoC,IAAN,EAAxBpW,KAAKC,MAAa,GAAP0W,IAAuBP,EAAKS,EAAYT,EAAKS,CAClE,CACA,OAAgC,EAAzB7W,KAAKqW,IAAID,EAAIhc,KAAKkc,IAC7B,EAMAH,GAAQvhB,UAAUkiB,MAAQ,WACtB1c,KAAKqc,SAAW,CACpB,EAMAN,GAAQvhB,UAAUmiB,OAAS,SAAUV,GACjCjc,KAAKgc,GAAKC,CACd,EAMAF,GAAQvhB,UAAUoiB,OAAS,SAAUV,GACjClc,KAAKkc,IAAMA,CACf,EAMAH,GAAQvhB,UAAUqiB,UAAY,SAAUT,GACpCpc,KAAKoc,OAASA,CAClB,EC3DaU,IAAAA,YAAOvZ,GAAAZ,EAAAma,EAAAvZ,GAAA,IAAAX,EAAAC,EAAAia,GAChB,SAAAA,EAAY/T,EAAK5G,GAAM,IAAAc,EACfyG,EADexG,OAAA4Z,IAEnB7Z,EAAAL,EAAAlI,KAAAsF,OACK+c,KAAO,GACZ9Z,EAAKkV,KAAO,GACRpP,GAAO,WAAQsH,EAAYtH,KAC3B5G,EAAO4G,EACPA,OAAMpE,IAEVxC,EAAOA,GAAQ,IACV2C,KAAO3C,EAAK2C,MAAQ,aACzB7B,EAAKd,KAAOA,EACZD,EAAqByB,EAAAV,GAAOd,GAC5Bc,EAAK+Z,cAAmC,IAAtB7a,EAAK6a,cACvB/Z,EAAKga,qBAAqB9a,EAAK8a,sBAAwBC,KACvDja,EAAKka,kBAAkBhb,EAAKgb,mBAAqB,KACjDla,EAAKma,qBAAqBjb,EAAKib,sBAAwB,KACvDna,EAAKoa,oBAAwD,QAAnC3T,EAAKvH,EAAKkb,2BAAwC,IAAP3T,EAAgBA,EAAK,IAC1FzG,EAAKqa,QAAU,IAAIvB,GAAQ,CACvBE,IAAKhZ,EAAKka,oBACVjB,IAAKjZ,EAAKma,uBACVhB,OAAQnZ,EAAKoa,wBAEjBpa,EAAKkH,QAAQ,MAAQhI,EAAKgI,QAAU,IAAQhI,EAAKgI,SACjDlH,EAAKqV,YAAc,SACnBrV,EAAK8F,IAAMA,EACX,IAAMwU,EAAUpb,EAAKqb,QAAUA,GAKf,OAJhBva,EAAKwa,QAAU,IAAIF,EAAQxI,QAC3B9R,EAAKya,QAAU,IAAIH,EAAQ3H,QAC3B3S,EAAKiV,cAAoC,IAArB/V,EAAKwb,YACrB1a,EAAKiV,cACLjV,EAAK4G,OAAO5G,CACpB,CA6TC,OA7TAE,EAAA2Z,EAAA,CAAA,CAAA7iB,IAAA,eAAA6J,MACD,SAAa8Z,GACT,OAAKtd,UAAU1D,QAEfoD,KAAK6d,gBAAkBD,EAChB5d,MAFIA,KAAK6d,aAGpB,GAAC,CAAA5jB,IAAA,uBAAA6J,MACD,SAAqB8Z,GACjB,YAAUjZ,IAANiZ,EACO5d,KAAK8d,uBAChB9d,KAAK8d,sBAAwBF,EACtB5d,KACX,GAAC,CAAA/F,IAAA,oBAAA6J,MACD,SAAkB8Z,GACd,IAAIlU,EACJ,YAAU/E,IAANiZ,EACO5d,KAAK+d,oBAChB/d,KAAK+d,mBAAqBH,EACF,QAAvBlU,EAAK1J,KAAKsd,eAA4B,IAAP5T,GAAyBA,EAAGiT,OAAOiB,GAC5D5d,KACX,GAAC,CAAA/F,IAAA,sBAAA6J,MACD,SAAoB8Z,GAChB,IAAIlU,EACJ,YAAU/E,IAANiZ,EACO5d,KAAKge,sBAChBhe,KAAKge,qBAAuBJ,EACJ,QAAvBlU,EAAK1J,KAAKsd,eAA4B,IAAP5T,GAAyBA,EAAGmT,UAAUe,GAC/D5d,KACX,GAAC,CAAA/F,IAAA,uBAAA6J,MACD,SAAqB8Z,GACjB,IAAIlU,EACJ,YAAU/E,IAANiZ,EACO5d,KAAKie,uBAChBje,KAAKie,sBAAwBL,EACL,QAAvBlU,EAAK1J,KAAKsd,eAA4B,IAAP5T,GAAyBA,EAAGkT,OAAOgB,GAC5D5d,KACX,GAAC,CAAA/F,IAAA,UAAA6J,MACD,SAAQ8Z,GACJ,OAAKtd,UAAU1D,QAEfoD,KAAKke,SAAWN,EACT5d,MAFIA,KAAKke,QAGpB,GACA,CAAAjkB,IAAA,uBAAA6J,MAMA,YAES9D,KAAKme,eACNne,KAAK6d,eACqB,IAA1B7d,KAAKsd,QAAQjB,UAEbrc,KAAKoe,WAEb,GACA,CAAAnkB,IAAA,OAAA6J,MAOA,SAAK/D,GAAI,IAAA0D,EAAAzD,KACL,IAAKA,KAAKsY,YAAYrT,QAAQ,QAC1B,OAAOjF,KACXA,KAAKgZ,OAAS,IAAIqF,GAAOre,KAAK+I,IAAK/I,KAAKmC,MACxC,IAAM0B,EAAS7D,KAAKgZ,OACd5X,EAAOpB,KACbA,KAAKsY,YAAc,UACnBtY,KAAKse,eAAgB,EAErB,IAAMC,EAAiB3e,GAAGiE,EAAQ,QAAQ,WACtCzC,EAAK8K,SACLnM,GAAMA,GACV,IACMsJ,EAAU,SAACnD,GACbzC,EAAKoH,UACLpH,EAAK6U,YAAc,SACnB7U,EAAKzC,aAAa,QAASkF,GACvBnG,EACAA,EAAGmG,GAIHzC,EAAK+a,wBAIPC,EAAW7e,GAAGiE,EAAQ,QAASwF,GACrC,IAAI,IAAUrJ,KAAKke,SAAU,CACzB,IAAM/T,EAAUnK,KAAKke,SAEf/E,EAAQnZ,KAAKqC,cAAa,WAC5Bkc,IACAlV,EAAQ,IAAIhG,MAAM,YAClBQ,EAAOsE,OACV,GAAEgC,GACCnK,KAAKmC,KAAKgK,WACVgN,EAAM9M,QAEVrM,KAAKmY,KAAKjY,MAAK,WACXuD,EAAKlB,eAAe4W,EACxB,GACJ,CAGA,OAFAnZ,KAAKmY,KAAKjY,KAAKqe,GACfve,KAAKmY,KAAKjY,KAAKue,GACRze,IACX,GACA,CAAA/F,IAAA,UAAA6J,MAMA,SAAQ/D,GACJ,OAAOC,KAAK6J,KAAK9J,EACrB,GACA,CAAA9F,IAAA,SAAA6J,MAKA,WAEI9D,KAAK6K,UAEL7K,KAAKsY,YAAc,OACnBtY,KAAKgB,aAAa,QAElB,IAAM6C,EAAS7D,KAAKgZ,OACpBhZ,KAAKmY,KAAKjY,KAAKN,GAAGiE,EAAQ,OAAQ7D,KAAK0e,OAAOpc,KAAKtC,OAAQJ,GAAGiE,EAAQ,OAAQ7D,KAAK2e,OAAOrc,KAAKtC,OAAQJ,GAAGiE,EAAQ,QAAS7D,KAAK0M,QAAQpK,KAAKtC,OAAQJ,GAAGiE,EAAQ,QAAS7D,KAAKsM,QAAQhK,KAAKtC,OAAQJ,GAAGI,KAAK0d,QAAS,UAAW1d,KAAK4e,UAAUtc,KAAKtC,OACvP,GACA,CAAA/F,IAAA,SAAA6J,MAKA,WACI9D,KAAKgB,aAAa,OACtB,GACA,CAAA/G,IAAA,SAAA6J,MAKA,SAAOzJ,GACH,IACI2F,KAAK0d,QAAQmB,IAAIxkB,EACpB,CACD,MAAOiM,GACHtG,KAAKsM,QAAQ,cAAehG,EAChC,CACJ,GACA,CAAArM,IAAA,YAAA6J,MAKA,SAAU/F,GAAQ,IAAA6J,EAAA5H,KAEdmL,IAAS,WACLvD,EAAK5G,aAAa,SAAUjD,EAChC,GAAGiC,KAAKqC,aACZ,GACA,CAAApI,IAAA,UAAA6J,MAKA,SAAQoC,GACJlG,KAAKgB,aAAa,QAASkF,EAC/B,GACA,CAAAjM,IAAA,SAAA6J,MAMA,SAAOyR,EAAKpT,GACR,IAAI0B,EAAS7D,KAAK+c,KAAKxH,GAQvB,OAPK1R,EAII7D,KAAKkY,eAAiBrU,EAAOib,QAClCjb,EAAOoT,WAJPpT,EAAS,IAAIsM,GAAOnQ,KAAMuV,EAAKpT,GAC/BnC,KAAK+c,KAAKxH,GAAO1R,GAKdA,CACX,GACA,CAAA5J,IAAA,WAAA6J,MAMA,SAASD,GAEL,IADA,IACAkb,EAAA,EAAAC,EADaplB,OAAOG,KAAKiG,KAAK+c,MACRgC,EAAAC,EAAApiB,OAAAmiB,IAAE,CAAnB,IAAMxJ,EAAGyJ,EAAAD,GAEV,GADe/e,KAAK+c,KAAKxH,GACduJ,OACP,MAER,CACA9e,KAAKif,QACT,GACA,CAAAhlB,IAAA,UAAA6J,MAMA,SAAQ/F,GAEJ,IADA,IAAM+J,EAAiB9H,KAAKyd,QAAQpf,OAAON,GAClC7B,EAAI,EAAGA,EAAI4L,EAAelL,OAAQV,IACvC8D,KAAKgZ,OAAO1U,MAAMwD,EAAe5L,GAAI6B,EAAOwV,QAEpD,GACA,CAAAtZ,IAAA,UAAA6J,MAKA,WACI9D,KAAKmY,KAAKne,SAAQ,SAAC0hB,GAAU,OAAKA,OAClC1b,KAAKmY,KAAKvb,OAAS,EACnBoD,KAAK0d,QAAQ7C,SACjB,GACA,CAAA5gB,IAAA,SAAA6J,MAKA,WACI9D,KAAKse,eAAgB,EACrBte,KAAKme,eAAgB,EACrBne,KAAKsM,QAAQ,gBACTtM,KAAKgZ,QACLhZ,KAAKgZ,OAAO7Q,OACpB,GACA,CAAAlO,IAAA,aAAA6J,MAKA,WACI,OAAO9D,KAAKif,QAChB,GACA,CAAAhlB,IAAA,UAAA6J,MAKA,SAAQhB,EAAQC,GACZ/C,KAAK6K,UACL7K,KAAKsd,QAAQZ,QACb1c,KAAKsY,YAAc,SACnBtY,KAAKgB,aAAa,QAAS8B,EAAQC,GAC/B/C,KAAK6d,gBAAkB7d,KAAKse,eAC5Bte,KAAKoe,WAEb,GACA,CAAAnkB,IAAA,YAAA6J,MAKA,WAAY,IAAAoE,EAAAlI,KACR,GAAIA,KAAKme,eAAiBne,KAAKse,cAC3B,OAAOte,KACX,IAAMoB,EAAOpB,KACb,GAAIA,KAAKsd,QAAQjB,UAAYrc,KAAK8d,sBAC9B9d,KAAKsd,QAAQZ,QACb1c,KAAKgB,aAAa,oBAClBhB,KAAKme,eAAgB,MAEpB,CACD,IAAMe,EAAQlf,KAAKsd,QAAQhB,WAC3Btc,KAAKme,eAAgB,EACrB,IAAMhF,EAAQnZ,KAAKqC,cAAa,WACxBjB,EAAKkd,gBAETpW,EAAKlH,aAAa,oBAAqBI,EAAKkc,QAAQjB,UAEhDjb,EAAKkd,eAETld,EAAKyI,MAAK,SAAC3D,GACHA,GACA9E,EAAK+c,eAAgB,EACrB/c,EAAKgd,YACLlW,EAAKlH,aAAa,kBAAmBkF,IAGrC9E,EAAK+d,aAEb,IACH,GAAED,GACClf,KAAKmC,KAAKgK,WACVgN,EAAM9M,QAEVrM,KAAKmY,KAAKjY,MAAK,WACXgI,EAAK3F,eAAe4W,EACxB,GACJ,CACJ,GACA,CAAAlf,IAAA,cAAA6J,MAKA,WACI,IAAMsb,EAAUpf,KAAKsd,QAAQjB,SAC7Brc,KAAKme,eAAgB,EACrBne,KAAKsd,QAAQZ,QACb1c,KAAKgB,aAAa,YAAaoe,EACnC,KAACtC,CAAA,EA9VwBpd,GCAvB2f,GAAQ,CAAA,EACd,SAASpjB,GAAO8M,EAAK5G,GACE,WAAfkO,EAAOtH,KACP5G,EAAO4G,EACPA,OAAMpE,GAGV,IASI2S,EATEgI,ECHH,SAAavW,GAAqB,IAAhBjE,EAAIxE,UAAA1D,OAAA,QAAA+H,IAAArE,UAAA,GAAAA,UAAA,GAAG,GAAIif,EAAGjf,UAAA1D,OAAA0D,EAAAA,kBAAAqE,EAC/B7J,EAAMiO,EAEVwW,EAAMA,GAA4B,oBAAbxY,UAA4BA,SAC7C,MAAQgC,IACRA,EAAMwW,EAAItY,SAAW,KAAOsY,EAAI9P,MAEjB,iBAAR1G,IACH,MAAQA,EAAItM,OAAO,KAEfsM,EADA,MAAQA,EAAItM,OAAO,GACb8iB,EAAItY,SAAW8B,EAGfwW,EAAI9P,KAAO1G,GAGpB,sBAAsByW,KAAKzW,KAExBA,OADA,IAAuBwW,EACjBA,EAAItY,SAAW,KAAO8B,EAGtB,WAAaA,GAI3BjO,EAAMoU,GAAMnG,IAGXjO,EAAIoK,OACD,cAAcsa,KAAK1kB,EAAImM,UACvBnM,EAAIoK,KAAO,KAEN,eAAesa,KAAK1kB,EAAImM,YAC7BnM,EAAIoK,KAAO,QAGnBpK,EAAIgK,KAAOhK,EAAIgK,MAAQ,IACvB,IACM2K,GADkC,IAA3B3U,EAAI2U,KAAKxK,QAAQ,KACV,IAAMnK,EAAI2U,KAAO,IAAM3U,EAAI2U,KAS/C,OAPA3U,EAAIuW,GAAKvW,EAAImM,SAAW,MAAQwI,EAAO,IAAM3U,EAAIoK,KAAOJ,EAExDhK,EAAI2kB,KACA3kB,EAAImM,SACA,MACAwI,GACC8P,GAAOA,EAAIra,OAASpK,EAAIoK,KAAO,GAAK,IAAMpK,EAAIoK,MAChDpK,CACX,CD7CmB4kB,CAAI3W,GADnB5G,EAAOA,GAAQ,IACc2C,MAAQ,cAC/B0K,EAAS8P,EAAO9P,OAChB6B,EAAKiO,EAAOjO,GACZvM,EAAOwa,EAAOxa,KACd6a,EAAgBN,GAAMhO,IAAOvM,KAAQua,GAAMhO,GAAU,KAkB3D,OAjBsBlP,EAAKyd,UACvBzd,EAAK,0BACL,IAAUA,EAAK0d,WACfF,EAGArI,EAAK,IAAIwF,GAAQtN,EAAQrN,IAGpBkd,GAAMhO,KACPgO,GAAMhO,GAAM,IAAIyL,GAAQtN,EAAQrN,IAEpCmV,EAAK+H,GAAMhO,IAEXiO,EAAO1b,QAAUzB,EAAKyB,QACtBzB,EAAKyB,MAAQ0b,EAAOvP,UAEjBuH,EAAGzT,OAAOyb,EAAOxa,KAAM3C,EAClC,QAGA0G,EAAc5M,GAAQ,CAClB6gB,QAAAA,GACA3M,OAAAA,GACAmH,GAAIrb,GACJgb,QAAShb"} \ No newline at end of file diff --git a/flask__websocket/ajax_vs_websocket/ajax/main.py b/flask__websocket/ajax_vs_websocket/ajax/main.py index dd2539aba..d50cd8d0d 100644 --- a/flask__websocket/ajax_vs_websocket/ajax/main.py +++ b/flask__websocket/ajax_vs_websocket/ajax/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from timeit import default_timer @@ -9,50 +9,48 @@ from flask import Flask, render_template, request, jsonify -app = Flask(__name__, static_folder='../_static') -app.config['SECRET_KEY'] = 'secret!' + +app = Flask(__name__, static_folder="../_static") +app.config["SECRET_KEY"] = "secret!" lock = Lock() DATA = { - 'count': 0, - 'start_time': 0.0 + "count": 0, + "start_time": 0.0, } -@app.route('/') +@app.route("/") def index(): with lock: - DATA['count'] = 0 + DATA["count"] = 0 - return render_template('index.html') + return render_template("index.html") -@app.route("/post_method", methods=['POST']) +@app.route("/post_method", methods=["POST"]) def post_method(): data = request.get_json() # print(data) with lock: - if DATA['count'] == 0: - DATA['start_time'] = default_timer() + if DATA["count"] == 0: + DATA["start_time"] = default_timer() - DATA['count'] += 1 + DATA["count"] += 1 return jsonify({ - 'number': data['number'], - 'count': DATA['count'], - 'elapsed': round(default_timer() - DATA['start_time'], 3) + "number": data["number"], + "count": DATA["count"], + "elapsed": round(default_timer() - DATA["start_time"], 3), }) -if __name__ == '__main__': +if __name__ == "__main__": # Localhost - HOST = '127.0.0.1' + HOST = "127.0.0.1" PORT = 12000 - print(f'http://{HOST}:{PORT}') + print(f"http://{HOST}:{PORT}") app.debug = True - app.run( - host=HOST, - port=PORT - ) + app.run(host=HOST, port=PORT) diff --git a/flask__websocket/ajax_vs_websocket/websocket/main.py b/flask__websocket/ajax_vs_websocket/websocket/main.py index 4477254b2..f39e0d5b1 100644 --- a/flask__websocket/ajax_vs_websocket/websocket/main.py +++ b/flask__websocket/ajax_vs_websocket/websocket/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from timeit import default_timer @@ -11,6 +11,7 @@ from flask_socketio import SocketIO, emit from engineio.payload import Payload + Payload.max_decode_packets = 1000 @@ -19,54 +20,55 @@ # the best option based on installed packages. async_mode = None -app = Flask(__name__, static_folder='../_static') -app.config['SECRET_KEY'] = 'secret!' +app = Flask(__name__, static_folder="../_static") +app.config["SECRET_KEY"] = "secret!" socketio = SocketIO(app, async_mode=async_mode) lock = Lock() DATA = { - 'count': 0, - 'start_time': 0.0 + "count": 0, + "start_time": 0.0, } -@app.route('/') +@app.route("/") def index(): with lock: - DATA['count'] = 0 + DATA["count"] = 0 - return render_template('index.html', async_mode=socketio.async_mode) + return render_template("index.html", async_mode=socketio.async_mode) -@socketio.on('post_method', namespace='/test') -def post_method(message): +@socketio.on("post_method", namespace="/test") +def post_method(message) -> None: # print(message) with lock: - if DATA['count'] == 0: - DATA['start_time'] = default_timer() + if DATA["count"] == 0: + DATA["start_time"] = default_timer() - DATA['count'] += 1 + DATA["count"] += 1 emit( - 'my_response', + "my_response", { - 'number': message['number'], - 'count': DATA['count'], - 'elapsed': round(default_timer() - DATA['start_time'], 3) - } + "number": message["number"], + "count": DATA["count"], + "elapsed": round(default_timer() - DATA["start_time"], 3), + }, ) -if __name__ == '__main__': +if __name__ == "__main__": # Localhost - HOST = '127.0.0.1' + HOST = "127.0.0.1" PORT = 12001 - print(f'http://{HOST}:{PORT}') + print(f"http://{HOST}:{PORT}") app.debug = True socketio.run( app, host=HOST, port=PORT, + allow_unsafe_werkzeug=True, ) diff --git a/flask__websocket/ajax_vs_websocket/websocket/templates/index.html b/flask__websocket/ajax_vs_websocket/websocket/templates/index.html index eb2e246e0..dfa8e0d06 100644 --- a/flask__websocket/ajax_vs_websocket/websocket/templates/index.html +++ b/flask__websocket/ajax_vs_websocket/websocket/templates/index.html @@ -3,7 +3,7 @@ Ajax vs Websocket. Test Websocket - +

Ajax vs Websocket. Test Websocket

diff --git a/flask__websocket/commands__websocket__flask-socketio/main.py b/flask__websocket/commands__websocket__flask-socketio/main.py index 42dbbf1a3..4324325e4 100644 --- a/flask__websocket/commands__websocket__flask-socketio/main.py +++ b/flask__websocket/commands__websocket__flask-socketio/main.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT import uuid +from datetime import datetime from flask import Flask, render_template, session, request from flask_socketio import SocketIO, emit @@ -17,25 +17,25 @@ async_mode = None app = Flask(__name__) -app.config['SECRET_KEY'] = 'secret!' +app.config["SECRET_KEY"] = "secret!" socketio = SocketIO(app, async_mode=async_mode) -@app.route('/') +@app.route("/") def index(): - return render_template('index.html', async_mode=socketio.async_mode) + return render_template("index.html", async_mode=socketio.async_mode) -@socketio.on('my_event', namespace='/test') -def test_message(message): - session['receive_count'] = session.get('receive_count', 0) + 1 +@socketio.on("my_event", namespace="/test") +def test_message(message) -> None: + session["receive_count"] = session.get("receive_count", 0) + 1 print(message) - data = message['data'] + data = message["data"] if data == "CURRENT_DATE_TIME": - response = DT.datetime.now().strftime('%Y-%m-%d_%H%M%S') + response = datetime.now().strftime("%Y-%m-%d_%H%M%S") elif data == "UUID": response = str(uuid.uuid4()) @@ -48,30 +48,30 @@ def test_message(message): response = data emit( - 'my_response', - {'data': response, 'count': session['receive_count']} + "my_response", + {"data": response, "count": session["receive_count"]}, ) -@socketio.on('my_ping', namespace='/test') -def ping_pong(): - emit('my_pong') +@socketio.on("my_ping", namespace="/test") +def ping_pong() -> None: + emit("my_pong") -@socketio.on('connect', namespace='/test') -def test_connect(): - emit('my_response', {'data': 'Connected'}) +@socketio.on("connect", namespace="/test") +def test_connect() -> None: + emit("my_response", {"data": "Connected"}) -@socketio.on('disconnect', namespace='/test') -def test_disconnect(): - print('Client disconnected', request.sid) +@socketio.on("disconnect", namespace="/test") +def test_disconnect() -> None: + print("Client disconnected", request.sid) -if __name__ == '__main__': - HOST = '127.0.0.1' +if __name__ == "__main__": + HOST = "127.0.0.1" PORT = 12000 - print(f'http://{HOST}:{PORT}') + print(f"http://{HOST}:{PORT}") socketio.run( app, diff --git a/flask__websocket/sending_all_from_while_in_thread/main.py b/flask__websocket/sending_all_from_while_in_thread/main.py new file mode 100644 index 000000000..fce7334c0 --- /dev/null +++ b/flask__websocket/sending_all_from_while_in_thread/main.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time + +from datetime import datetime +from threading import Thread + +from flask import Flask, render_template, session, request + +# pip install flask-socketio==5.3.6 +from flask_socketio import SocketIO, emit + + +# Set this variable to "threading", "eventlet" or "gevent" to test the +# different async modes, or leave it set to None for the application to choose +# the best option based on installed packages. +async_mode = None + +app = Flask(__name__) +app.config["SECRET_KEY"] = "secret!" +socketio = SocketIO(app, async_mode=async_mode) + + +def send_all_cycled() -> None: + with app.app_context(): + i = 0 + while True: + i += 1 + emit( + "my_response_all", + {"data": f"#{i}. {datetime.now().isoformat()}"}, + broadcast=True, # Send all clients + namespace="/", + ) + time.sleep(1) + + +thread = Thread(target=send_all_cycled) +thread.daemon = True +thread.start() + + +@app.route("/") +def index(): + return render_template("index.html") + + +@socketio.on("my_event") +def test_message(message) -> None: + session["receive_count"] = session.get("receive_count", 0) + 1 + print(message) + + response = message["data"] + emit( + "my_response", + {"data": response, "count": session["receive_count"], "sid": request.sid}, + broadcast=True, # Send all clients + ) + + +@socketio.on("my_ping") +def ping_pong() -> None: + emit("my_pong") + + +@socketio.on("connect") +def test_connect() -> None: + print("Client connected", request.sid) + emit("my_response", {"data": f"Connected!", "count": 0, "sid": request.sid}) + + +@socketio.on("disconnect") +def test_disconnect() -> None: + print("Client disconnected", request.sid) + + +if __name__ == "__main__": + # HOST = '127.0.0.1' + # HOST = "0.0.0.0" + PORT = 12000 + # print(f"http://{HOST}:{PORT}") + + socketio.run( + app, + # host=HOST, + port=PORT, + allow_unsafe_werkzeug=True, + ) diff --git a/flask__websocket/sending_all_from_while_in_thread/static/index.js b/flask__websocket/sending_all_from_while_in_thread/static/index.js new file mode 100644 index 000000000..d94c761e9 --- /dev/null +++ b/flask__websocket/sending_all_from_while_in_thread/static/index.js @@ -0,0 +1,62 @@ +$(document).ready(function() { + // Connect to the Socket.IO server. + var socket = io(); + + // Event handler for new connections. + // The callback function is invoked when a connection with the + // server is established. + socket.on('connect', function() { + socket.emit('my_event', {data: "I'm connected!"}); + }); + + // Event handler for server sent data. + // The callback function is invoked whenever the server emits data + // to the client. The data is then displayed in the "Received" + // section of the page. + socket.on('my_response', function(msg, cb) { + console.log(msg); + $('#log').prepend( + '
' + $('
').text(`Received #${msg.count}: ${msg.data} from #${msg.sid}`).html() + ); + if (cb) + cb(); + }); + + socket.on('my_response_all', function(msg, cb) { + console.log(msg); + $('#server_data').text(msg.data); + if (cb) + cb(); + }); + + // Interval function that tests message latency by sending a "ping" + // message. The server then responds with a "pong" message and the + // round trip time is measured. + var ping_pong_times = []; + var start_time; + window.setInterval(function() { + start_time = (new Date).getTime(); + socket.emit('my_ping'); + }, 1000); + + // Handler for the "pong" message. When the pong is received, the + // time from the ping is stored, and the average of the last 30 + // samples is average and displayed. + socket.on('my_pong', function() { + var latency = (new Date).getTime() - start_time; + ping_pong_times.push(latency); + ping_pong_times = ping_pong_times.slice(-30); // keep last 30 samples + var sum = 0; + for (var i = 0; i < ping_pong_times.length; i++) + sum += ping_pong_times[i]; + $('#ping-pong').text(Math.round(10 * sum / ping_pong_times.length) / 10); + }); + + // Handlers for the different forms in the page. + // These accept data from the user and send it to the server in a + // variety of ways + $('form#emit').submit(function(event) { + socket.emit('my_event', {data: $('#emit_data').val()}); + return false; + }); +}); \ No newline at end of file diff --git a/flask__websocket/sending_all_from_while_in_thread/static/jquery.js b/flask__websocket/sending_all_from_while_in_thread/static/jquery.js new file mode 100644 index 000000000..d4b67f7e6 --- /dev/null +++ b/flask__websocket/sending_all_from_while_in_thread/static/jquery.js @@ -0,0 +1,10308 @@ +/*! + * jQuery JavaScript Library v1.11.1 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2014-05-01T17:42Z + */ + +(function( global, factory ) { + + if ( typeof module === "object" && typeof module.exports === "object" ) { + // For CommonJS and CommonJS-like environments where a proper window is present, + // execute the factory and get jQuery + // For environments that do not inherently posses a window with a document + // (such as Node.js), expose a jQuery-making factory as module.exports + // This accentuates the need for the creation of a real window + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Can't do this because several apps including ASP.NET trace +// the stack via arguments.caller.callee and Firefox dies if +// you try to trace through "use strict" call chains. (#13335) +// Support: Firefox 18+ +// + +var deletedIds = []; + +var slice = deletedIds.slice; + +var concat = deletedIds.concat; + +var push = deletedIds.push; + +var indexOf = deletedIds.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var support = {}; + + + +var + version = "1.11.1", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }, + + // Support: Android<4.1, IE<9 + // Make sure we trim BOM and NBSP + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }; + +jQuery.fn = jQuery.prototype = { + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // Start with an empty selector + selector: "", + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num != null ? + + // Return just the one element from the set + ( num < 0 ? this[ num + this.length ] : this[ num ] ) : + + // Return all the elements in a clean array + slice.call( this ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + ret.context = this.context; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: deletedIds.sort, + splice: deletedIds.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var src, copyIsArray, copy, name, options, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + isWindow: function( obj ) { + /* jshint eqeqeq: false */ + return obj != null && obj == obj.window; + }, + + isNumeric: function( obj ) { + // parseFloat NaNs numeric-cast false positives (null|true|false|"") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + return !jQuery.isArray( obj ) && obj - parseFloat( obj ) >= 0; + }, + + isEmptyObject: function( obj ) { + var name; + for ( name in obj ) { + return false; + } + return true; + }, + + isPlainObject: function( obj ) { + var key; + + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Support: IE<9 + // Handle iteration over inherited properties before own properties. + if ( support.ownLast ) { + for ( key in obj ) { + return hasOwn.call( obj, key ); + } + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + type: function( obj ) { + if ( obj == null ) { + return obj + ""; + } + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call(obj) ] || "object" : + typeof obj; + }, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && jQuery.trim( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + // args is for internal usage only + each: function( obj, callback, args ) { + var value, + i = 0, + length = obj.length, + isArray = isArraylike( obj ); + + if ( args ) { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { + break; + } + } + } else { + for ( i in obj ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { + break; + } + } + } else { + for ( i in obj ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { + break; + } + } + } + } + + return obj; + }, + + // Support: Android<4.1, IE<9 + trim: function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArraylike( Object(arr) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + var len; + + if ( arr ) { + if ( indexOf ) { + return indexOf.call( arr, elem, i ); + } + + len = arr.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in arr && arr[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + while ( j < len ) { + first[ i++ ] = second[ j++ ]; + } + + // Support: IE<9 + // Workaround casting of .length to NaN on otherwise arraylike objects (e.g., NodeLists) + if ( len !== len ) { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, + i = 0, + length = elems.length, + isArray = isArraylike( elems ), + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var args, proxy, tmp; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + now: function() { + return +( new Date() ); + }, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +}); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +function isArraylike( obj ) { + var length = obj.length, + type = jQuery.type( obj ); + + if ( type === "function" || jQuery.isWindow( obj ) ) { + return false; + } + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v1.10.19 + * http://sizzlejs.com/ + * + * Copyright 2013 jQuery Foundation, Inc. and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2014-04-18 + */ +(function( window ) { + +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + -(new Date()), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // General-purpose constants + strundefined = typeof undefined, + MAX_NEGATIVE = 1 << 31, + + // Instance methods + hasOwn = ({}).hasOwnProperty, + arr = [], + pop = arr.pop, + push_native = arr.push, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf if we can't use a native one + indexOf = arr.indexOf || function( elem ) { + var i = 0, + len = this.length; + for ( ; i < len; i++ ) { + if ( this[i] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + // http://www.w3.org/TR/css3-syntax/#characters + characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", + + // Loosely modeled on CSS identifier characters + // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors + // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = characterEncoding.replace( "w", "w#" ), + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + characterEncoding + ")(?:" + whitespace + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + + "*\\]", + + pseudos = ":(" + characterEncoding + ")(?:\\((" + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), + + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + characterEncoding + ")" ), + "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), + "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + rescape = /'|\\/g, + + // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), + funescape = function( _, escaped, escapedWhitespace ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + // Support: Firefox<24 + // Workaround erroneous numeric interpretation of +"0x" + return high !== high || escapedWhitespace ? + escaped : + high < 0 ? + // BMP codepoint + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }; + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + (arr = slice.call( preferredDoc.childNodes )), + preferredDoc.childNodes + ); + // Support: Android<4.0 + // Detect silently failing push.apply + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + push_native.apply( target, slice.call(els) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + // Can't trust NodeList.length + while ( (target[j++] = els[i++]) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var match, elem, m, nodeType, + // QSA vars + i, groups, old, nid, newContext, newSelector; + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + + context = context || document; + results = results || []; + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { + return []; + } + + if ( documentIsHTML && !seed ) { + + // Shortcuts + if ( (match = rquickExpr.exec( selector )) ) { + // Speed-up: Sizzle("#ID") + if ( (m = match[1]) ) { + if ( nodeType === 9 ) { + elem = context.getElementById( m ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document (jQuery #6963) + if ( elem && elem.parentNode ) { + // Handle the case where IE, Opera, and Webkit return items + // by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + } else { + // Context is not a document + if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && + contains( context, elem ) && elem.id === m ) { + results.push( elem ); + return results; + } + } + + // Speed-up: Sizzle("TAG") + } else if ( match[2] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Speed-up: Sizzle(".CLASS") + } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // QSA path + if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + nid = old = expando; + newContext = context; + newSelector = nodeType === 9 && selector; + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + groups = tokenize( selector ); + + if ( (old = context.getAttribute("id")) ) { + nid = old.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", nid ); + } + nid = "[id='" + nid + "'] "; + + i = groups.length; + while ( i-- ) { + groups[i] = nid + toSelector( groups[i] ); + } + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; + newSelector = groups.join(","); + } + + if ( newSelector ) { + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch(qsaError) { + } finally { + if ( !old ) { + context.removeAttribute("id"); + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {Function(string, Object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key + " " ] = value); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created div and expects a boolean result + */ +function assert( fn ) { + var div = document.createElement("div"); + + try { + return !!fn( div ); + } catch (e) { + return false; + } finally { + // Remove from its parent by default + if ( div.parentNode ) { + div.parentNode.removeChild( div ); + } + // release memory in IE + div = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split("|"), + i = attrs.length; + + while ( i-- ) { + Expr.attrHandle[ arr[i] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + ( ~b.sourceIndex || MAX_NEGATIVE ) - + ( ~a.sourceIndex || MAX_NEGATIVE ); + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== strundefined && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, + doc = node ? node.ownerDocument || node : preferredDoc, + parent = doc.defaultView; + + // If no document and documentElement is available, return + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Set our document + document = doc; + docElem = doc.documentElement; + + // Support tests + documentIsHTML = !isXML( doc ); + + // Support: IE>8 + // If iframe document is assigned to "document" variable and if iframe has been reloaded, + // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 + // IE6-8 do not support the defaultView property so parent will be undefined + if ( parent && parent !== parent.top ) { + // IE11 does not have attachEvent, so all must suffer + if ( parent.addEventListener ) { + parent.addEventListener( "unload", function() { + setDocument(); + }, false ); + } else if ( parent.attachEvent ) { + parent.attachEvent( "onunload", function() { + setDocument(); + }); + } + } + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans) + support.attributes = assert(function( div ) { + div.className = "i"; + return !div.getAttribute("className"); + }); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert(function( div ) { + div.appendChild( doc.createComment("") ); + return !div.getElementsByTagName("*").length; + }); + + // Check if getElementsByClassName can be trusted + support.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) { + div.innerHTML = "
"; + + // Support: Safari<4 + // Catch class over-caching + div.firstChild.className = "i"; + // Support: Opera<10 + // Catch gEBCN failure to find non-leading classes + return div.getElementsByClassName("i").length === 2; + }); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert(function( div ) { + docElem.appendChild( div ).id = expando; + return !doc.getElementsByName || !doc.getElementsByName( expando ).length; + }); + + // ID find and filter + if ( support.getById ) { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== strundefined && documentIsHTML ) { + var m = context.getElementById( id ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [ m ] : []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + } else { + // Support: IE6/7 + // getElementById is not reliable as a find shortcut + delete Expr.find["ID"]; + + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + } + + // Tag + Expr.find["TAG"] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== strundefined ) { + return context.getElementsByTagName( tag ); + } + } : + function( tag, context ) { + var elem, + tmp = [], + i = 0, + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( (elem = results[i++]) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See http://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + div.innerHTML = ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( div.querySelectorAll("[msallowclip^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + }); + + assert(function( div ) { + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = doc.createElement("input"); + input.setAttribute( "type", "hidden" ); + div.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( div.querySelectorAll("[name=d]").length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + div.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { + + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( div, "div" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( div, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully does not implement inclusive descendent + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + + // Choose the first element that is related to our preferred document + if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + return -1; + } + if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + return a === doc ? -1 : + b === doc ? 1 : + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); + } + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + return doc; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + if ( support.matchesSelector && documentIsHTML && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch(e) {} + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + (val = elem.getAttributeNode(name)) && val.specified ? + val.value : + null; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( (elem = results[i++]) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + while ( (node = elem[i++]) ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[6] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[3] ) { + match[2] = match[4] || match[5] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { return true; } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, context, xml ) { + var cache, outerCache, node, diff, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + // Seek `elem` from a previously-cached index + outerCache = parent[ expando ] || (parent[ expando ] = {}); + cache = outerCache[ type ] || []; + nodeIndex = cache[0] === dirruns && cache[1]; + diff = cache[0] === dirruns && cache[2]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( (node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + // Use previously-cached element index if available + } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { + diff = cache[1]; + + // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) + } else { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { + // Cache the index of each encountered element + if ( useCache ) { + (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf.call( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifier + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": function( elem ) { + return elem.disabled === false; + }, + + "disabled": function( elem ) { + return elem.disabled === true; + }, + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( (tokens = []) ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push({ + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + }); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push({ + value: matched, + type: type, + matches: match + }); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && dir === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + if ( (oldCache = outerCache[ dir ]) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return (newCache[ 2 ] = oldCache[ 2 ]); + } else { + // Reuse newcache so results back-propagate to previous elements + outerCache[ dir ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { + return true; + } + } + } + } + } + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf.call( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + len = elems.length; + + if ( outermost ) { + outermostContext = context !== document && context; + } + + // Add elements passing elementMatchers directly to results + // Keep `i` a string if there are no elements so `matchedCount` will be "00" below + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + while ( (matcher = elementMatchers[j++]) ) { + if ( matcher( elem, context, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // Apply set filters to unmatched elements + matchedCount += i; + if ( bySet && i !== matchedCount ) { + j = 0; + while ( (matcher = setMatchers[j++]) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( (selector = compiled.selector || selector) ); + + results = results || []; + + // Try to minimize operations if there is no seed and only one group + if ( match.length === 1 ) { + + // Take a shortcut and set the context if the root selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + support.getById && context.nodeType === 9 && documentIsHTML && + Expr.relative[ tokens[1].type ] ) { + + context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; + +// Support: Chrome<14 +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert(function( div1 ) { + // Should return 1, but returns 4 (following) + return div1.compareDocumentPosition( document.createElement("div") ) & 1; +}); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert(function( div ) { + div.innerHTML = ""; + return div.firstChild.getAttribute("href") === "#" ; +}) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + }); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert(function( div ) { + div.innerHTML = ""; + div.firstChild.setAttribute( "value", "" ); + return div.firstChild.getAttribute( "value" ) === ""; +}) ) { + addHandle( "value", function( elem, name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + }); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert(function( div ) { + return div.getAttribute("disabled") == null; +}) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + null; + } + }); +} + +return Sizzle; + +})( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.pseudos; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + + +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/); + + + +var risSimple = /^.[^:#\[\.,]*$/; + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + /* jshint -W018 */ + return !!qualifier.call( elem, i, elem ) !== not; + }); + + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + }); + + } + + if ( typeof qualifier === "string" ) { + if ( risSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + qualifier = jQuery.filter( qualifier, elements ); + } + + return jQuery.grep( elements, function( elem ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not; + }); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + })); +}; + +jQuery.fn.extend({ + find: function( selector ) { + var i, + ret = [], + self = this, + len = self.length; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }) ); + } + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = this.selector ? this.selector + " " + selector : selector; + return ret; + }, + filter: function( selector ) { + return this.pushStack( winnow(this, selector || [], false) ); + }, + not: function( selector ) { + return this.pushStack( winnow(this, selector || [], true) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +}); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // Use the correct document accordingly with window argument (sandbox) + document = window.document, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + init = jQuery.fn.init = function( selector, context ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + + // scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[1], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return typeof rootjQuery.ready !== "undefined" ? + rootjQuery.ready( selector ) : + // Execute immediately if ready is not present + selector( jQuery ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.extend({ + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +jQuery.fn.extend({ + has: function( target ) { + var i, + targets = jQuery( target, this ), + len = targets.length; + + return this.filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { + // Always skip document fragments + if ( cur.nodeType < 11 && (pos ? + pos.index(cur) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector(cur, selectors)) ) { + + matched.push( cur ); + break; + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.unique( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + } +}); + +function sibling( cur, dir ) { + do { + cur = cur[ dir ]; + } while ( cur && cur.nodeType !== 1 ); + + return cur; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + if ( this.length > 1 ) { + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + ret = jQuery.unique( ret ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + } + + return this.pushStack( ret ); + }; +}); +var rnotwhite = (/\S+/g); + + + +// String to Object options format cache +var optionsCache = {}; + +// Convert String-formatted options into Object-formatted ones and store in cache +function createOptions( options ) { + var object = optionsCache[ options ] = {}; + jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + }); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + ( optionsCache[ options ] || createOptions( options ) ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // First callback to fire (used internally by add and fireWith) + firingStart, + // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = !options.once && [], + // Fire callbacks + fire = function( data ) { + memory = options.memory && data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { + memory = false; // To prevent further calls using add + break; + } + } + firing = false; + if ( list ) { + if ( stack ) { + if ( stack.length ) { + fire( stack.shift() ); + } + } else if ( memory ) { + list = []; + } else { + self.disable(); + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + // First, we save the current length + var start = list.length; + (function add( args ) { + jQuery.each( args, function( _, arg ) { + var type = jQuery.type( arg ); + if ( type === "function" ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && type !== "string" ) { + // Inspect recursively + add( arg ); + } + }); + })( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + // Handle firing indexes + if ( firing ) { + if ( index <= firingLength ) { + firingLength--; + } + if ( index <= firingIndex ) { + firingIndex--; + } + } + } + }); + } + return this; + }, + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); + }, + // Remove all callbacks from the list + empty: function() { + list = []; + firingLength = 0; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( list && ( !fired || stack ) ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +jQuery.extend({ + + Deferred: function( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], + [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], + [ "notify", "progress", jQuery.Callbacks("memory") ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred(function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[1] ](function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[1] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[0] ] = function() { + deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[0] + "With" ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( values === progressValues ) { + deferred.notifyWith( contexts, values ); + + } else if ( !(--remaining) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +}); + + +// The deferred used on DOM ready +var readyList; + +jQuery.fn.ready = function( fn ) { + // Add the callback + jQuery.ready.promise().done( fn ); + + return this; +}; + +jQuery.extend({ + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.triggerHandler ) { + jQuery( document ).triggerHandler( "ready" ); + jQuery( document ).off( "ready" ); + } + } +}); + +/** + * Clean-up method for dom ready events + */ +function detach() { + if ( document.addEventListener ) { + document.removeEventListener( "DOMContentLoaded", completed, false ); + window.removeEventListener( "load", completed, false ); + + } else { + document.detachEvent( "onreadystatechange", completed ); + window.detachEvent( "onload", completed ); + } +} + +/** + * The ready event handler and self cleanup method + */ +function completed() { + // readyState === "complete" is good enough for us to call the dom ready in oldIE + if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) { + detach(); + jQuery.ready(); + } +} + +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called after the browser event has already occurred. + // we once tried to use readyState "interactive" here, but it caused issues like the one + // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + setTimeout( jQuery.ready ); + + // Standards-based browsers support DOMContentLoaded + } else if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed, false ); + + // If IE event model is used + } else { + // Ensure firing before onload, maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", completed ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", completed ); + + // If IE and not a frame + // continually check to see if the document is ready + var top = false; + + try { + top = window.frameElement == null && document.documentElement; + } catch(e) {} + + if ( top && top.doScroll ) { + (function doScrollCheck() { + if ( !jQuery.isReady ) { + + try { + // Use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + top.doScroll("left"); + } catch(e) { + return setTimeout( doScrollCheck, 50 ); + } + + // detach all dom ready events + detach(); + + // and execute any waiting functions + jQuery.ready(); + } + })(); + } + } + } + return readyList.promise( obj ); +}; + + +var strundefined = typeof undefined; + + + +// Support: IE<9 +// Iteration over object's inherited properties before its own +var i; +for ( i in jQuery( support ) ) { + break; +} +support.ownLast = i !== "0"; + +// Note: most support tests are defined in their respective modules. +// false until the test is run +support.inlineBlockNeedsLayout = false; + +// Execute ASAP in case we need to set body.style.zoom +jQuery(function() { + // Minified: var a,b,c,d + var val, div, body, container; + + body = document.getElementsByTagName( "body" )[ 0 ]; + if ( !body || !body.style ) { + // Return for frameset docs that don't have a body + return; + } + + // Setup + div = document.createElement( "div" ); + container = document.createElement( "div" ); + container.style.cssText = "position:absolute;border:0;width:0;height:0;top:0;left:-9999px"; + body.appendChild( container ).appendChild( div ); + + if ( typeof div.style.zoom !== strundefined ) { + // Support: IE<8 + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + div.style.cssText = "display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1"; + + support.inlineBlockNeedsLayout = val = div.offsetWidth === 3; + if ( val ) { + // Prevent IE 6 from affecting layout for positioned elements #11048 + // Prevent IE from shrinking the body in IE 7 mode #12869 + // Support: IE<8 + body.style.zoom = 1; + } + } + + body.removeChild( container ); +}); + + + + +(function() { + var div = document.createElement( "div" ); + + // Execute the test only if not already executed in another module. + if (support.deleteExpando == null) { + // Support: IE<9 + support.deleteExpando = true; + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + } + + // Null elements to avoid leaks in IE. + div = null; +})(); + + +/** + * Determines whether an object can have data + */ +jQuery.acceptData = function( elem ) { + var noData = jQuery.noData[ (elem.nodeName + " ").toLowerCase() ], + nodeType = +elem.nodeType || 1; + + // Do not set data on non-element DOM nodes because it will not be cleared (#8335). + return nodeType !== 1 && nodeType !== 9 ? + false : + + // Nodes accept data unless otherwise specified; rejection can be conditional + !noData || noData !== true && elem.getAttribute("classid") === noData; +}; + + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /([A-Z])/g; + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + var name; + for ( name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} + +function internalData( elem, name, data, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var ret, thisCache, + internalKey = jQuery.expando, + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + id = elem[ internalKey ] = deletedIds.pop() || jQuery.guid++; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + // Avoid exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + cache[ id ] = isNode ? {} : { toJSON: jQuery.noop }; + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( typeof name === "string" ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; +} + +function internalRemoveData( elem, name, pvt ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split(" "); + } + } + } else { + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = name.concat( jQuery.map( name, jQuery.camelCase ) ); + } + + i = name.length; + while ( i-- ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject( cache[ id ] ) ) { + return; + } + } + + // Destroy the cache + if ( isNode ) { + jQuery.cleanData( [ elem ], true ); + + // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) + /* jshint eqeqeq: false */ + } else if ( support.deleteExpando || cache != cache.window ) { + /* jshint eqeqeq: true */ + delete cache[ id ]; + + // When all else fails, null + } else { + cache[ id ] = null; + } +} + +jQuery.extend({ + cache: {}, + + // The following elements (space-suffixed to avoid Object.prototype collisions) + // throw uncatchable exceptions if you attempt to set expando properties + noData: { + "applet ": true, + "embed ": true, + // ...but Flash objects (which have this classid) *can* handle expandos + "object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data ) { + return internalData( elem, name, data ); + }, + + removeData: function( elem, name ) { + return internalRemoveData( elem, name ); + }, + + // For internal use only. + _data: function( elem, name, data ) { + return internalData( elem, name, data, true ); + }, + + _removeData: function( elem, name ) { + return internalRemoveData( elem, name, true ); + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var i, name, data, + elem = this[0], + attrs = elem && elem.attributes; + + // Special expections of .data basically thwart jQuery.access, + // so implement the relevant behavior ourselves + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE11+ + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.slice(5) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + return arguments.length > 1 ? + + // Sets one value + this.each(function() { + jQuery.data( this, key, value ); + }) : + + // Gets one value + // Try to fetch any internally stored data first + elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : undefined; + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + + +jQuery.extend({ + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray(data) ) { + queue = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // not intended for public consumption - generates a queueHooks object, or returns the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return jQuery._data( elem, key ) || jQuery._data( elem, key, { + empty: jQuery.Callbacks("once memory").add(function() { + jQuery._removeData( elem, type + "queue" ); + jQuery._removeData( elem, key ); + }) + }); + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + // ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = jQuery._data( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +}); +var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source; + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var isHidden = function( elem, el ) { + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); + }; + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + length = elems.length, + bulk = key == null; + + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < length; i++ ) { + fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); + } + } + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + length ? fn( elems[0], key ) : emptyGet; +}; +var rcheckableType = (/^(?:checkbox|radio)$/i); + + + +(function() { + // Minified: var a,b,c + var input = document.createElement( "input" ), + div = document.createElement( "div" ), + fragment = document.createDocumentFragment(); + + // Setup + div.innerHTML = "
a"; + + // IE strips leading whitespace when .innerHTML is used + support.leadingWhitespace = div.firstChild.nodeType === 3; + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + support.tbody = !div.getElementsByTagName( "tbody" ).length; + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + support.htmlSerialize = !!div.getElementsByTagName( "link" ).length; + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + support.html5Clone = + document.createElement( "nav" ).cloneNode( true ).outerHTML !== "<:nav>"; + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + input.type = "checkbox"; + input.checked = true; + fragment.appendChild( input ); + support.appendChecked = input.checked; + + // Make sure textarea (and checkbox) defaultValue is properly cloned + // Support: IE6-IE11+ + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; + + // #11217 - WebKit loses check when the name is after the checked attribute + fragment.appendChild( div ); + div.innerHTML = ""; + + // Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3 + // old WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE<9 + // Opera does not clone events (and typeof div.attachEvent === undefined). + // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() + support.noCloneEvent = true; + if ( div.attachEvent ) { + div.attachEvent( "onclick", function() { + support.noCloneEvent = false; + }); + + div.cloneNode( true ).click(); + } + + // Execute the test only if not already executed in another module. + if (support.deleteExpando == null) { + // Support: IE<9 + support.deleteExpando = true; + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + } +})(); + + +(function() { + var i, eventName, + div = document.createElement( "div" ); + + // Support: IE<9 (lack submit/change bubble), Firefox 23+ (lack focusin event) + for ( i in { submit: true, change: true, focusin: true }) { + eventName = "on" + i; + + if ( !(support[ i + "Bubbles" ] = eventName in window) ) { + // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) + div.setAttribute( eventName, "t" ); + support[ i + "Bubbles" ] = div.attributes[ eventName ].expando === false; + } + } + + // Null elements to avoid leaks in IE. + div = null; +})(); + + +var rformElems = /^(?:input|select|textarea)$/i, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + var tmp, events, t, handleObjIn, + special, eventHandle, handleObj, + handlers, type, namespaces, origType, + elemData = jQuery._data( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !(events = elemData.events) ) { + events = elemData.events = {}; + } + if ( !(eventHandle = elemData.handle) ) { + eventHandle = elemData.handle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== strundefined && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !(handlers = events[ type ]) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + var j, handleObj, tmp, + origCount, t, events, + special, handlers, type, + namespaces, origType, + elemData = jQuery.hasData( elem ) && jQuery._data( elem ); + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + delete elemData.handle; + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery._removeData( elem, "events" ); + } + }, + + trigger: function( event, data, elem, onlyHandlers ) { + var handle, ontype, cur, + bubbleType, special, tmp, i, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; + + cur = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf(".") >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf(":") < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join("."); + event.namespace_re = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === (elem.ownerDocument || document) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { + + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && jQuery.acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && + jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + try { + elem[ type ](); + } catch ( e ) { + // IE<9 dies on focus/blur to hidden element (#1486,#12518) + // only reproducible on winXP IE8 native, not IE9 in IE8 mode + } + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, ret, handleObj, matched, j, + handlerQueue = [], + args = slice.call( arguments ), + handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( (event.result = ret) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var sel, handleObj, matches, i, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { + + /* jshint eqeqeq: false */ + for ( ; cur != this; cur = cur.parentNode || this ) { + /* jshint eqeqeq: true */ + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) >= 0 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, handlers: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); + } + + return handlerQueue; + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: IE<9 + // Fix target property (#1925) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Support: Chrome 23+, Safari? + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // Support: IE<9 + // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) + event.metaKey = !!event.metaKey; + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var body, eventDoc, doc, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + special: { + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== safeActiveElement() && this.focus ) { + try { + this.focus(); + return false; + } catch ( e ) { + // Support: IE<9 + // If we error on focus to hidden element (#1486, #12518), + // let .trigger() run the handlers + } + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === safeActiveElement() && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { + this.click(); + return false; + } + }, + + // For cross-browser consistency, don't fire native .click() on links + _default: function( event ) { + return jQuery.nodeName( event.target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + var name = "on" + type; + + if ( elem.detachEvent ) { + + // #8545, #7054, preventing memory leaks for custom events in IE6-8 + // detachEvent needed property on element, by name of that event, to properly expose it to GC + if ( typeof elem[ name ] === strundefined ) { + elem[ name ] = null; + } + + elem.detachEvent( name, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + // Support: IE < 9, Android < 4.0 + src.returnValue === false ? + returnTrue : + returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + if ( !e ) { + return; + } + + // If preventDefault exists, run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // Support: IE + // Otherwise set the returnValue property of the original event to false + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + if ( !e ) { + return; + } + // If stopPropagation exists, run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + + // Support: IE + // Set the cancelBubble property of the original event to true + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && e.stopImmediatePropagation ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !jQuery._data( form, "submitBubbles" ) ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + jQuery._data( form, "submitBubbles", true ); + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + } + // Allow triggered, simulated change events (#11500) + jQuery.event.simulate( "change", this, event, true ); + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + jQuery._data( elem, "changeBubbles", true ); + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return !rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler on the document while someone wants focusin/focusout + var handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + var doc = this.ownerDocument || this, + attaches = jQuery._data( doc, fix ); + + if ( !attaches ) { + doc.addEventListener( orig, handler, true ); + } + jQuery._data( doc, fix, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this, + attaches = jQuery._data( doc, fix ) - 1; + + if ( !attaches ) { + doc.removeEventListener( orig, handler, true ); + jQuery._removeData( doc, fix ); + } else { + jQuery._data( doc, fix, attaches ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var type, origFn; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + var elem = this[0]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +}); + + +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, + rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + rtagName = /<([\w:]+)/, + rtbody = /\s*$/g, + + // We have to close these tags to support XHTML (#13200) + wrapMap = { + option: [ 1, "" ], + legend: [ 1, "
", "
" ], + area: [ 1, "", "" ], + param: [ 1, "", "" ], + thead: [ 1, "", "
" ], + tr: [ 2, "", "
" ], + col: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, + // unless wrapped in a div with non-breaking characters in front of it. + _default: support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
", "
" ] + }, + safeFragment = createSafeFragment( document ), + fragmentDiv = safeFragment.appendChild( document.createElement("div") ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +function getAll( context, tag ) { + var elems, elem, + i = 0, + found = typeof context.getElementsByTagName !== strundefined ? context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== strundefined ? context.querySelectorAll( tag || "*" ) : + undefined; + + if ( !found ) { + for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { + if ( !tag || jQuery.nodeName( elem, tag ) ) { + found.push( elem ); + } else { + jQuery.merge( found, getAll( elem, tag ) ); + } + } + } + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], found ) : + found; +} + +// Used in buildFragment, fixes the defaultChecked property +function fixDefaultChecked( elem ) { + if ( rcheckableType.test( elem.type ) ) { + elem.defaultChecked = elem.checked; + } +} + +// Support: IE<8 +// Manipulating tables requires a tbody +function manipulationTarget( elem, content ) { + return jQuery.nodeName( elem, "table" ) && + jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? + + elem.getElementsByTagName("tbody")[0] || + elem.appendChild( elem.ownerDocument.createElement("tbody") ) : + elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = (jQuery.find.attr( elem, "type" ) !== null) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + if ( match ) { + elem.type = match[1]; + } else { + elem.removeAttribute("type"); + } + return elem; +} + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var elem, + i = 0; + for ( ; (elem = elems[i]) != null; i++ ) { + jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); + } +} + +function cloneCopyEvent( src, dest ) { + + if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { + return; + } + + var type, i, l, + oldData = jQuery._data( src ), + curData = jQuery._data( dest, oldData ), + events = oldData.events; + + if ( events ) { + delete curData.handle; + curData.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + + // make the cloned public data object a copy from the original + if ( curData.data ) { + curData.data = jQuery.extend( {}, curData.data ); + } +} + +function fixCloneNodeIssues( src, dest ) { + var nodeName, e, data; + + // We do not need to do anything for non-Elements + if ( dest.nodeType !== 1 ) { + return; + } + + nodeName = dest.nodeName.toLowerCase(); + + // IE6-8 copies events bound via attachEvent when using cloneNode. + if ( !support.noCloneEvent && dest[ jQuery.expando ] ) { + data = jQuery._data( dest ); + + for ( e in data.events ) { + jQuery.removeEvent( dest, e, data.handle ); + } + + // Event data gets referenced instead of copied if the expando gets copied too + dest.removeAttribute( jQuery.expando ); + } + + // IE blanks contents when cloning scripts, and tries to evaluate newly-set text + if ( nodeName === "script" && dest.text !== src.text ) { + disableScript( dest ).text = src.text; + restoreScript( dest ); + + // IE6-10 improperly clones children of object elements using classid. + // IE10 throws NoModificationAllowedError if parent is null, #12132. + } else if ( nodeName === "object" ) { + if ( dest.parentNode ) { + dest.outerHTML = src.outerHTML; + } + + // This path appears unavoidable for IE9. When cloning an object + // element in IE9, the outerHTML strategy above is not sufficient. + // If the src has innerHTML and the destination does not, + // copy the src.innerHTML into the dest.innerHTML. #10324 + if ( support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { + dest.innerHTML = src.innerHTML; + } + + } else if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + // IE6-8 fails to persist the checked state of a cloned checkbox + // or radio button. Worse, IE6-7 fail to give the cloned element + // a checked appearance if the defaultChecked value isn't also set + + dest.defaultChecked = dest.checked = src.checked; + + // IE6-7 get confused and end up setting the value of a cloned + // checkbox/radio button to an empty string instead of "on" + if ( dest.value !== src.value ) { + dest.value = src.value; + } + + // IE6-8 fails to return the selected option to the default selected + // state when cloning options + } else if ( nodeName === "option" ) { + dest.defaultSelected = dest.selected = src.defaultSelected; + + // IE6-8 fails to set the defaultValue to the correct value when + // cloning other types of input fields + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var destElements, node, clone, i, srcElements, + inPage = jQuery.contains( elem.ownerDocument, elem ); + + if ( support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { + clone = elem.cloneNode( true ); + + // IE<=8 does not properly clone detached, unknown element nodes + } else { + fragmentDiv.innerHTML = elem.outerHTML; + fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); + } + + if ( (!support.noCloneEvent || !support.noCloneChecked) && + (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { + + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + // Fix all IE cloning issues + for ( i = 0; (node = srcElements[i]) != null; ++i ) { + // Ensure that the destination node is not null; Fixes #9587 + if ( destElements[i] ) { + fixCloneNodeIssues( node, destElements[i] ); + } + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0; (node = srcElements[i]) != null; i++ ) { + cloneCopyEvent( node, destElements[i] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + destElements = srcElements = node = null; + + // Return the cloned set + return clone; + }, + + buildFragment: function( elems, context, scripts, selection ) { + var j, elem, contains, + tmp, tag, tbody, wrap, + l = elems.length, + + // Ensure a safe fragment + safe = createSafeFragment( context ), + + nodes = [], + i = 0; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || safe.appendChild( context.createElement("div") ); + + // Deserialize a standard representation + tag = (rtagName.exec( elem ) || [ "", "" ])[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + + tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1>" ) + wrap[2]; + + // Descend through wrappers to the right content + j = wrap[0]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Manually add leading whitespace removed by IE + if ( !support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); + } + + // Remove IE's autoinserted from table fragments + if ( !support.tbody ) { + + // String was a , *may* have spurious + elem = tag === "table" && !rtbody.test( elem ) ? + tmp.firstChild : + + // String was a bare or + wrap[1] === "
" && !rtbody.test( elem ) ? + tmp : + 0; + + j = elem && elem.childNodes.length; + while ( j-- ) { + if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { + elem.removeChild( tbody ); + } + } + } + + jQuery.merge( nodes, tmp.childNodes ); + + // Fix #12392 for WebKit and IE > 9 + tmp.textContent = ""; + + // Fix #12392 for oldIE + while ( tmp.firstChild ) { + tmp.removeChild( tmp.firstChild ); + } + + // Remember the top-level container for proper cleanup + tmp = safe.lastChild; + } + } + } + + // Fix #11356: Clear elements from fragment + if ( tmp ) { + safe.removeChild( tmp ); + } + + // Reset defaultChecked for any radios and checkboxes + // about to be appended to the DOM in IE 6/7 (#8060) + if ( !support.appendChecked ) { + jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); + } + + i = 0; + while ( (elem = nodes[ i++ ]) ) { + + // #4087 - If origin and destination elements are the same, and this is + // that element, do not do anything + if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( safe.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( (elem = tmp[ j++ ]) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + tmp = null; + + return safe; + }, + + cleanData: function( elems, /* internal */ acceptData ) { + var elem, type, id, data, + i = 0, + internalKey = jQuery.expando, + cache = jQuery.cache, + deleteExpando = support.deleteExpando, + special = jQuery.event.special; + + for ( ; (elem = elems[i]) != null; i++ ) { + if ( acceptData || jQuery.acceptData( elem ) ) { + + id = elem[ internalKey ]; + data = id && cache[ id ]; + + if ( data ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Remove cache only if it was not already removed by jQuery.event.remove + if ( cache[ id ] ) { + + delete cache[ id ]; + + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( deleteExpando ) { + delete elem[ internalKey ]; + + } else if ( typeof elem.removeAttribute !== strundefined ) { + elem.removeAttribute( internalKey ); + + } else { + elem[ internalKey ] = null; + } + + deletedIds.push( id ); + } + } + } + } + } +}); + +jQuery.fn.extend({ + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); + }, null, value, arguments.length ); + }, + + append: function() { + return this.domManip( arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + }); + }, + + prepend: function() { + return this.domManip( arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + }); + }, + + before: function() { + return this.domManip( arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + }); + }, + + after: function() { + return this.domManip( arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + }); + }, + + remove: function( selector, keepData /* Internal Use Only */ ) { + var elem, + elems = selector ? jQuery.filter( selector, this ) : this, + i = 0; + + for ( ; (elem = elems[i]) != null; i++ ) { + + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem ) ); + } + + if ( elem.parentNode ) { + if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { + setGlobalEval( getAll( elem, "script" ) ); + } + elem.parentNode.removeChild( elem ); + } + } + + return this; + }, + + empty: function() { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + } + + // Remove any remaining nodes + while ( elem.firstChild ) { + elem.removeChild( elem.firstChild ); + } + + // If this is a select, ensure that it displays empty (#12336) + // Support: IE<9 + if ( elem.options && jQuery.nodeName( elem, "select" ) ) { + elem.options.length = 0; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map(function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + }); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined ) { + return elem.nodeType === 1 ? + elem.innerHTML.replace( rinlinejQuery, "" ) : + undefined; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + ( support.htmlSerialize || !rnoshimcache.test( value ) ) && + ( support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && + !wrapMap[ (rtagName.exec( value ) || [ "", "" ])[ 1 ].toLowerCase() ] ) { + + value = value.replace( rxhtmlTag, "<$1>" ); + + try { + for (; i < l; i++ ) { + // Remove element nodes and prevent memory leaks + elem = this[i] || {}; + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch(e) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var arg = arguments[ 0 ]; + + // Make the changes, replacing each context element with the new content + this.domManip( arguments, function( elem ) { + arg = this.parentNode; + + jQuery.cleanData( getAll( this ) ); + + if ( arg ) { + arg.replaceChild( elem, this ); + } + }); + + // Force removal if there was no new content (e.g., from empty arguments) + return arg && (arg.length || arg.nodeType) ? this : this.remove(); + }, + + detach: function( selector ) { + return this.remove( selector, true ); + }, + + domManip: function( args, callback ) { + + // Flatten any nested arrays + args = concat.apply( [], args ); + + var first, node, hasScripts, + scripts, doc, fragment, + i = 0, + l = this.length, + set = this, + iNoClone = l - 1, + value = args[0], + isFunction = jQuery.isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return this.each(function( index ) { + var self = set.eq( index ); + if ( isFunction ) { + args[0] = value.call( this, index, self.html() ); + } + self.domManip( args, callback ); + }); + } + + if ( l ) { + fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + if ( first ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( this[i], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { + + if ( node.src ) { + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl ) { + jQuery._evalUrl( node.src ); + } + } else { + jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); + } + } + } + } + + // Fix #11809: Avoid leaking memory + fragment = first = null; + } + } + + return this; + } +}); + +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + i = 0, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone(true); + jQuery( insert[i] )[ original ]( elems ); + + // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +}); + + +var iframe, + elemdisplay = {}; + +/** + * Retrieve the actual display of a element + * @param {String} name nodeName of the element + * @param {Object} doc Document object + */ +// Called only from within defaultDisplay +function actualDisplay( name, doc ) { + var style, + elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), + + // getDefaultComputedStyle might be reliably used only on attached element + display = window.getDefaultComputedStyle && ( style = window.getDefaultComputedStyle( elem[ 0 ] ) ) ? + + // Use of this method is a temporary fix (more like optmization) until something better comes along, + // since it was removed from specification and supported only in FF + style.display : jQuery.css( elem[ 0 ], "display" ); + + // We don't have any data stored on the element, + // so use "detach" method as fast way to get rid of the element + elem.detach(); + + return display; +} + +/** + * Try to determine the default display value of an element + * @param {String} nodeName + */ +function defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; + + if ( !display ) { + display = actualDisplay( nodeName, doc ); + + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { + + // Use the already-created iframe if possible + iframe = (iframe || jQuery( " +
ArabicHebrewPolish
BulgarianHindiPortuguese
CatalanHmong DawRomanian
Chinese SimplifiedHungarianRussian
Chinese TraditionalIndonesianSlovak
CzechItalianSlovenian
DanishJapaneseSpanish
DutchKlingonSwedish
EnglishKoreanThai
EstonianLatvianTurkish
FinnishLithuanianUkrainian
FrenchMalayUrdu
GermanMalteseVietnamese
GreekNorwegianWelsh
Haitian CreolePersian
\ No newline at end of file diff --git a/html_parsing/foxtrot_com_ua__ru__search/main.py b/html_parsing/foxtrot_com_ua__ru__search/main.py index 6f681f3b3..8368e2bf6 100644 --- a/html_parsing/foxtrot_com_ua__ru__search/main.py +++ b/html_parsing/foxtrot_com_ua__ru__search/main.py @@ -1,13 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT -import time +import datetime as dt -from typing import List, Tuple, Union from pathlib import Path # pip install pandas @@ -17,6 +15,7 @@ from selenium import webdriver from selenium.webdriver.firefox.options import Options from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver.common.by import By # TODO: Using logging instead print @@ -25,14 +24,14 @@ def get_text_by_css(parent, css_selector: str, default: str) -> str: try: - return parent.find_element_by_css_selector(css_selector).text + return parent.find_element(By.CSS_SELECTOR, css_selector).text except: return default -def parse(url_search: str) -> List[Tuple[str, str, str]]: +def parse(url_search: str) -> list[tuple[str, str, str]]: options = Options() - options.add_argument('--headless') + options.add_argument("--headless") items = [] @@ -44,15 +43,15 @@ def parse(url_search: str) -> List[Tuple[str, str, str]]: while page <= last_page: url = url_search if page > 1: - url = f'{url_search}&page={page}' + url = f"{url_search}&page={page}" - print(f'Load: {url}') + print(f"Load: {url}") driver.get(url) - for item_el in driver.find_elements_by_css_selector(".card[data-url]"): - name = get_text_by_css(item_el, '.card__title', 'Null') - price = get_text_by_css(item_el, '.card-price', '-') - nal = get_text_by_css(item_el, '.card__buttons', '-') + for item_el in driver.find_elements(By.CSS_SELECTOR, ".card[data-url]"): + name = get_text_by_css(item_el, ".card__title", "Null") + price = get_text_by_css(item_el, ".card-price", "-") + nal = get_text_by_css(item_el, ".card__buttons", "-") row = name, price, nal print(row) @@ -61,8 +60,8 @@ def parse(url_search: str) -> List[Tuple[str, str, str]]: # Обновление номера последней страницы try: - pages_count_el = driver.find_element_by_css_selector('*[data-pages-count]') - last_page = int(pages_count_el.get_attribute('data-pages-count')) + pages_count_el = driver.find_element(By.CSS_SELECTOR, "*[data-pages-count]") + last_page = int(pages_count_el.get_attribute("data-pages-count")) except NoSuchElementException: break @@ -75,21 +74,20 @@ def parse(url_search: str) -> List[Tuple[str, str, str]]: return items - def save_goods( - file_name: Union[str, Path], - items: List[Tuple[str, str, str]], - encoding='utf-8' -): - df = pd.DataFrame(items, columns=['Name', 'Price', 'Nal']) + file_name: str | Path, + items: list[tuple[str, str, str]], + encoding="utf-8", +) -> None: + df = pd.DataFrame(items, columns=["Name", "Price", "Nal"]) df.to_csv(file_name, encoding=encoding) -if __name__ == '__main__': +if __name__ == "__main__": url = "https://www.foxtrot.com.ua/ru/search?query=gazer&filter=_195_588" items = parse(url) - print(f'Total goods: {len(items)}') + print(f"Total goods: {len(items)}") - file_name = f'foxtrot_parser_{DT.datetime.now():%Y-%m-%d}.csv' - print(f'Saved to {file_name}') + file_name = f"foxtrot_parser_{dt.datetime.now():%Y-%m-%d}.csv" + print(f"Saved to {file_name}") save_goods(file_name, items) diff --git a/html_parsing/freelance_habr_com__tasks.py b/html_parsing/freelance_habr_com__tasks.py index 1d236ac7f..40d04b1fa 100644 --- a/html_parsing/freelance_habr_com__tasks.py +++ b/html_parsing/freelance_habr_com__tasks.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from urllib.parse import urljoin @@ -10,11 +10,11 @@ from bs4 import BeautifulSoup -rs = requests.get('https://freelance.habr.com/tasks?q=python') -root = BeautifulSoup(rs.content, 'html.parser') +rs = requests.get("https://freelance.habr.com/tasks?q=python") +root = BeautifulSoup(rs.content, "html.parser") urls = [ - (urljoin(rs.url, a['href']), a.get_text(strip=True)) - for a in root.select('.task__title > a[href]') + (urljoin(rs.url, a["href"]), a.get_text(strip=True)) + for a in root.select(".task__title > a[href]") ] print(len(urls), urls) # 25 [('https://freelance.habr.com/tasks/349695', 'Парсер '), ..., 'Доработка бота Telegram на Python')] diff --git a/html_parsing/gametime__use_cubiq_ru/extract_all_games.py b/html_parsing/gametime__use_cubiq_ru/extract_all_games.py deleted file mode 100644 index 3d5bd457b..000000000 --- a/html_parsing/gametime__use_cubiq_ru/extract_all_games.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import json -import time -from pathlib import Path - -from bs4 import BeautifulSoup - -from main import get, session - - -FILE = Path(__file__).resolve() -DIR = FILE.parent -FILE_CACHE = DIR / f'{FILE.stem}.json' - -try: - cache = json.load(open(FILE_CACHE, encoding='utf-8')) -except: - cache = dict() - - -URL_PAGE = 'https://cubiq.ru/gametime/page/{page}/' - -if __name__ == '__main__': - page = 1 - while True: - url = URL_PAGE.format(page=page) - print(url) - - try: - rs = session.get(url) - root = BeautifulSoup(rs.content, 'html.parser') - for a in root.select('.gridlove-post > .entry-image > a'): - game = a['title'] - if game in cache: - continue - - url = a['href'] - if data := get(url): - time_obj = data['Основной сюжет'] - cache[game] = { - 'text': time_obj.text, - 'seconds': time_obj.seconds, - } - print(f'Saved {game!r}: {time_obj.text}') - - json.dump(cache, open(FILE_CACHE, 'w', encoding='utf-8'), indent=4, ensure_ascii=False) - time.sleep(1) - - next_page = root.select_one('.gridlove-pagination > .next.page-numbers') - if not next_page: - break - - page += 1 - - except Exception: - time.sleep(1) diff --git a/html_parsing/gametime__use_cubiq_ru/extract_all_games__show_the_shortest_time.py b/html_parsing/gametime__use_cubiq_ru/extract_all_games__show_the_shortest_time.py deleted file mode 100644 index 44dab2117..000000000 --- a/html_parsing/gametime__use_cubiq_ru/extract_all_games__show_the_shortest_time.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from extract_all_games import cache - - -print(f'Всего игр: {len(cache)}') -print() -# Всего игр: 11301 - -games = [] -for game, time_obj in cache.items(): - seconds = time_obj['seconds'] - if not seconds or game.upper().endswith('DLC'): - continue - - games.append((game, time_obj['text'], seconds)) - -# Сортировка по возрастанию времени прохождения -games.sort(key=lambda x: x[2]) - -n = 100 -print(f'Первые {n} игр с минимум времени прохождения:') -for i, (game, time_text, _) in enumerate(games[:n], 1): - print(f'{i:3}. {game!r}: {time_text}') -""" -Первые 100 игр с минимум времени прохождения: - 1. 'Mitoza': 1 мин. - 2. 'Lost in Space (2018)': 1 мин. - 3. 'Run Jesus Run a.k.a. The 10 Second Gospel': 1 мин. - 4. 'First Job': 2 мин. - 5. 'Sakuya Izayoi Gives You Advice And Dabs': 2 мин. -... - 96. 'Star Gladiator': 11 мин. - 97. 'Flightless (2017)': 11 мин. - 98. 'Kimulator: Fight for your destiny': 11 мин. - 99. 'UBERMOSH Vol.3': 11 мин. -100. 'A Game About': 12 мин. -""" diff --git a/html_parsing/gametime__use_cubiq_ru/found_from_local_files.py b/html_parsing/gametime__use_cubiq_ru/found_from_local_files.py deleted file mode 100644 index 7177f72fa..000000000 --- a/html_parsing/gametime__use_cubiq_ru/found_from_local_files.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import json -import os -import time -from pathlib import Path - -from main import find - - -DIR = Path(__file__).resolve().parent -DIR_CACHE = DIR / 'cache.json' - -try: - cache = json.load(open(DIR_CACHE, encoding='utf-8')) -except: - cache = dict() - -DIR_GAMES = Path(os.path.expanduser(r'~\Desktop\Пройти')) -game_names = [p.stem for p in DIR_GAMES.glob('*.lnk')] - -games = [] -changed = False -for game in game_names: - if game not in cache: - data = find(game) - if data: - time_obj = data['Основной сюжет'] - cache[game] = { - 'text': time_obj.text, - 'seconds': time_obj.seconds, - } - print(f'{game}: {time_obj.text}') - - else: - cache[game] = None - print(f'{game}: ') - - changed = True - time.sleep(1) - - time_obj = cache[game] - if time_obj: - games.append((game, time_obj['text'], time_obj['seconds'])) - -if changed: - json.dump(cache, open(DIR_CACHE, 'w', encoding='utf-8'), indent=4, ensure_ascii=False) - print() - -# Сортировка по возрастанию времени прохождения -games.sort(key=lambda x: x[2]) - -print('Первые 10 игр с минимум времени прохождения:') -for i, (game, time_text, _) in enumerate(games[:10], 1): - print(f'{i:2}. {game!r}: {time_text}') -""" -Первые 10 игр с минимум времени прохождения: - 1. 'FOTONICA': 2 ч. 11 мин. - 2. 'The Wild Eight': 4 ч. - 3. 'Sonic Forces': 4 ч. 12 мин. - 4. 'Halo - Spartan Assault': 4 ч. 17 мин. - 5. 'NecroVision Lost Company': 4 ч. 52 мин. - 6. 'Niko - Through the Dream': 5 ч. 21 мин. - 7. 'FEZ': 5 ч. 52 мин. - 8. 'Lucius': 6 ч. 13 мин. - 9. 'Flashback': 6 ч. 14 мин. -10. 'True Crime - New York City': 7 ч. -""" diff --git a/html_parsing/gametime__use_cubiq_ru/main.py b/html_parsing/gametime__use_cubiq_ru/main.py deleted file mode 100644 index caf5202dc..000000000 --- a/html_parsing/gametime__use_cubiq_ru/main.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import re -from dataclasses import dataclass -from typing import Dict, Optional -from urllib.parse import urljoin - -import requests -from bs4 import BeautifulSoup - - -@dataclass -class Time: - text: str - seconds: int - - @classmethod - def from_text(cls, value: str) -> 'Time': - seconds = to_seconds(value) - return cls(value, seconds) - - -USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0' - -session = requests.Session() -session.headers['User-Agent'] = USER_AGENT - - -def to_seconds(time_str: str) -> int: - kind_to_seconds = { - 'ч': 60 * 60, - 'мин': 60, - } - - seconds = 0 - for value, kind in re.findall(r'(\d+) (ч|мин)', time_str): - if kind not in kind_to_seconds: - raise Exception(f'Неизвестный kind={kind}!') - - seconds += int(value) * kind_to_seconds[kind] - - return seconds - - -# SOURCE: https://github.com/gil9red/price_of_games/blob/9311f9cbc6b9e57d0308436e3dbf3e524f23ef74/app_parser/utils.py -def smart_comparing_names(name_1: str, name_2: str) -> bool: - """ - Функция для сравнивания двух названий игр. - Возвращает True, если совпадают, иначе -- False. - - """ - - # Приведение строк к одному регистру - name_1 = name_1.lower() - name_2 = name_2.lower() - - def remove_postfix(text: str) -> str: - for postfix in ('dlc', 'expansion'): - if text.endswith(postfix): - return text[:-len(postfix)] - return text - - # Удаление символов кроме буквенных, цифр и _: "the witcher®3:___ вася! wild hunt" -> "thewitcher3___васяwildhunt" - def clear_name(name: str) -> str: - return re.sub(r'\W', '', name) - - name_1 = clear_name(name_1) - name_1 = remove_postfix(name_1) - - name_2 = clear_name(name_2) - name_2 = remove_postfix(name_2) - - return name_1 == name_2 - - -def get(url: str) -> Dict[str, Dict[str, Time]]: - rs = session.get(url) - root = BeautifulSoup(rs.content, 'html.parser') - - data = { - 'title': root.select_one('.entry-header').get_text(strip=True), - } - - for li in root.select('ul.game_times > li'): - name = li.h5.get_text(strip=True) - value = li.div.get_text(strip=True) - - data[name] = Time.from_text(value) - - return data - - -def find(game: str) -> Optional[Dict[str, Dict[str, Time]]]: - url_search = 'https://cubiq.ru/gametime/?s=' + game - - rs = session.get(url_search) - root = BeautifulSoup(rs.content, 'html.parser') - - for a in root.select('.entry-title > a[href]'): - name = a.get_text(strip=True) - if smart_comparing_names(name, game): - url = urljoin(rs.url, a['href']) - return get(url) - - -if __name__ == '__main__': - assert to_seconds('25 ч. 18 мин.') == 91080 - assert to_seconds('71 ч. 50 мин.') == 258600 - assert to_seconds('113 ч.') == 406800 - - url = 'https://cubiq.ru/gametime/age-of-wonders-iii/' - rs = get(url) - print(rs) - # { - # 'title': 'Время прохождения Age of Wonders III', - # 'Основной сюжет': Time(text='25 ч. 18 мин.', seconds=91080), - # 'Cюжет и доп. задания': Time(text='71 ч. 50 мин.', seconds=258600), - # 'Перфекционист': Time(text='113 ч.', seconds=406800) - # } - - print() - print(find('dead space')) - # { - # 'title': 'Время прохождения Dead Space', - # 'Основной сюжет': Time(text='11 ч. 10 мин.', seconds=40200), - # 'Cюжет и доп. задания': Time(text='13 ч. 10 мин.', seconds=47400), - # 'Перфекционист': Time(text='20 ч. 41 мин.', seconds=74460) - # } - - print() - print(find('dead space 2')) - # { - # 'title': 'Время прохождения Dead Space 2', - # 'Основной сюжет': Time(text='9 ч. 18 мин.', seconds=33480), - # 'Cюжет и доп. задания': Time(text='11 ч. 49 мин.', seconds=42540), - # 'Перфекционист': Time(text='17 ч. 23 мин.', seconds=62580) - # } diff --git a/html_parsing/get_comments__russianfood_com.py b/html_parsing/get_comments__russianfood_com.py index 1c15d2151..31de4cd40 100644 --- a/html_parsing/get_comments__russianfood_com.py +++ b/html_parsing/get_comments__russianfood_com.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from urllib.parse import urljoin @@ -11,7 +11,7 @@ import requests -PATTERN_NEXT = re.compile('Следующая →') +PATTERN_NEXT = re.compile("Следующая →") def get_comments(url: str, no_quote=False) -> list: @@ -19,43 +19,43 @@ def get_comments(url: str, no_quote=False) -> list: while True: rs = requests.get(url) - root = BeautifulSoup(rs.content, 'html.parser') + root = BeautifulSoup(rs.content, "html.parser") # Перебор комментариев - for comment_text_el in root.select('.comment > div'): + for comment_text_el in root.select(".comment > div"): # Перебор цитат - for blockquote_el in comment_text_el.select('blockquote'): + for blockquote_el in comment_text_el.select("blockquote"): if no_quote: blockquote_el.decompose() continue blockquote_el.replace_with( - "«" + blockquote_el.get_text(separator='\n', strip=True) + "»" + "«" + blockquote_el.get_text(separator="\n", strip=True) + "»" ) - for br in comment_text_el.select('br'): - br.replace_with('\n') + for br in comment_text_el.select("br"): + br.replace_with("\n") - comment_text = comment_text_el.get_text(separator='\n', strip=True) + comment_text = comment_text_el.get_text(separator="\n", strip=True) items.append(comment_text) - a_next = root.find(name='a', text=PATTERN_NEXT) + a_next = root.find(name="a", text=PATTERN_NEXT) if not a_next: break - url = urljoin(rs.url, a_next.get('href')) + url = urljoin(rs.url, a_next.get("href")) return items -if __name__ == '__main__': +if __name__ == "__main__": for url in [ - 'https://www.russianfood.com/recipes/recipe.php?rid=107392', - 'https://www.russianfood.com/recipes/recipe.php?rid=121323', + "https://www.russianfood.com/recipes/recipe.php?rid=107392", + "https://www.russianfood.com/recipes/recipe.php?rid=121323", ]: print(url) for i, comment_text in enumerate(get_comments(url), 1): print(i, repr(comment_text)) - print('\n' + '-' * 100 + '\n') \ No newline at end of file + print("\n" + "-" * 100 + "\n") diff --git a/html_parsing/get_comments__stopgame_ru.py b/html_parsing/get_comments__stopgame_ru.py index 98d34de69..c3d11d58f 100644 --- a/html_parsing/get_comments__stopgame_ru.py +++ b/html_parsing/get_comments__stopgame_ru.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from bs4 import BeautifulSoup @@ -10,40 +10,40 @@ def get_comments(url: str, no_quote=False) -> list: rs = requests.get(url) - root = BeautifulSoup(rs.content, 'html.parser') + root = BeautifulSoup(rs.content, "html.parser") items = [] # Перебор комментариев - for comment_text_el in root.select('.comment-text'): + for comment_text_el in root.select(".comment-text"): # Перебор цитат - for blockquote_el in comment_text_el.select('blockquote'): + for blockquote_el in comment_text_el.select("blockquote"): if no_quote: blockquote_el.decompose() continue blockquote_el.replace_with( - "«" + blockquote_el.get_text(separator='\n', strip=True) + "»" + "«" + blockquote_el.get_text(separator="\n", strip=True) + "»" ) - for br in comment_text_el.select('br'): - br.replace_with('\n') + for br in comment_text_el.select("br"): + br.replace_with("\n") - comment_text = comment_text_el.get_text(separator='\n', strip=True) + comment_text = comment_text_el.get_text(separator="\n", strip=True) items.append(comment_text) return items -if __name__ == '__main__': +if __name__ == "__main__": for url in [ - 'https://stopgame.ru/show/102770/atom_rpg_review', - 'https://stopgame.ru/show/43787/project_dark_review', - 'https://stopgame.ru/show/82379/dark_souls_iii_videoreview', + "https://stopgame.ru/show/102770/atom_rpg_review", + "https://stopgame.ru/show/43787/project_dark_review", + "https://stopgame.ru/show/82379/dark_souls_iii_videoreview", ]: print(url) for i, comment_text in enumerate(get_comments(url), 1): print(i, repr(comment_text)) - print('\n' + '-' * 100 + '\n') + print("\n" + "-" * 100 + "\n") diff --git a/html_parsing/get_customers_of_bink.py b/html_parsing/get_customers_of_bink.py index 7fbb5321f..9d7306231 100644 --- a/html_parsing/get_customers_of_bink.py +++ b/html_parsing/get_customers_of_bink.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import requests from bs4 import BeautifulSoup -url = 'http://www.radgametools.com/binkgames.htm' +url = "http://www.radgametools.com/binkgames.htm" rs = requests.get(url) -root = BeautifulSoup(rs.content, 'html.parser') +root = BeautifulSoup(rs.content, "html.parser") -items = [x.get_text(strip=True) for x in root.select('.gameslist > dl > dt')] -print(f'Items ({len(items)}): {items}') +items = [x.get_text(strip=True) for x in root.select(".gameslist > dl > dt")] +print(f"Items ({len(items)}): {items}") # Items (403): ['1C', '2015 Inc', '2XL Games', ..., 'Zombie', 'Zoë Mode', 'Zoo Digital Publishing'] diff --git a/html_parsing/get_game_genres/_view_db_Dump.py b/html_parsing/get_game_genres/_view_db_Dump.py deleted file mode 100644 index 4a7783276..000000000 --- a/html_parsing/get_game_genres/_view_db_Dump.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from collections import defaultdict -from db import Dump - - -print('Total:', Dump.select().count()) - -genres = Dump.get_all_genres() -print(f'Genres ({len(genres)}): {genres}') - -games = Dump.get_all_games() -print(f'Games ({len(games)}): {games}') - -sites = Dump.get_all_sites() -print(f'Sites ({len(sites)}): {sites}') - -print() - -max_width = max(len(x.site) for x in Dump.select(Dump.site).distinct()) -fmt_str = ' {:<%d} : {}' % max_width - -game_by_dump = defaultdict(list) -for x in Dump.get(): - game_by_dump[x.name].append(x) - -for game, dumps in game_by_dump.items(): - print(game) - - for dump in dumps: - print( - fmt_str.format(dump.site, dump.genres) - ) - - print() diff --git a/html_parsing/get_game_genres/_view_db_site_by_genres.py b/html_parsing/get_game_genres/_view_db_site_by_genres.py deleted file mode 100644 index 72f4621d2..000000000 --- a/html_parsing/get_game_genres/_view_db_site_by_genres.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from collections import defaultdict -from db import Dump - - -genres = Dump.get_all_genres() -print(f'Genres ({len(genres)}): {genres}') - -sites = Dump.get_all_sites() -print(f'Sites ({len(sites)}): {sites}') - -print() - -max_width = max(len(x.site) for x in Dump.select(Dump.site).distinct()) -fmt_str = '{:<%d} : ({}) {}' % max_width - -site_by_genres = defaultdict(list) -for x in Dump.get(): - site_by_genres[x.site] += x.genres - -for k, v in site_by_genres.items(): - site_by_genres[k] = sorted(set(v)) - -print(f'Total {len(site_by_genres)}:') - -for site, genres in site_by_genres.items(): - print(fmt_str.format(site, len(genres), genres)) diff --git a/html_parsing/get_game_genres/common.py b/html_parsing/get_game_genres/common.py deleted file mode 100644 index 8bfcc13e3..000000000 --- a/html_parsing/get_game_genres/common.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import datetime as DT -import re -from pathlib import Path - - -# SOURCE: https://github.com/gil9red/price_of_games/blob/9311f9cbc6b9e57d0308436e3dbf3e524f23ef74/app_parser/utils.py -def smart_comparing_names(name_1: str, name_2: str) -> bool: - """ - Функция для сравнивания двух названий игр. - Возвращает True, если совпадают, иначе -- False. - - """ - - # Приведение строк к одному регистру - name_1 = name_1.lower() - name_2 = name_2.lower() - - def remove_postfix(text: str) -> str: - for postfix in ('dlc', 'expansion'): - if text.endswith(postfix): - return text[:-len(postfix)] - return text - - # Удаление символов кроме буквенных, цифр и _: "the witcher®3:___ вася! wild hunt" -> "thewitcher3___васяwildhunt" - def clear_name(name: str) -> str: - return re.sub(r'\W', '', name) - - name_1 = clear_name(name_1) - name_1 = remove_postfix(name_1) - - name_2 = clear_name(name_2) - name_2 = remove_postfix(name_2) - - return name_1 == name_2 - - -def get_uniques(items: list) -> list: - return list(set(items)) - - -def pretty_path(path: str) -> str: - return str(Path(path).resolve()) - - -def get_current_datetime_str(fmt='%Y-%m-%d_%H%M%S') -> str: - return DT.datetime.now().strftime(fmt) - - -# SOURCE: https://github.com/django/django/blob/03dbdfd9bbbbd0b0172aad648c6bbe3f39541137/django/utils/text.py#L221 -def get_valid_filename(s): - """ - Return the given string converted to a string that can be used for a clean - filename. Remove leading and trailing spaces; convert other spaces to - underscores; and remove anything that is not an alphanumeric, dash, - underscore, or dot. - >>> get_valid_filename("john's portrait in 2004.jpg") - 'johns_portrait_in_2004.jpg' - """ - s = str(s).strip().replace(' ', '_') - return re.sub(r'(?u)[^-\w.]', '', s) - - -USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0' - -DIR = Path(__file__).resolve().parent -DIR_ERRORS = str(DIR / 'errors') -DIR_LOGS = str(DIR / 'logs') - -NEED_LOGS = True -LOG_FORMAT = '[%(asctime)s] %(levelname)-8s %(message)s' - - -TEST_GAMES = [ - 'Hellgate: London', - 'The Incredible Adventures of Van Helsing', - 'Dark Souls: Prepare to Die Edition', - 'Twin Sector', - 'Call of Cthulhu: Dark Corners of the Earth', -] - - -def _common_test(get_game_genres, sleep=1, max_number=None): - if max_number is None: - max_number = len(TEST_GAMES) - - import time - - for name in TEST_GAMES[:max_number]: - print(f'Search {name!r}...') - print(f' Genres: {get_game_genres(name)}\n') - - time.sleep(sleep) diff --git a/html_parsing/get_game_genres/common_utils.py b/html_parsing/get_game_genres/common_utils.py deleted file mode 100644 index efe7c30a0..000000000 --- a/html_parsing/get_game_genres/common_utils.py +++ /dev/null @@ -1,376 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from glob import glob -import importlib.util -from pathlib import Path -import sys -import threading -from typing import Dict, Callable - -# For import parsers/* -sys.path.append('parsers') - - -from common import DIR_LOGS - - -def module_from_file(file_path: str): - module_name = Path(file_path).stem - spec = importlib.util.spec_from_file_location(module_name, file_path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - - if module_name not in sys.modules: - sys.modules[module_name] = module - - return module - - -def get_funcs_parsers() -> Dict[str, Callable]: - data = dict() - - for file_name in glob('parsers/*.py'): - module = module_from_file(file_name) - if 'get_game_genres' not in dir(module): - continue - - data[module.__name__] = module.get_game_genres - - return data - - -def get_parsers() -> list: - items = [] - - for file_name in glob('parsers/*.py'): - module = module_from_file(file_name) - for attr in dir(module): - if not attr.endswith('_Parser'): - continue - - cls = getattr(module, attr) - items.append( - cls.instance() - ) - - return items - - -# Parser from https://github.com/gil9red/played_games/blob/f23777a1368f9124450bedac036791068d8ca099/mini_played_games_parser.py#L7 -def parse_played_games(text: str, silence: bool=False) -> dict: - """ - Функция для парсинга списка игр. - """ - - FINISHED_GAME = 'FINISHED_GAME' - NOT_FINISHED_GAME = 'NOT_FINISHED_GAME' - FINISHED_WATCHED = 'FINISHED_WATCHED' - NOT_FINISHED_WATCHED = 'NOT_FINISHED_WATCHED' - - FLAG_BY_CATEGORY = { - ' ': FINISHED_GAME, - '- ': NOT_FINISHED_GAME, - ' -': NOT_FINISHED_GAME, - ' @': FINISHED_WATCHED, - '@ ': FINISHED_WATCHED, - '-@': NOT_FINISHED_WATCHED, - '@-': NOT_FINISHED_WATCHED, - } - - # Регулярка вытаскивает выражения вида: 1, 2, 3 или 1-3, или римские цифры: III, IV - import re - PARSE_GAME_NAME_PATTERN = re.compile( - r'(\d+(, *?\d+)+)|(\d+ *?- *?\d+)|([MDCLXVI]+(, ?[MDCLXVI]+)+)', - flags=re.IGNORECASE - ) - - def parse_game_name(game_name: str) -> list: - """ - Функция принимает название игры и пытается разобрать его, после возвращает список названий. - У некоторых игр в названии может указываться ее части или диапазон частей, поэтому для правильного - составления списка игр такие случаи нужно обрабатывать. - Пример: - "Resident Evil 4, 5, 6" -> ["Resident Evil 4", "Resident Evil 5", "Resident Evil 6"] - "Resident Evil 1-3" -> ["Resident Evil", "Resident Evil 2", "Resident Evil 3"] - "Resident Evil 4" -> ["Resident Evil 4"] - """ - - match = PARSE_GAME_NAME_PATTERN.search(game_name) - if match is None: - return [game_name] - - seq_str = match.group(0) - - # "Resident Evil 4, 5, 6" -> "Resident Evil" - # For not valid "Trollface Quest 1-7-8" -> "Trollface Quest" - index = game_name.index(seq_str) - base_name = game_name[:index].strip() - - seq_str = seq_str.replace(' ', '') - - if ',' in seq_str: - # '1,2,3' -> ['1', '2', '3'] - seq = seq_str.split(',') - - elif '-' in seq_str: - seq = seq_str.split('-') - - # ['1', '7'] -> [1, 7] - seq = list(map(int, seq)) - - # [1, 7] -> ['1', '2', '3', '4', '5', '6', '7'] - seq = list(map(str, range(seq[0], seq[1] + 1))) - - else: - return [game_name] - - # Сразу проверяем номер игры в серии и если она первая, то не добавляем в названии ее номер - return [base_name if num == '1' else base_name + " " + num for num in seq] - - platforms = dict() - platform = None - - for line in text.splitlines(): - line = line.rstrip() - if not line: - continue - - flag = line[:2] - if flag not in FLAG_BY_CATEGORY and line.endswith(':'): - platform_name = line[:-1] - - platform = { - FINISHED_GAME: [], - NOT_FINISHED_GAME: [], - FINISHED_WATCHED: [], - NOT_FINISHED_WATCHED: [], - } - platforms[platform_name] = platform - - continue - - if not platform: - continue - - category_name = FLAG_BY_CATEGORY.get(flag) - if not category_name: - if not silence: - print('Странный формат строки: "{}"'.format(line)) - continue - - category = platform[category_name] - - game_name = line[2:] - for game in parse_game_name(game_name): - if game in category: - if not silence: - print('Предотвращено добавление дубликата игры "{}"'.format(game)) - continue - - category.append(game) - - return platforms - - -# SOURCE: https://github.com/gil9red/price_of_games/blob/5432514a3322d359ca8ef509f5ef173ce6969203/common.py#L482 -def get_games_list() -> list: - import requests - rs = requests.get('https://gist.github.com/gil9red/2f80a34fb601cd685353') - - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'html.parser') - href = root.select_one('.file-actions > a')['href'] - - from urllib.parse import urljoin - raw_url = urljoin(rs.url, href) - - rs = requests.get(raw_url) - content_gist = rs.text - - platforms = parse_played_games(content_gist) - - # Пройденные игры - finished_game_list = platforms['PC']['FINISHED_GAME'] - - # Просмотренные игры - finished_watched_game_list = platforms['PC']['FINISHED_WATCHED'] - - return sorted(set(finished_game_list + finished_watched_game_list)) - - -# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/8fa9b9c23d10b5ee7ff0161da997b463f7a861bf/wait/wait.py#L7 -def wait(days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0): - from datetime import timedelta, datetime - from itertools import cycle - import sys - import time - - try: - progress_bar = cycle('|/-\\|/-\\') - - today = datetime.today() - timeout_date = today + timedelta( - days=days, seconds=seconds, microseconds=microseconds, - milliseconds=milliseconds, minutes=minutes, hours=hours, weeks=weeks - ) - - def str_timedelta(td: timedelta) -> str: - td = str(td) - - # Remove ms - # 0:01:40.123000 -> 0:01:40 - if '.' in td: - td = td[:td.rindex('.')] - - # 0:01:40 -> 00:01:40 - if td.startswith('0:'): - td = '00:' + td[2:] - - return td - - while today <= timeout_date: - left = timeout_date - today - left = str_timedelta(left) - - print('\r' + ' ' * 100 + '\r', end='') - print('[{}] Time left to wait: {}'.format(next(progress_bar), left), end='') - sys.stdout.flush() - - # Delay 1 seconds - time.sleep(1) - - today = datetime.today() - - print('\r' + ' ' * 100 + '\r', end='') - - except KeyboardInterrupt: - print() - print('Waiting canceled') - - -# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/163c91d6882b548c904ad40703dac00c0a64e5a2/logger_example.py#L7 -def get_logger(name='dump.txt', encoding='utf-8'): - Path(DIR_LOGS).mkdir(parents=True, exist_ok=True) - - file = DIR_LOGS + '/' + name - - import logging - log = logging.getLogger(name) - log.setLevel(logging.DEBUG) - - formatter = logging.Formatter('[%(asctime)s] %(levelname)-8s %(message)s') - - from logging.handlers import RotatingFileHandler - fh = RotatingFileHandler(file, maxBytes=10_000_000, backupCount=5, encoding=encoding) - fh.setFormatter(formatter) - log.addHandler(fh) - - import sys - sh = logging.StreamHandler(stream=sys.stdout) - sh.setFormatter(formatter) - log.addHandler(sh) - - return log - - -# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/c06ff5fb6a0abfb5a41652f71d7adf7ee414e4c8/multithreading__threading__examples/atomic_counter.py#L14 -class AtomicCounter: - def __init__(self, initial=0): - self.value = initial - self._lock = threading.Lock() - - def inc(self, num=1): - with self._lock: - self.value += num - return self.value - - -# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/f0403620f7948306ad9e34a373f2aabc0237fb2a/seconds_to_str.py -def seconds_to_str(seconds: int) -> str: - hh, mm = divmod(seconds, 3600) - mm, ss = divmod(mm, 60) - return "%02d:%02d:%02d" % (hh, mm, ss) - - -def print_parsers(parsers: list, log=print): - max_width = len(max([x.get_site_name() for x in parsers], key=len)) - fmt_str = ' {:<%d} : {}' % max_width - items = [ - fmt_str.format(parser.get_site_name(), parser.__class__) - for parser in parsers - ] - - log(f'Parsers ({len(parsers)}):\n' + "\n".join(items)) - - -if __name__ == "__main__": - items = get_games_list() - print(f'Games ({len(items)}): {", ".join(items[:5])}...') - # Games (743): 35MM, 60 Seconds!, A Bird Story, A Plague Tale: Innocence, A Story About My Uncle... - - print() - - parsers = get_parsers() - print_parsers(parsers) - # Parsers (14): - # ag_ru : - # gamebomb_ru : - # gamefaqs_gamespot_com : - # gameguru_ru : - # gamer_info_com : - # gamespot_com : - # igromania_ru : - # iwantgames_ru : - # metacritic_com : - # mobygames_com : - # playground_ru : - # spong_com : - # stopgame_ru : - # store_steampowered_com : - - print() - - game = 'Dead Space' - print(f'Search genres for {game!r}:') - - for parser in get_parsers(): - parser._need_logs = False - print(f" {parser.get_site_name():<25}: {parser.get_game_genres(game)}") - - # ag_ru : ['Экшены', 'Шутеры'] - # gamebomb_ru : ['Боевик-приключения', 'Шутер'] - # gamefaqs_gamespot_com : ['Action', 'Arcade', 'Shooter', 'Third-Person'] - # gameguru_ru : ['Экшен', 'Шутер'] - # gamer_info_com : ['ужасы', 'action'] - # gamespot_com : ['Action', '3D', 'Shooter', 'Third-Person'] - # igromania_ru : ['Боевик от третьего лица', 'Боевик', 'Ужасы'] - # iwantgames_ru : [] - # metacritic_com : ['Action', 'Shooter', 'Sci-Fi', 'Arcade', 'Third-Person'] - # mobygames_com : ['Action'] - # playground_ru : ['От третьего лица', 'Экшен', 'Космос', 'Ужасы'] - # spong_com : ['Adventure: Survival Horror'] - # stopgame_ru : [] - # store_steampowered_com : ['Action'] - - # print() - # - # for site, get_game_genres in get_funcs_parsers().items(): - # print(f"{site:<25}: {get_game_genres('Dead Space')}") - # # ag_ru : ['Шутеры', 'Экшены'] - # # gamebomb_ru : ['Боевик-приключения', 'Шутер'] - # # gamefaqs_gamespot_com : ['Arcade', 'Shooter', 'Action', 'Third-Person'] - # # gameguru_ru : ['Экшен', 'Шутер'] - # # gamer_info_com : ['ужасы', 'action'] - # # gamespot_com : ['3D', 'Shooter', 'Action', 'Third-Person'] - # # igromania_ru : ['Боевик', 'Ужасы', 'Боевик от третьего лица'] - # # iwantgames_ru : [] - # # metacritic_com : ['Arcade', 'Third-Person', 'Sci-Fi', 'Action', 'Shooter'] - # # mobygames_com : ['Action'] - # # playground_ru : ['Ужасы', 'От третьего лица', 'Космос', 'Экшен'] - # # spong_com : ['Adventure: Survival Horror'] - # # stopgame_ru : [] - # # store_steampowered_com : ['Action'] diff --git a/html_parsing/get_game_genres/db.py b/html_parsing/get_game_genres/db.py deleted file mode 100644 index 885706deb..000000000 --- a/html_parsing/get_game_genres/db.py +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import datetime as DT -import json -import shutil - -from collections import defaultdict -from typing import Dict, List, Iterable, Optional -from pathlib import Path - -# pip install peewee -from peewee import * -from playhouse.sqliteq import SqliteQueueDatabase - - -DB_DIR_NAME = str(Path(__file__).resolve().parent / 'database') -DB_FILE_NAME = str(Path(DB_DIR_NAME) / 'games.sqlite') - - -Path(DB_DIR_NAME).mkdir(parents=True, exist_ok=True) - - -def db_create_backup(backup_dir='backup', date_fmt='%Y-%m-%d'): - backup_path = Path(backup_dir) - backup_path.mkdir(parents=True, exist_ok=True) - - zip_name = DT.datetime.today().strftime(date_fmt) - zip_name = backup_path / zip_name - - shutil.make_archive( - zip_name, - 'zip', - DB_DIR_NAME - ) - - -class ListField(Field): - def python_value(self, value: str) -> List: - return json.loads(value, encoding='utf-8') - - def db_value(self, value: Optional[Iterable]) -> str: - if value is not None: - if isinstance(value, str): - return value - - if not isinstance(value, list): - raise Exception('Type must be a list') - - return json.dumps(value, ensure_ascii=False) - - -# Simple Sqlite -# db = SqliteDatabase( -# DB_FILE_NAME, -# pragmas={ -# 'foreign_keys': 1, # Ensure foreign-key constraints are enforced. -# 'journal_mode': 'wal', # WAL-mode -# 'cache_size': -1024 * 64 # 64MB page-cache -# } -# ) -# -# This working with multithreading -# SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqliteq -db = SqliteQueueDatabase( - DB_FILE_NAME, - pragmas={ - 'foreign_keys': 1, - 'journal_mode': 'wal', # WAL-mode - 'cache_size': -1024 * 64 # 64MB page-cache - }, - use_gevent=False, # Use the standard library "threading" module. - autostart=True, - queue_max_size=64, # Max. # of pending writes that can accumulate. - results_timeout=5.0 # Max. time to wait for query to be executed. -) - - -class BaseModel(Model): - class Meta: - database = db - - -class Dump(BaseModel): - name = CharField() - site = CharField() - genres = ListField() - - @classmethod - def exists(cls, site: str, name: str) -> bool: - return cls.select().where( - cls.site == site, cls.name == name - ).exists() - - @classmethod - def add(cls, site: str, name: str, genres: list): - if not cls.exists(site, name): - cls.create(site=site, name=name, genres=genres) - - @classmethod - def get(cls) -> List['Dump']: - return cls.select().where(cls.genres != '[]').order_by(cls.name) - - @classmethod - def get_games_by_site(cls, site: str) -> List['Dump']: - return list(cls.select().where(cls.site == site).order_by(cls.name)) - - @classmethod - def get_genres_by_game(cls, game_name: str) -> List[str]: - items = [] - - for dump in cls.select().where(cls.name == game_name): - items += dump.genres - - return sorted(set(items)) - - @classmethod - def get_all_genres(cls) -> List[str]: - items = [] - - for dump in cls.select(): - items += dump.genres - - return sorted(set(items)) - - @classmethod - def get_all_games(cls) -> List[str]: - return [ - dump.name - for dump in cls.select(cls.name).order_by(cls.name).distinct() - ] - - @classmethod - def get_all_sites(cls) -> List[str]: - return [ - dump.site - for dump in cls.select(cls.site).order_by(cls.site).distinct() - ] - - @classmethod - def dump(cls) -> Dict[str, List[str]]: - game_by_genres = defaultdict(list) - - for dump in cls.select().order_by(cls.name): - game_by_genres[dump.name] += dump.genres - - for k, v in game_by_genres.items(): - game_by_genres[k] = sorted(set(v)) - - return game_by_genres - - class Meta: - indexes = ( - (("name", "site"), True), - ) - - def __repr__(self): - return f'Dump(name={self.name!r}, site={self.site!r}, genres={self.genres})' - - def __str__(self): - return repr(self) - - -db.connect() -db.create_tables([Dump]) - - -if __name__ == '__main__': - print('Total:', Dump.select().count()) - - genres = Dump.get_all_genres() - print(f'Genres ({len(genres)}): {genres}') - - games = Dump.get_all_games() - print(f'Games ({len(games)}): {games}') - - sites = Dump.get_all_sites() - print(f'Sites ({len(sites)}): {sites}') - - print() - - print(Dump.get_genres_by_game('Dead Space')) - # ['3D', 'Action', 'Adventure: Survival Horror', 'Arcade', 'Sci-Fi', 'Shooter', 'Third-Person', 'action', - # 'Боевик', 'Боевик от третьего лица', 'Боевик-приключения', 'Космос', 'От третьего лица', 'Ужасы', 'Шутер', - # 'Шутеры', 'Экшен', 'Экшены', 'ужасы'] - - # print() - # - # for x in Dump.get(): - # print(x) - # - # print() - - # - # For testing - # - # Dump.add(site='foo', name='123', genres=['RPG', 'Action']) - # Dump.add(site='foo', name='456', genres=['RPG']) - # - # for dump in Dump.select(): - # print(dump) - # - # # Dump(name='123', site='foo', genres=['RPG', 'Action']) - # # Dump(name='456', site='foo', genres=['RPG']) - # - # print() - # - # print(Dump.get_games_by_site('foo')) - # # [Dump(name='123', site='foo', genres=['RPG', 'Action']), Dump(name='456', site='foo', genres=['RPG'])] diff --git a/html_parsing/get_game_genres/export_import/data/games.json b/html_parsing/get_game_genres/export_import/data/games.json deleted file mode 100644 index fdc4e9037..000000000 --- a/html_parsing/get_game_genres/export_import/data/games.json +++ /dev/null @@ -1,85632 +0,0 @@ -[ - { - "id": 1, - "name": "35MM", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Квест" - ] - }, - { - "id": 2, - "name": "35MM", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 3, - "name": "35MM", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 4, - "name": "35MM", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5, - "name": "35MM", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 6, - "name": "35MM", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 7, - "name": "60 Seconds!", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Стратегия", - "Симулятор", - "Квест" - ] - }, - { - "id": 8, - "name": "35MM", - "site": "iwantgames_ru", - "genres": [ - "Ужасы" - ] - }, - { - "id": 9, - "name": "35MM", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Постапокалипсис", - "Выживание", - "Экшен", - "От первого лица" - ] - }, - { - "id": 10, - "name": "60 Seconds!", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Симулятор" - ] - }, - { - "id": 11, - "name": "60 Seconds!", - "site": "gamer_info_com", - "genres": [ - "инди", - "survival" - ] - }, - { - "id": 12, - "name": "60 Seconds!", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Казуальные", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 13, - "name": "35MM", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 14, - "name": "A Bird Story", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 15, - "name": "60 Seconds!", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 16, - "name": "60 Seconds!", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 17, - "name": "60 Seconds!", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 18, - "name": "35MM", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 19, - "name": "60 Seconds!", - "site": "playground_ru", - "genres": [ - "Инди", - "Аркада", - "Постапокалипсис", - "Выживание" - ] - }, - { - "id": 20, - "name": "A Bird Story", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 21, - "name": "A Plague Tale: Innocence", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 22, - "name": "A Bird Story", - "site": "gamer_info_com", - "genres": [ - "инди", - "приключения" - ] - }, - { - "id": 23, - "name": "A Bird Story", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 24, - "name": "60 Seconds!", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 25, - "name": "35MM", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 26, - "name": "A Bird Story", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 27, - "name": "A Bird Story", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 28, - "name": "35MM", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 29, - "name": "A Story About My Uncle", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 30, - "name": "A Bird Story", - "site": "playground_ru", - "genres": [ - "Ретро", - "Адвенчура", - "Инди" - ] - }, - { - "id": 31, - "name": "A Bird Story", - "site": "iwantgames_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 32, - "name": "A Plague Tale: Innocence", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 33, - "name": "35MM", - "site": "spong_com", - "genres": [] - }, - { - "id": 34, - "name": "A Plague Tale: Innocence", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 35, - "name": "A Plague Tale: Innocence", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Боевик" - ] - }, - { - "id": 36, - "name": "Adam Wolfe", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 37, - "name": "A Plague Tale: Innocence", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 38, - "name": "A Bird Story", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 39, - "name": "60 Seconds!", - "site": "store_steampowered_com", - "genres": [ - "Strategy", - "Casual", - "Indie", - "Adventure", - "Simulation" - ] - }, - { - "id": 40, - "name": "60 Seconds!", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 41, - "name": "A Plague Tale: Innocence", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 42, - "name": "A Plague Tale: Innocence", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Стелс" - ] - }, - { - "id": 43, - "name": "A Plague Tale: Innocence", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Приключения" - ] - }, - { - "id": 44, - "name": "A Story About My Uncle", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл", - "Платформер" - ] - }, - { - "id": 45, - "name": "60 Seconds!", - "site": "spong_com", - "genres": [] - }, - { - "id": 46, - "name": "A Story About My Uncle", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 47, - "name": "A Story About My Uncle", - "site": "gamer_info_com", - "genres": [ - "инди", - "платформеры" - ] - }, - { - "id": 48, - "name": "Alan Wake", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 49, - "name": "A Story About My Uncle", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Аркадные", - "Казуальные", - "Платформеры" - ] - }, - { - "id": 50, - "name": "A Plague Tale: Innocence", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 51, - "name": "Adam Wolfe", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 52, - "name": "A Bird Story", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 53, - "name": "A Story About My Uncle", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Платформер" - ] - }, - { - "id": 54, - "name": "Alan Wake's American Nightmare", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 55, - "name": "A Story About My Uncle", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 56, - "name": "A Story About My Uncle", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 57, - "name": "A Bird Story", - "site": "spong_com", - "genres": [] - }, - { - "id": 58, - "name": "Adam Wolfe", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 59, - "name": "35MM", - "site": "mobygames_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 60, - "name": "Adam Wolfe", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 61, - "name": "A Bird Story", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 62, - "name": "Adam Wolfe", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 63, - "name": "60 Seconds!", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 64, - "name": "Alice: Madness Returns", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 65, - "name": "Adam Wolfe", - "site": "playground_ru", - "genres": [] - }, - { - "id": 66, - "name": "A Story About My Uncle", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 67, - "name": "Alan Wake", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер" - ] - }, - { - "id": 68, - "name": "Alan Wake", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 69, - "name": "Adam Wolfe", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 70, - "name": "A Plague Tale: Innocence", - "site": "spong_com", - "genres": [] - }, - { - "id": 71, - "name": "A Plague Tale: Innocence", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 72, - "name": "Adam Wolfe", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 73, - "name": "Alan Wake", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "приключения", - "action" - ] - }, - { - "id": 74, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 75, - "name": "Alan Wake", - "site": "ag_ru", - "genres": [] - }, - { - "id": 76, - "name": "60 Seconds!", - "site": "mobygames_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 77, - "name": "Alan Wake's American Nightmare", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 78, - "name": "A Story About My Uncle", - "site": "spong_com", - "genres": [] - }, - { - "id": 79, - "name": "Alan Wake", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 80, - "name": "Alan Wake", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 81, - "name": "Adam Wolfe", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 82, - "name": "Alan Wake's American Nightmare", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер" - ] - }, - { - "id": 83, - "name": "Alan Wake's American Nightmare", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 84, - "name": "Alan Wake", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 85, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 86, - "name": "A Plague Tale: Innocence", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 87, - "name": "A Story About My Uncle", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 88, - "name": "Alice: Madness Returns", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 89, - "name": "Alan Wake's American Nightmare", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 90, - "name": "Alien Rage - Unlimited", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 91, - "name": "Adam Wolfe", - "site": "spong_com", - "genres": [] - }, - { - "id": 92, - "name": "Alice: Madness Returns", - "site": "gamer_info_com", - "genres": [ - "приключения", - "action" - ] - }, - { - "id": 93, - "name": "Alan Wake's American Nightmare", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 94, - "name": "Alan Wake's American Nightmare", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 95, - "name": "Alice: Madness Returns", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 96, - "name": "Alan Wake", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 97, - "name": "Alan Wake's American Nightmare", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Linear", - "Action Adventure" - ] - }, - { - "id": 98, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 99, - "name": "Alien: Isolation", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 100, - "name": "Adam Wolfe", - "site": "gamespot_com", - "genres": [ - "Puzzle", - "Hidden Object" - ] - }, - { - "id": 101, - "name": "A Bird Story", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 102, - "name": "Alice: Madness Returns", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 103, - "name": "A Story About My Uncle", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 104, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 105, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 106, - "name": "Alice: Madness Returns", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада" - ] - }, - { - "id": 107, - "name": "Alice: Madness Returns", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 108, - "name": "Alan Wake's American Nightmare", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 109, - "name": "Alice: Madness Returns", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 110, - "name": "Aliens vs. Predator (2010)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 111, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 112, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 113, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 114, - "name": "A Bird Story", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 115, - "name": "Alan Wake", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 116, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 117, - "name": "Aliens: Colonial Marines", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 118, - "name": "Alan Wake", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 119, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 120, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 121, - "name": "Alien Rage - Unlimited", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 122, - "name": "Alien Rage - Unlimited", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 123, - "name": "Adam Wolfe", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 124, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 125, - "name": "Alice: Madness Returns", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 126, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 127, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 128, - "name": "Alien Rage - Unlimited", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 129, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 130, - "name": "A Plague Tale: Innocence", - "site": "mobygames_com", - "genres": [ - "Compilation" - ] - }, - { - "id": 131, - "name": "Alien: Isolation", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 132, - "name": "A Plague Tale: Innocence", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 133, - "name": "Alan Wake's American Nightmare", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 134, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 135, - "name": "Alone in the Dark (2008)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 136, - "name": "Alan Wake's American Nightmare", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 137, - "name": "Alien: Isolation", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 138, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 139, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 140, - "name": "Alien Rage - Unlimited", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 141, - "name": "Alien Rage - Unlimited", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 142, - "name": "Alan Wake", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 143, - "name": "Alien: Isolation", - "site": "gamer_info_com", - "genres": [ - "ужасы" - ] - }, - { - "id": 144, - "name": "Aliens vs. Predator (2010)", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 145, - "name": "Alice: Madness Returns", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 146, - "name": "Amnesia: A Machine for Pigs", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 147, - "name": "Alien Rage - Unlimited", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 148, - "name": "Aliens vs. Predator (2010)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 149, - "name": "Alien Rage - Unlimited", - "site": "playground_ru", - "genres": [] - }, - { - "id": 150, - "name": "Alien: Isolation", - "site": "ag_ru", - "genres": [] - }, - { - "id": 151, - "name": "Alien: Isolation", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 152, - "name": "Amnesia: The Dark Descent", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 153, - "name": "Aliens: Colonial Marines", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 154, - "name": "Aliens vs. Predator (2010)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 155, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 156, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 157, - "name": "Alien: Isolation", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 158, - "name": "Alan Wake's American Nightmare", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 159, - "name": "Among The Sleep", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 160, - "name": "Aliens vs. Predator (2010)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 161, - "name": "Alien: Isolation", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ужасы" - ] - }, - { - "id": 162, - "name": "A Story About My Uncle", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 163, - "name": "Alice: Madness Returns", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 164, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 165, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 166, - "name": "Aliens vs. Predator (2010)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 167, - "name": "Aliens: Colonial Marines", - "site": "gamer_info_com", - "genres": [ - "командные шутеры", - "FPS" - ] - }, - { - "id": 168, - "name": "Alien Rage - Unlimited", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 169, - "name": "Angry Birds", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "На логику", - "Головоломка" - ] - }, - { - "id": 170, - "name": "Aliens vs. Predator (2010)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 171, - "name": "Alice: Madness Returns", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 172, - "name": "Aliens: Colonial Marines", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 173, - "name": "A Story About My Uncle", - "site": "metacritic_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 174, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 175, - "name": "Aliens: Colonial Marines", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 176, - "name": "Aliens vs. Predator (2010)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 177, - "name": "Alone in the Dark (2008)", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 178, - "name": "Angry Birds Rio", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 179, - "name": "Alien Rage - Unlimited", - "site": "spong_com", - "genres": [] - }, - { - "id": 180, - "name": "Aliens: Colonial Marines", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 181, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 182, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 183, - "name": "Alien: Isolation", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 184, - "name": "Aliens: Colonial Marines", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 185, - "name": "Angry Birds Seasons", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 186, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 187, - "name": "Adam Wolfe", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 188, - "name": "Amnesia: A Machine for Pigs", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 189, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 190, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 191, - "name": "Alien: Isolation", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Adventure: Survival Horror" - ] - }, - { - "id": 192, - "name": "Aliens: Colonial Marines", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 193, - "name": "Alone in the Dark (2008)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 194, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 195, - "name": "Alone in the Dark (2008)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 196, - "name": "Angry Birds Space", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "На логику", - "Головоломка" - ] - }, - { - "id": 197, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 198, - "name": "Aliens vs. Predator (2010)", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 199, - "name": "Amnesia: The Dark Descent", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 200, - "name": "Alien Rage - Unlimited", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 201, - "name": "Alone in the Dark (2008)", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 202, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 203, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 204, - "name": "Aliens vs. Predator (2010)", - "site": "spong_com", - "genres": [] - }, - { - "id": 205, - "name": "Alan Wake", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 206, - "name": "Angry Birds Star Wars", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 207, - "name": "Alone in the Dark (2008)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 208, - "name": "Amnesia: A Machine for Pigs", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "приключения" - ] - }, - { - "id": 209, - "name": "Amnesia: A Machine for Pigs", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл" - ] - }, - { - "id": 210, - "name": "Among The Sleep", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 211, - "name": "Alone in the Dark (2008)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 212, - "name": "Aliens: Colonial Marines", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 213, - "name": "Angry Birds Star Wars II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 214, - "name": "Aliens: Colonial Marines", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 215, - "name": "Adam Wolfe", - "site": "metacritic_com", - "genres": [ - "General", - "Hidden Object", - "Puzzle" - ] - }, - { - "id": 216, - "name": "Amnesia: A Machine for Pigs", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 217, - "name": "Alone in the Dark (2008)", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Ужасы", - "Адвенчура", - "Экшен", - "От первого лица" - ] - }, - { - "id": 218, - "name": "Amnesia: The Dark Descent", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 219, - "name": "Alien: Isolation", - "site": "gamespot_com", - "genres": [ - "VR", - "3D", - "Action", - "Survival", - "Adventure" - ] - }, - { - "id": 220, - "name": "Amnesia: A Machine for Pigs", - "site": "ag_ru", - "genres": [] - }, - { - "id": 221, - "name": "Amnesia: The Dark Descent", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 222, - "name": "Armored Kitten", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 223, - "name": "Alien Rage - Unlimited", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 224, - "name": "Angry Birds", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Логика" - ] - }, - { - "id": 225, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 226, - "name": "Amnesia: A Machine for Pigs", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 227, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 228, - "name": "Aliens vs. Predator (2010)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 229, - "name": "Among The Sleep", - "site": "gamer_info_com", - "genres": [ - "ужасы" - ] - }, - { - "id": 230, - "name": "Amnesia: The Dark Descent", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 231, - "name": "Ashes", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 232, - "name": "Amnesia: A Machine for Pigs", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "От первого лица" - ] - }, - { - "id": 233, - "name": "Amnesia: The Dark Descent", - "site": "ag_ru", - "genres": [] - }, - { - "id": 234, - "name": "Among The Sleep", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 235, - "name": "Alan Wake's American Nightmare", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 236, - "name": "Alone in the Dark (2008)", - "site": "spong_com", - "genres": [] - }, - { - "id": 237, - "name": "Amnesia: The Dark Descent", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 238, - "name": "Angry Birds Rio", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Логика" - ] - }, - { - "id": 239, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 240, - "name": "Alone in the Dark (2008)", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 241, - "name": "Angry Birds", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические" - ] - }, - { - "id": 242, - "name": "Alien: Isolation", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 243, - "name": "Among The Sleep", - "site": "gamefaqs_gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 244, - "name": "Amnesia: The Dark Descent", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "Инди", - "От первого лица" - ] - }, - { - "id": 245, - "name": "Among The Sleep", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 246, - "name": "Aliens: Colonial Marines", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 247, - "name": "Angry Birds", - "site": "gamebomb_ru", - "genres": [ - "Логические игры", - "Головоломки/Пазл" - ] - }, - { - "id": 248, - "name": "Angry Birds Seasons", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Логика" - ] - }, - { - "id": 249, - "name": "BLACK ROSE", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 250, - "name": "Among The Sleep", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 251, - "name": "Angry Birds Rio", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические" - ] - }, - { - "id": 252, - "name": "Amnesia: A Machine for Pigs", - "site": "spong_com", - "genres": [] - }, - { - "id": 253, - "name": "Amnesia: A Machine for Pigs", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 254, - "name": "Angry Birds", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 255, - "name": "Alice: Madness Returns", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 256, - "name": "Aliens vs. Predator (2010)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 257, - "name": "Alan Wake", - "site": "metacritic_com", - "genres": [ - "Modern", - "Survival", - "Action Adventure", - "Horror" - ] - }, - { - "id": 258, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 259, - "name": "Angry Birds", - "site": "ag_ru", - "genres": [ - "Экшены", - "Головоломки", - "Аркадные" - ] - }, - { - "id": 260, - "name": "Among The Sleep", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "Экшен", - "Виртуальная реальность", - "От первого лица" - ] - }, - { - "id": 261, - "name": "Bad Dream: Bridge", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 262, - "name": "Angry Birds Space", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Логика" - ] - }, - { - "id": 263, - "name": "Angry Birds Rio", - "site": "gamebomb_ru", - "genres": [ - "Головоломки/Пазл" - ] - }, - { - "id": 264, - "name": "Angry Birds", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 265, - "name": "Angry Birds Seasons", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические" - ] - }, - { - "id": 266, - "name": "Bad Dream: Butcher", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 267, - "name": "Amnesia: The Dark Descent", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 268, - "name": "Angry Birds Rio", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 269, - "name": "Amnesia: The Dark Descent", - "site": "spong_com", - "genres": [] - }, - { - "id": 270, - "name": "Angry Birds Star Wars", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 271, - "name": "Angry Birds Rio", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Казуальные", - "Аркадные" - ] - }, - { - "id": 272, - "name": "Aliens: Colonial Marines", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 273, - "name": "Angry Birds", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада" - ] - }, - { - "id": 274, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 275, - "name": "Angry Birds Seasons", - "site": "gamebomb_ru", - "genres": [ - "Головоломки/Пазл" - ] - }, - { - "id": 276, - "name": "Alone in the Dark (2008)", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 277, - "name": "Bad Dream: Coma", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 278, - "name": "Angry Birds Rio", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 279, - "name": "Angry Birds Space", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические" - ] - }, - { - "id": 280, - "name": "Angry Birds Star Wars II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 281, - "name": "Among The Sleep", - "site": "spong_com", - "genres": [] - }, - { - "id": 282, - "name": "Angry Birds Seasons", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 283, - "name": "Angry Birds Seasons", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Головоломки", - "Аркадные" - ] - }, - { - "id": 284, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 285, - "name": "Bad Dream: Cyclops", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 286, - "name": "Angry Birds Rio", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада" - ] - }, - { - "id": 287, - "name": "Among The Sleep", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 288, - "name": "Angry Birds Space", - "site": "gamebomb_ru", - "genres": [ - "Головоломки/Пазл" - ] - }, - { - "id": 289, - "name": "Amnesia: A Machine for Pigs", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 290, - "name": "Angry Birds Star Wars", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические" - ] - }, - { - "id": 291, - "name": "Angry Birds Seasons", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 292, - "name": "Armored Kitten", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 293, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 294, - "name": "Bad Dream: Graveyard", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 295, - "name": "Angry Birds", - "site": "spong_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 296, - "name": "Alan Wake's American Nightmare", - "site": "metacritic_com", - "genres": [ - "Modern", - "General", - "Linear", - "Action Adventure" - ] - }, - { - "id": 297, - "name": "Angry Birds Space", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 298, - "name": "Angry Birds Space", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Казуальные", - "Аркадные" - ] - }, - { - "id": 299, - "name": "Alone in the Dark (2008)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 300, - "name": "Angry Birds Seasons", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада" - ] - }, - { - "id": 301, - "name": "Angry Birds Star Wars", - "site": "gamebomb_ru", - "genres": [ - "Головоломки/Пазл" - ] - }, - { - "id": 302, - "name": "Angry Birds", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 303, - "name": "Bad Dream: Hospital", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 304, - "name": "Angry Birds Star Wars II", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические" - ] - }, - { - "id": 305, - "name": "Angry Birds Rio", - "site": "spong_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 306, - "name": "Angry Birds Space", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 307, - "name": "Ashes", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Приключение", - "Боевик" - ] - }, - { - "id": 308, - "name": "Amnesia: The Dark Descent", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 309, - "name": "Angry Birds Star Wars", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Аркадные" - ] - }, - { - "id": 310, - "name": "Alien Rage - Unlimited", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 311, - "name": "Angry Birds Star Wars", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 312, - "name": "Angry Birds Star Wars II", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 313, - "name": "Bad Dream: Memories", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 314, - "name": "Angry Birds Space", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада" - ] - }, - { - "id": 315, - "name": "Angry Birds Rio", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 316, - "name": "Armored Kitten", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 317, - "name": "Angry Birds Seasons", - "site": "spong_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 318, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 319, - "name": "Angry Birds Star Wars", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 320, - "name": "Armored Kitten", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 321, - "name": "Baldur's Gate", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 322, - "name": "Amnesia: A Machine for Pigs", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 323, - "name": "Angry Birds Star Wars II", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Аркадные" - ] - }, - { - "id": 324, - "name": "Among The Sleep", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 325, - "name": "Angry Birds Space", - "site": "spong_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 326, - "name": "Angry Birds Star Wars", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада" - ] - }, - { - "id": 327, - "name": "Angry Birds Star Wars II", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 328, - "name": "Ashes", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 329, - "name": "Ashes", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 330, - "name": "Bastion", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 331, - "name": "BLACK ROSE", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 332, - "name": "Angry Birds Seasons", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 333, - "name": "Alien: Isolation", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 334, - "name": "Angry Birds Star Wars II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 335, - "name": "Armored Kitten", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Аркадные" - ] - }, - { - "id": 336, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 337, - "name": "Batman: The Telltale Series", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 338, - "name": "Angry Birds Star Wars", - "site": "spong_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 339, - "name": "Alice: Madness Returns", - "site": "metacritic_com", - "genres": [ - "General", - "Survival", - "Action Adventure", - "Horror", - "Adventure" - ] - }, - { - "id": 340, - "name": "Bad Dream: Bridge", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 341, - "name": "Armored Kitten", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 342, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 343, - "name": "Angry Birds Star Wars II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 344, - "name": "Angry Birds", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 345, - "name": "Amnesia: The Dark Descent", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 346, - "name": "Aliens vs. Predator (2010)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 347, - "name": "Angry Birds Space", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 348, - "name": "Ashes", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Инди", - "Приключения" - ] - }, - { - "id": 349, - "name": "BLACK ROSE", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 350, - "name": "Armored Kitten", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 351, - "name": "Battlefield 1", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 352, - "name": "Angry Birds Star Wars II", - "site": "spong_com", - "genres": [] - }, - { - "id": 353, - "name": "Bad Dream: Butcher", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 354, - "name": "Armored Kitten", - "site": "playground_ru", - "genres": [] - }, - { - "id": 355, - "name": "Ashes", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 356, - "name": "BLACK ROSE", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 357, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 358, - "name": "Bad Dream: Bridge", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 359, - "name": "Battlefield 1942", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 360, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 361, - "name": "Among The Sleep", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 362, - "name": "Angry Birds Star Wars", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 363, - "name": "Ashes", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 364, - "name": "Aliens: Colonial Marines", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 365, - "name": "Angry Birds Rio", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 366, - "name": "Armored Kitten", - "site": "spong_com", - "genres": [] - }, - { - "id": 367, - "name": "Bad Dream: Coma", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 368, - "name": "Ashes", - "site": "playground_ru", - "genres": [] - }, - { - "id": 369, - "name": "Bad Dream: Butcher", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 370, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 371, - "name": "Battlefield 4", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 372, - "name": "Bad Dream: Bridge", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 373, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 374, - "name": "BLACK ROSE", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 375, - "name": "Angry Birds Star Wars II", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 376, - "name": "Angry Birds", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 377, - "name": "Bad Dream: Cyclops", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 378, - "name": "Ashes", - "site": "spong_com", - "genres": [] - }, - { - "id": 379, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 380, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "playground_ru", - "genres": [] - }, - { - "id": 381, - "name": "Battlefield Hardline", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 382, - "name": "Bad Dream: Coma", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 383, - "name": "BLACK ROSE", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 384, - "name": "Angry Birds Seasons", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 385, - "name": "Bad Dream: Butcher", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 386, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 387, - "name": "Alien Rage - Unlimited", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 388, - "name": "Bad Dream: Graveyard", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 389, - "name": "Battlefield V", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 390, - "name": "BLACK ROSE", - "site": "playground_ru", - "genres": [] - }, - { - "id": 391, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "spong_com", - "genres": [] - }, - { - "id": 392, - "name": "Bad Dream: Cyclops", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 393, - "name": "Angry Birds Rio", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 394, - "name": "Bad Dream: Bridge", - "site": "ag_ru", - "genres": [] - }, - { - "id": 395, - "name": "BLACK ROSE", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 396, - "name": "Armored Kitten", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 397, - "name": "Bad Dream: Bridge", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 398, - "name": "Bad Dream: Coma", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 399, - "name": "Battlefield Vietnam", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 400, - "name": "Bad Dream: Hospital", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 401, - "name": "Angry Birds Space", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 402, - "name": "Alone in the Dark (2008)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 403, - "name": "Bad Dream: Graveyard", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 404, - "name": "Bad Dream: Bridge", - "site": "playground_ru", - "genres": [] - }, - { - "id": 405, - "name": "BLACK ROSE", - "site": "spong_com", - "genres": [] - }, - { - "id": 406, - "name": "Bad Dream: Butcher", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 407, - "name": "Bad Dream: Butcher", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 408, - "name": "Angry Birds Seasons", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 409, - "name": "Bad Dream: Bridge", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 410, - "name": "Bayonetta", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Слэшер" - ] - }, - { - "id": 411, - "name": "Ashes", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 412, - "name": "Bad Dream: Hospital", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 413, - "name": "Bad Dream: Memories", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 414, - "name": "Bad Dream: Cyclops", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 415, - "name": "Bad Dream: Butcher", - "site": "playground_ru", - "genres": [] - }, - { - "id": 416, - "name": "Bad Dream: Bridge", - "site": "spong_com", - "genres": [] - }, - { - "id": 417, - "name": "Beholder", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 418, - "name": "Angry Birds Star Wars", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 419, - "name": "Bad Dream: Coma", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 420, - "name": "Angry Birds Space", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 421, - "name": "Amnesia: A Machine for Pigs", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 422, - "name": "Bad Dream: Memories", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 423, - "name": "Bad Dream: Butcher", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 424, - "name": "Bad Dream: Coma", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 425, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 426, - "name": "Bad Dream: Graveyard", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 427, - "name": "Baldur's Gate", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 428, - "name": "Beholder (Beta)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 429, - "name": "Bad Dream: Butcher", - "site": "spong_com", - "genres": [] - }, - { - "id": 430, - "name": "Bad Dream: Coma", - "site": "playground_ru", - "genres": [ - "Аркада", - "Адвенчура", - "Инди" - ] - }, - { - "id": 431, - "name": "Bad Dream: Cyclops", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 432, - "name": "Alien: Isolation", - "site": "metacritic_com", - "genres": [ - "Sci-Fi", - "Survival", - "General", - "Action Adventure" - ] - }, - { - "id": 433, - "name": "Baldur's Gate", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 434, - "name": "Bad Dream: Coma", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 435, - "name": "Angry Birds Star Wars", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 436, - "name": "Angry Birds Star Wars II", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 437, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "Инди", - "Квест" - ] - }, - { - "id": 438, - "name": "BLACK ROSE", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 439, - "name": "Bad Dream: Hospital", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 440, - "name": "Bastion", - "site": "igromania_ru", - "genres": [ - "Логика", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 441, - "name": "Bad Dream: Cyclops", - "site": "ag_ru", - "genres": [] - }, - { - "id": 442, - "name": "Bad Dream: Coma", - "site": "spong_com", - "genres": [] - }, - { - "id": 443, - "name": "Bad Dream: Cyclops", - "site": "playground_ru", - "genres": [] - }, - { - "id": 444, - "name": "Amnesia: The Dark Descent", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 445, - "name": "Bad Dream: Graveyard", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 446, - "name": "Aliens vs. Predator (2010)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 447, - "name": "Ben and Ed", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 448, - "name": "Bastion", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 449, - "name": "Bad Dream: Cyclops", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 450, - "name": "Bad Dream: Memories", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 451, - "name": "Batman: The Telltale Series", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 452, - "name": "Bad Dream: Hospital", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 453, - "name": "Angry Birds Star Wars II", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 454, - "name": "Bad Dream: Cyclops", - "site": "spong_com", - "genres": [] - }, - { - "id": 455, - "name": "Bad Dream: Bridge", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 456, - "name": "Bad Dream: Graveyard", - "site": "playground_ru", - "genres": [] - }, - { - "id": 457, - "name": "Bad Dream: Graveyard", - "site": "ag_ru", - "genres": [] - }, - { - "id": 458, - "name": "Ben and Ed: Blood Party", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 459, - "name": "Among The Sleep", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 460, - "name": "Armored Kitten", - "site": "gamespot_com", - "genres": [ - "Shooter", - "Action", - "2D", - "Fixed-Screen" - ] - }, - { - "id": 461, - "name": "Batman: The Telltale Series", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 462, - "name": "Battlefield 1", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 463, - "name": "Baldur's Gate", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 464, - "name": "Bad Dream: Graveyard", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 465, - "name": "Bad Dream: Graveyard", - "site": "spong_com", - "genres": [] - }, - { - "id": 466, - "name": "Bad Dream: Memories", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 467, - "name": "Besiege", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Стратегия", - "Симулятор" - ] - }, - { - "id": 468, - "name": "Bad Dream: Hospital", - "site": "playground_ru", - "genres": [] - }, - { - "id": 469, - "name": "Bad Dream: Butcher", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 470, - "name": "Bad Dream: Hospital", - "site": "ag_ru", - "genres": [] - }, - { - "id": 471, - "name": "Armored Kitten", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 472, - "name": "Battlefield 1942", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 473, - "name": "Battlefield 1", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Многопользоватеский шутер", - "Боевик/Экшн" - ] - }, - { - "id": 474, - "name": "Beyond Good and Evil", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 475, - "name": "Bastion", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 476, - "name": "Bad Dream: Hospital", - "site": "spong_com", - "genres": [] - }, - { - "id": 477, - "name": "Bad Dream: Memories", - "site": "playground_ru", - "genres": [] - }, - { - "id": 478, - "name": "Angry Birds", - "site": "mobygames_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 479, - "name": "Bad Dream: Hospital", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 480, - "name": "Baldur's Gate", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 481, - "name": "Bad Dream: Coma", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 482, - "name": "Bad Dream: Memories", - "site": "ag_ru", - "genres": [] - }, - { - "id": 483, - "name": "Battlefield 4", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 484, - "name": "Ashes", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 485, - "name": "Battlefield 1942", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер" - ] - }, - { - "id": 486, - "name": "Bad Dream: Memories", - "site": "spong_com", - "genres": [] - }, - { - "id": 487, - "name": "Batman: The Telltale Series", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 488, - "name": "Aliens: Colonial Marines", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Sci-Fi", - "Action", - "First-Person", - "Shooter" - ] - }, - { - "id": 489, - "name": "Baldur's Gate", - "site": "playground_ru", - "genres": [ - "Фэнтези", - "Ролевая", - "Вид сверху" - ] - }, - { - "id": 490, - "name": "Bad Dream: Memories", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 491, - "name": "Bastion", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 492, - "name": "Angry Birds Rio", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 493, - "name": "Baldur's Gate", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Стратегии" - ] - }, - { - "id": 494, - "name": "Bad Dream: Cyclops", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 495, - "name": "Ashes", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Casual", - "Adventure" - ] - }, - { - "id": 496, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 497, - "name": "Battlefield Hardline", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 498, - "name": "Binary Domain", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 499, - "name": "Baldur's Gate", - "site": "spong_com", - "genres": [] - }, - { - "id": 500, - "name": "Battlefield 1", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 501, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 502, - "name": "Bastion", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая", - "Платформер", - "Вид сверху" - ] - }, - { - "id": 503, - "name": "Baldur's Gate", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 504, - "name": "Battlefield V", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица" - ] - }, - { - "id": 505, - "name": "BioShock", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 506, - "name": "Bastion", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди" - ] - }, - { - "id": 507, - "name": "Bad Dream: Graveyard", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 508, - "name": "Batman: The Telltale Series", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 509, - "name": "Angry Birds Seasons", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 510, - "name": "Battlefield 4", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Многопользоватеский шутер", - "Боевик/Экшн" - ] - }, - { - "id": 511, - "name": "Bastion", - "site": "spong_com", - "genres": [] - }, - { - "id": 512, - "name": "Battlefield 1942", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 513, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 514, - "name": "Alone in the Dark (2008)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 515, - "name": "Batman: The Telltale Series", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 516, - "name": "BioShock 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 517, - "name": "Battlefield Vietnam", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 518, - "name": "Bastion", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 519, - "name": "Bad Dream: Hospital", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 520, - "name": "Batman: The Telltale Series", - "site": "spong_com", - "genres": [] - }, - { - "id": 521, - "name": "Battlefield 1", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 522, - "name": "BLACK ROSE", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 523, - "name": "Angry Birds Space", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 524, - "name": "BioShock Infinite", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 525, - "name": "Battlefield 4", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 526, - "name": "Batman: The Telltale Series", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 527, - "name": "Battlefield Hardline", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 528, - "name": "Bayonetta", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 529, - "name": "Battlefield 1", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 530, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Аддон" - ] - }, - { - "id": 531, - "name": "Bad Dream: Bridge", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 532, - "name": "Battlefield 1", - "site": "spong_com", - "genres": [] - }, - { - "id": 533, - "name": "Batman: The Telltale Series", - "site": "iwantgames_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 534, - "name": "BLACK ROSE", - "site": "store_steampowered_com", - "genres": [ - "Free to Play", - "Indie", - "Adventure" - ] - }, - { - "id": 535, - "name": "Battlefield 1942", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 536, - "name": "Battlefield Hardline", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 537, - "name": "Bad Dream: Memories", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 538, - "name": "Beholder", - "site": "igromania_ru", - "genres": [ - "Симулятор жизни" - ] - }, - { - "id": 539, - "name": "Battlefield 1", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 540, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 541, - "name": "Battlefield V", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Многопользоватеский шутер", - "Боевик/Экшн" - ] - }, - { - "id": 542, - "name": "Bad Dream: Butcher", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 543, - "name": "Angry Birds Star Wars", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 544, - "name": "Battlefield 1942", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Strategy: Combat" - ] - }, - { - "id": 545, - "name": "Battlefield V", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 546, - "name": "Bad Dream: Bridge", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 547, - "name": "Battlefield 4", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 548, - "name": "Battlefield 1", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 549, - "name": "Beholder (Beta)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 550, - "name": "Bionic Commando", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 551, - "name": "Baldur's Gate", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 552, - "name": "Battlefield 1942", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 553, - "name": "Battlefield 4", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 554, - "name": "Battlefield Vietnam", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 555, - "name": "Amnesia: A Machine for Pigs", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure", - "Horror" - ] - }, - { - "id": 556, - "name": "Angry Birds Star Wars II", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 557, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 558, - "name": "Battlefield Vietnam", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 559, - "name": "Black Mesa", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Ремейк", - "Шутер" - ] - }, - { - "id": 560, - "name": "Battlefield Hardline", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 561, - "name": "Battlefield 1942", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 562, - "name": "Bad Dream: Butcher", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 563, - "name": "Bastion", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 564, - "name": "Battlefield Hardline", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 565, - "name": "Battlefield 1942", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 566, - "name": "Bayonetta", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 567, - "name": "Bad Dream: Coma", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 568, - "name": "BlackSite: Area 51", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 569, - "name": "Ben and Ed", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 570, - "name": "Battlefield 4", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 571, - "name": "Armored Kitten", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 572, - "name": "Bayonetta", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 573, - "name": "Battlefield V", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 574, - "name": "Battlefield 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 575, - "name": "Blades of Time", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 576, - "name": "Battlefield V", - "site": "spong_com", - "genres": [] - }, - { - "id": 577, - "name": "Bad Dream: Cyclops", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 578, - "name": "Amnesia: The Dark Descent", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure", - "Horror" - ] - }, - { - "id": 579, - "name": "Batman: The Telltale Series", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 580, - "name": "Ben and Ed: Blood Party", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 581, - "name": "Beholder", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Симулятор" - ] - }, - { - "id": 582, - "name": "Bad Dream: Coma", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 583, - "name": "Battlefield 4", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 584, - "name": "Beholder", - "site": "gamer_info_com", - "genres": [ - "инди" - ] - }, - { - "id": 585, - "name": "Battlefield Hardline", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 586, - "name": "Battlefield Vietnam", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 587, - "name": "Blades of Time: Dismal Swamp", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 588, - "name": "Ashes", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 589, - "name": "Bad Dream: Graveyard", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 590, - "name": "Battlefield Hardline", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 591, - "name": "Battlefield Vietnam", - "site": "spong_com", - "genres": [ - "Combat Game", - "Shoot 'Em Up" - ] - }, - { - "id": 592, - "name": "Beholder (Beta)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 593, - "name": "Besiege", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 594, - "name": "Battlefield 1", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 595, - "name": "Bad Dream: Cyclops", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 596, - "name": "Blood Omen 2: Legacy of Kain", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 597, - "name": "Battlefield Hardline", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 598, - "name": "Beholder (Beta)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 599, - "name": "Battlefield V", - "site": "ag_ru", - "genres": [ - "Шутеры", - "ММО", - "Экшены", - "Приключения" - ] - }, - { - "id": 600, - "name": "Bayonetta", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Linear", - "Action Adventure" - ] - }, - { - "id": 601, - "name": "Bad Dream: Hospital", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 602, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 603, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 604, - "name": "Beyond Good and Evil", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 605, - "name": "Bayonetta", - "site": "spong_com", - "genres": [ - "Combat Game" - ] - }, - { - "id": 606, - "name": "Battlefield V", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 607, - "name": "BloodRayne", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 608, - "name": "Bad Dream: Graveyard", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 609, - "name": "Battlefield 1942", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 610, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 611, - "name": "Bad Dream: Memories", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 612, - "name": "Battlefield V", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 613, - "name": "Ben and Ed", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Платформер", - "Боевик/Экшн" - ] - }, - { - "id": 614, - "name": "Battlefield Vietnam", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 615, - "name": "Beholder", - "site": "spong_com", - "genres": [] - }, - { - "id": 616, - "name": "Beholder", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 617, - "name": "BloodRayne 2", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 618, - "name": "Binary Domain", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 619, - "name": "BLACK ROSE", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 620, - "name": "Battlefield Vietnam", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 621, - "name": "Ben and Ed: Blood Party", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 622, - "name": "Ben and Ed", - "site": "gamer_info_com", - "genres": [ - "инди", - "аркадные" - ] - }, - { - "id": 623, - "name": "BloodRayne: Betrayal", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 624, - "name": "Beholder (Beta)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 625, - "name": "Bad Dream: Hospital", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 626, - "name": "Battlefield 4", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 627, - "name": "Beholder (Beta)", - "site": "spong_com", - "genres": [] - }, - { - "id": 628, - "name": "Battlefield Vietnam", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 629, - "name": "Baldur's Gate", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 630, - "name": "Bad Dream: Bridge", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 631, - "name": "BioShock", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 632, - "name": "Bayonetta", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 633, - "name": "Bayonetta", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 634, - "name": "Bloodbath kavkaz", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 635, - "name": "Besiege", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 636, - "name": "Ben and Ed: Blood Party", - "site": "gamer_info_com", - "genres": [ - "инди", - "аркадные" - ] - }, - { - "id": 637, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 638, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 639, - "name": "Among The Sleep", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 640, - "name": "Bad Dream: Memories", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 641, - "name": "BioShock 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 642, - "name": "Bad Dream: Butcher", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 643, - "name": "Beholder", - "site": "ag_ru", - "genres": [ - "Приключения", - "Ролевые", - "Инди", - "Стратегии" - ] - }, - { - "id": 644, - "name": "Battlefield Hardline", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 645, - "name": "Bloodline: Линия крови", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 646, - "name": "Beyond Good and Evil", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 647, - "name": "Beholder", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 648, - "name": "Besiege", - "site": "gamer_info_com", - "genres": [ - "симуляторы", - "инди" - ] - }, - { - "id": 649, - "name": "Ben and Ed", - "site": "spong_com", - "genres": [] - }, - { - "id": 650, - "name": "Ben and Ed", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 651, - "name": "BioShock Infinite", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 652, - "name": "Blue Estate", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 653, - "name": "Baldur's Gate", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 654, - "name": "Bastion", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 655, - "name": "Beholder (Beta)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 656, - "name": "Battlefield V", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 657, - "name": "Binary Domain", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 658, - "name": "Bayonetta", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Слэшер" - ] - }, - { - "id": 659, - "name": "Ben and Ed: Blood Party", - "site": "spong_com", - "genres": [] - }, - { - "id": 660, - "name": "Bad Dream: Coma", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 661, - "name": "Beholder (Beta)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 662, - "name": "Beyond Good and Evil", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 663, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 664, - "name": "Blues and Bullets: Episode 1", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 665, - "name": "Ben and Ed: Blood Party", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 666, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 667, - "name": "Battlefield Vietnam", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 668, - "name": "Besiege", - "site": "spong_com", - "genres": [ - "Strategy: Combat" - ] - }, - { - "id": 669, - "name": "Batman: The Telltale Series", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 670, - "name": "BioShock", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 671, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 672, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 673, - "name": "Bad Dream: Cyclops", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 674, - "name": "Binary Domain", - "site": "gamer_info_com", - "genres": [ - "командные шутеры" - ] - }, - { - "id": 675, - "name": "Beholder", - "site": "playground_ru", - "genres": [ - "Инди", - "Адвенчура", - "Стратегия" - ] - }, - { - "id": 676, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 677, - "name": "Besiege", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 678, - "name": "Bastion", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 679, - "name": "Boogeyman", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 680, - "name": "Beyond Good and Evil", - "site": "spong_com", - "genres": [] - }, - { - "id": 681, - "name": "Beholder (Beta)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 682, - "name": "Ben and Ed", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 683, - "name": "Bionic Commando", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 684, - "name": "BioShock 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 685, - "name": "BioShock", - "site": "gamer_info_com", - "genres": [ - "FPS", - "RPG", - "action" - ] - }, - { - "id": 686, - "name": "Bad Dream: Graveyard", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 687, - "name": "Bayonetta", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 688, - "name": "Beyond Good and Evil", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 689, - "name": "Ben and Ed", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 690, - "name": "Battlefield 1", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 691, - "name": "Borderlands", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG", - "Шутер" - ] - }, - { - "id": 692, - "name": "Binary Domain", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 693, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 694, - "name": "Angry Birds", - "site": "metacritic_com", - "genres": [ - "General", - "Strategy", - "Miscellaneous", - "Puzzle", - "Action" - ] - }, - { - "id": 695, - "name": "Batman: The Telltale Series", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 696, - "name": "Ben and Ed: Blood Party", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди" - ] - }, - { - "id": 697, - "name": "Borderlands 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 698, - "name": "BioShock 2", - "site": "gamer_info_com", - "genres": [ - "FPS", - "RPG", - "action" - ] - }, - { - "id": 699, - "name": "Black Mesa", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 700, - "name": "Binary Domain", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Third-Person", - "Action", - "Tactical", - "Shooter" - ] - }, - { - "id": 701, - "name": "Beholder", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 702, - "name": "Bad Dream: Hospital", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 703, - "name": "Ben and Ed: Blood Party", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 704, - "name": "BioShock", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Adventure" - ] - }, - { - "id": 705, - "name": "Battlefield 1942", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 706, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG", - "Шутер" - ] - }, - { - "id": 707, - "name": "Ben and Ed", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "Адвенчура" - ] - }, - { - "id": 708, - "name": "BlackSite: Area 51", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 709, - "name": "BioShock Infinite", - "site": "gamer_info_com", - "genres": [ - "FPS", - "action" - ] - }, - { - "id": 710, - "name": "BioShock", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 711, - "name": "Battlefield 1", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 712, - "name": "Besiege", - "site": "ag_ru", - "genres": [ - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 713, - "name": "Bad Dream: Memories", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 714, - "name": "Besiege", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 715, - "name": "Beholder (Beta)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 716, - "name": "BioShock 2", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 717, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 718, - "name": "Ben and Ed: Blood Party", - "site": "playground_ru", - "genres": [] - }, - { - "id": 719, - "name": "Blades of Time", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 720, - "name": "Battlefield 4", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 721, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 722, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 723, - "name": "Beyond Good and Evil", - "site": "ag_ru", - "genres": [] - }, - { - "id": 724, - "name": "Battlefield 1942", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 725, - "name": "BioShock Infinite", - "site": "spong_com", - "genres": [] - }, - { - "id": 726, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 727, - "name": "Beyond Good and Evil", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 728, - "name": "BioShock 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 729, - "name": "Blades of Time: Dismal Swamp", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 730, - "name": "Besiege", - "site": "playground_ru", - "genres": [ - "Логические" - ] - }, - { - "id": 731, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 732, - "name": "Baldur's Gate", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 733, - "name": "BioShock Infinite", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 734, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 735, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 736, - "name": "Blood Omen 2: Legacy of Kain", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 737, - "name": "Binary Domain", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 738, - "name": "Angry Birds Rio", - "site": "metacritic_com", - "genres": [ - "General", - "Action", - "Puzzle" - ] - }, - { - "id": 739, - "name": "Ben and Ed", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 740, - "name": "BioShock Infinite", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 741, - "name": "Binary Domain", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 742, - "name": "Battlefield 4", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 743, - "name": "Battlefield Hardline", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 744, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG", - "Шутер" - ] - }, - { - "id": 745, - "name": "Beyond Good and Evil", - "site": "playground_ru", - "genres": [] - }, - { - "id": 746, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 747, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 748, - "name": "BloodRayne", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 749, - "name": "Bionic Commando", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 750, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 751, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 752, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 753, - "name": "Bastion", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 754, - "name": "Battlefield Hardline", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 755, - "name": "BioShock", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 756, - "name": "Ben and Ed: Blood Party", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 757, - "name": "Binary Domain", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Киберпанк" - ] - }, - { - "id": 758, - "name": "Battlefield V", - "site": "gamespot_com", - "genres": [ - "Shooter", - "Action", - "First-Person", - "Tactical" - ] - }, - { - "id": 759, - "name": "Bionic Commando", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 760, - "name": "BloodRayne 2", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 761, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG", - "Шутер", - "Аддон" - ] - }, - { - "id": 762, - "name": "Black Mesa", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 763, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 764, - "name": "Bionic Commando", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер" - ] - }, - { - "id": 765, - "name": "BioShock", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 766, - "name": "BioShock 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 767, - "name": "Battlefield V", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 768, - "name": "Black Mesa", - "site": "spong_com", - "genres": [] - }, - { - "id": 769, - "name": "Besiege", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 770, - "name": "BloodRayne: Betrayal", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 771, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Аддон" - ] - }, - { - "id": 772, - "name": "BioShock", - "site": "playground_ru", - "genres": [ - "Экшен", - "Стимпанк", - "Шутер" - ] - }, - { - "id": 773, - "name": "Angry Birds Seasons", - "site": "metacritic_com", - "genres": [ - "General", - "Action", - "Miscellaneous", - "Puzzle" - ] - }, - { - "id": 774, - "name": "BlackSite: Area 51", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 775, - "name": "Battlefield Vietnam", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 776, - "name": "Bionic Commando", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 777, - "name": "Black Mesa", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 778, - "name": "BioShock 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 779, - "name": "Borderlands: The Pre-Sequel!", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 780, - "name": "BlackSite: Area 51", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 781, - "name": "Batman: The Telltale Series", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Adventure" - ] - }, - { - "id": 782, - "name": "Bloodbath kavkaz", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 783, - "name": "Battlefield Vietnam", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 784, - "name": "BioShock Infinite", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 785, - "name": "Beyond Good and Evil", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 786, - "name": "BioShock 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Стимпанк", - "От первого лица", - "Шутер" - ] - }, - { - "id": 787, - "name": "Blades of Time", - "site": "gamer_info_com", - "genres": [ - "hack & slash", - "action" - ] - }, - { - "id": 788, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 789, - "name": "Black Mesa", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 790, - "name": "BlackSite: Area 51", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 791, - "name": "Blades of Time", - "site": "spong_com", - "genres": [ - "Beat 'Em Up: Hack and Slash" - ] - }, - { - "id": 792, - "name": "Bloodline: Линия крови", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 793, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 794, - "name": "Binary Domain", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 795, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Аддон" - ] - }, - { - "id": 796, - "name": "BioShock Infinite", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 797, - "name": "BioShock Infinite", - "site": "playground_ru", - "genres": [ - "Экшен", - "Стимпанк", - "От первого лица", - "Шутер" - ] - }, - { - "id": 798, - "name": "Blades of Time: Dismal Swamp", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 799, - "name": "Bayonetta", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 800, - "name": "BlackSite: Area 51", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 801, - "name": "Bayonetta", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 802, - "name": "Battlefield 1", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 803, - "name": "Blades of Time: Dismal Swamp", - "site": "spong_com", - "genres": [] - }, - { - "id": 804, - "name": "Blades of Time", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 805, - "name": "Blue Estate", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 806, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Аддон" - ] - }, - { - "id": 807, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 808, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 809, - "name": "BioShock", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 810, - "name": "Blood Omen 2: Legacy of Kain", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 811, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 812, - "name": "Angry Birds Space", - "site": "metacritic_com", - "genres": [ - "General", - "Action", - "Puzzle" - ] - }, - { - "id": 813, - "name": "Blades of Time: Dismal Swamp", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 814, - "name": "Blues and Bullets: Episode 1", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 815, - "name": "Blades of Time", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 816, - "name": "Blood Omen 2: Legacy of Kain", - "site": "spong_com", - "genres": [] - }, - { - "id": 817, - "name": "Brink", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 818, - "name": "Battlefield 1942", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 819, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 820, - "name": "BloodRayne", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 821, - "name": "Blood Omen 2: Legacy of Kain", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 822, - "name": "Bionic Commando", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 823, - "name": "BioShock 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 824, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 825, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 826, - "name": "Beholder", - "site": "store_steampowered_com", - "genres": [ - "Strategy", - "Indie", - "Adventure" - ] - }, - { - "id": 827, - "name": "Broforce", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 828, - "name": "Blades of Time: Dismal Swamp", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 829, - "name": "BloodRayne", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 830, - "name": "Beholder", - "site": "gamespot_com", - "genres": [ - "Strategy" - ] - }, - { - "id": 831, - "name": "Broken Age", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 832, - "name": "BloodRayne 2", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 833, - "name": "BloodRayne", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 834, - "name": "Bionic Commando", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 835, - "name": "Black Mesa", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 836, - "name": "Boogeyman", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 837, - "name": "BioShock Infinite", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 838, - "name": "Bionic Commando", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 839, - "name": "Beholder (Beta)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 840, - "name": "BloodRayne 2", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 841, - "name": "Beholder (Beta)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 842, - "name": "Blood Omen 2: Legacy of Kain", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 843, - "name": "Brothers: A Tale Of Two Sons", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 844, - "name": "Battlefield 4", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 845, - "name": "BloodRayne: Betrayal", - "site": "gamer_info_com", - "genres": [ - "beat 'em up" - ] - }, - { - "id": 846, - "name": "BloodRayne 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 847, - "name": "Borderlands", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Ролевая игра" - ] - }, - { - "id": 848, - "name": "BlackSite: Area 51", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 849, - "name": "BloodRayne: Betrayal", - "site": "spong_com", - "genres": [] - }, - { - "id": 850, - "name": "Black Mesa", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 851, - "name": "Black Mesa", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Инди" - ] - }, - { - "id": 852, - "name": "Angry Birds Star Wars", - "site": "metacritic_com", - "genres": [ - "General", - "Action", - "Strategy", - "Puzzle" - ] - }, - { - "id": 853, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 854, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 855, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 856, - "name": "Brutal Legend", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Стратегия" - ] - }, - { - "id": 857, - "name": "BloodRayne", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Third-Person", - "Action", - "Arcade", - "Shooter" - ] - }, - { - "id": 858, - "name": "Bloodbath kavkaz", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 859, - "name": "Borderlands 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 860, - "name": "BloodRayne: Betrayal", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 861, - "name": "Bloodbath kavkaz", - "site": "spong_com", - "genres": [] - }, - { - "id": 862, - "name": "Buff Knight Advanced", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 863, - "name": "BlackSite: Area 51", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 864, - "name": "Battlefield Hardline", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 865, - "name": "Blades of Time", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 866, - "name": "BlackSite: Area 51", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 867, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 868, - "name": "BloodRayne 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Third-Person", - "Action", - "Arcade", - "Shooter" - ] - }, - { - "id": 869, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 870, - "name": "Bloodline: Линия крови", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 871, - "name": "Bulb Boy", - "site": "gameguru_ru", - "genres": [ - "Point-and-click", - "Квест" - ] - }, - { - "id": 872, - "name": "Ben and Ed", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 873, - "name": "Bloodbath kavkaz", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 874, - "name": "Bloodline: Линия крови", - "site": "spong_com", - "genres": [] - }, - { - "id": 875, - "name": "Blades of Time: Dismal Swamp", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 876, - "name": "Blades of Time", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 877, - "name": "Ben and Ed", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 878, - "name": "Blades of Time", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Слэшер" - ] - }, - { - "id": 879, - "name": "Angry Birds Star Wars II", - "site": "metacritic_com", - "genres": [ - "General", - "Action", - "Strategy", - "Puzzle" - ] - }, - { - "id": 880, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 881, - "name": "Bulletstorm", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 882, - "name": "BloodRayne: Betrayal", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 883, - "name": "Bionic Commando", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 884, - "name": "Blue Estate", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 885, - "name": "Bloodline: Линия крови", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 886, - "name": "Battlefield V", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 887, - "name": "Blades of Time: Dismal Swamp", - "site": "playground_ru", - "genres": [] - }, - { - "id": 888, - "name": "Call of Cthulhu (2018)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 889, - "name": "Blue Estate", - "site": "spong_com", - "genres": [] - }, - { - "id": 890, - "name": "Blood Omen 2: Legacy of Kain", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 891, - "name": "Ben and Ed: Blood Party", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 892, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 893, - "name": "Blades of Time: Dismal Swamp", - "site": "ag_ru", - "genres": [] - }, - { - "id": 894, - "name": "Blues and Bullets: Episode 1", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 895, - "name": "Blue Estate", - "site": "gamebomb_ru", - "genres": [ - "Шутер" - ] - }, - { - "id": 896, - "name": "Bloodbath kavkaz", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 897, - "name": "Black Mesa", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 898, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Квест" - ] - }, - { - "id": 899, - "name": "Blues and Bullets: Episode 1", - "site": "spong_com", - "genres": [] - }, - { - "id": 900, - "name": "Battlefield Vietnam", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 901, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 902, - "name": "Blood Omen 2: Legacy of Kain", - "site": "playground_ru", - "genres": [] - }, - { - "id": 903, - "name": "BloodRayne", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 904, - "name": "Blues and Bullets: Episode 1", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 905, - "name": "Bloodline: Линия крови", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 906, - "name": "Ben and Ed: Blood Party", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 907, - "name": "Call of Duty", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 908, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 909, - "name": "Blood Omen 2: Legacy of Kain", - "site": "ag_ru", - "genres": [] - }, - { - "id": 910, - "name": "Armored Kitten", - "site": "metacritic_com", - "genres": [ - "Top-Down", - "Action", - "Shoot-'Em-Up", - "Shooter" - ] - }, - { - "id": 911, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 912, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 913, - "name": "Besiege", - "site": "store_steampowered_com", - "genres": [ - "Early Access", - "Simulation", - "Indie" - ] - }, - { - "id": 914, - "name": "BlackSite: Area 51", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 915, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 916, - "name": "BloodRayne", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 917, - "name": "Bayonetta", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 918, - "name": "BloodRayne 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 919, - "name": "Call of Duty 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 920, - "name": "Blue Estate", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Light Gun", - "Shooter" - ] - }, - { - "id": 921, - "name": "Boogeyman", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 922, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 923, - "name": "Boogeyman", - "site": "spong_com", - "genres": [] - }, - { - "id": 924, - "name": "Boogeyman", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 925, - "name": "BloodRayne", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 926, - "name": "Blades of Time", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 927, - "name": "Call of Duty 4: Modern Warfare", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 928, - "name": "BloodRayne 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица" - ] - }, - { - "id": 929, - "name": "Beholder", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics", - "Adventure" - ] - }, - { - "id": 930, - "name": "Blues and Bullets: Episode 1", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 931, - "name": "Borderlands", - "site": "gamer_info_com", - "genres": [ - "FPS", - "RPG" - ] - }, - { - "id": 932, - "name": "Besiege", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 933, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 934, - "name": "BloodRayne: Betrayal", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 935, - "name": "Beyond Good and Evil", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 936, - "name": "Borderlands", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 937, - "name": "Ashes", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 938, - "name": "Call of Duty: Advanced Warfare", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 939, - "name": "BloodRayne 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 940, - "name": "Borderlands", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 941, - "name": "Beholder (Beta)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 942, - "name": "BloodRayne: Betrayal", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "Слэшер" - ] - }, - { - "id": 943, - "name": "Blades of Time: Dismal Swamp", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 944, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 945, - "name": "Beyond Good and Evil", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 946, - "name": "Borderlands 2", - "site": "gamer_info_com", - "genres": [ - "FPS", - "RPG" - ] - }, - { - "id": 947, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 948, - "name": "Bloodbath kavkaz", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 949, - "name": "Call of Duty: Black Ops", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 950, - "name": "Borderlands 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 951, - "name": "Borderlands: The Pre-Sequel!", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 952, - "name": "BloodRayne: Betrayal", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Платформеры" - ] - }, - { - "id": 953, - "name": "Binary Domain", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 954, - "name": "Call of Duty: Black Ops II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 955, - "name": "Bloodbath kavkaz", - "site": "playground_ru", - "genres": [ - "Инди", - "Вид сверху", - "Мультиплеер", - "Адвенчура", - "Экшен" - ] - }, - { - "id": 956, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 957, - "name": "Boogeyman", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 958, - "name": "Blood Omen 2: Legacy of Kain", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 959, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 960, - "name": "Bloodline: Линия крови", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 961, - "name": "Binary Domain", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "Tactical", - "Shooter" - ] - }, - { - "id": 962, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 963, - "name": "Call of Duty: Black Ops III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 964, - "name": "Bloodline: Линия крови", - "site": "playground_ru", - "genres": [] - }, - { - "id": 965, - "name": "Borderlands 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 966, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 967, - "name": "Bloodbath kavkaz", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 968, - "name": "BloodRayne", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 969, - "name": "Borderlands", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 970, - "name": "Ben and Ed", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 971, - "name": "Call of Duty: Ghosts", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 972, - "name": "Blue Estate", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 973, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 974, - "name": "Blue Estate", - "site": "playground_ru", - "genres": [] - }, - { - "id": 975, - "name": "BioShock", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 976, - "name": "BioShock", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 977, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 978, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 979, - "name": "Call of Duty: Infinite Warfare", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 980, - "name": "Bloodline: Линия крови", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 981, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 982, - "name": "BloodRayne 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 983, - "name": "Blues and Bullets: Episode 1", - "site": "playground_ru", - "genres": [] - }, - { - "id": 984, - "name": "Blues and Bullets: Episode 1", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 985, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 986, - "name": "Borderlands 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 987, - "name": "Call of Duty: Modern Warfare 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 988, - "name": "Ben and Ed: Blood Party", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 989, - "name": "Blue Estate", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 990, - "name": "BioShock 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 991, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 992, - "name": "Brink", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица" - ] - }, - { - "id": 993, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 994, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 995, - "name": "BioShock 2", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 996, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 997, - "name": "BloodRayne: Betrayal", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 998, - "name": "Call of Duty: Modern Warfare 3", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 999, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1000, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1001, - "name": "Blues and Bullets: Episode 1", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1002, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1003, - "name": "Boogeyman", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1004, - "name": "Broforce", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 1005, - "name": "Besiege", - "site": "mobygames_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 1006, - "name": "Call of Duty: United Offensive", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 1007, - "name": "BioShock Infinite", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1008, - "name": "Boogeyman", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1009, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1010, - "name": "Bloodbath kavkaz", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1011, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1012, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1013, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1014, - "name": "Broken Age", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1015, - "name": "BioShock Infinite", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1016, - "name": "Call of Duty: WWII", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1017, - "name": "Beyond Good and Evil", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1018, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1019, - "name": "Borderlands", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1020, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1021, - "name": "Borderlands", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1022, - "name": "Call of Duty: World at War", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 1023, - "name": "Brothers: A Tale Of Two Sons", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1024, - "name": "Bloodline: Линия крови", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1025, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1026, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1027, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1028, - "name": "Boogeyman", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Стратегии" - ] - }, - { - "id": 1029, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1030, - "name": "Binary Domain", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1031, - "name": "Canabalt", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 1032, - "name": "Borderlands 2", - "site": "playground_ru", - "genres": [ - "Ролевая", - "Шутер", - "Экшен", - "Открытый мир", - "От первого лица" - ] - }, - { - "id": 1033, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1034, - "name": "Brutal Legend", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 1035, - "name": "Borderlands 2", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "РПГ" - ] - }, - { - "id": 1036, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1037, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1038, - "name": "Blue Estate", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 1039, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1040, - "name": "Castlevania: Lords of Shadow", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 1041, - "name": "Borderlands", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 1042, - "name": "BioShock", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1043, - "name": "Buff Knight Advanced", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 1044, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 1045, - "name": "Bionic Commando", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 1046, - "name": "Borderlands: The Pre-Sequel!", - "site": "gamer_info_com", - "genres": [ - "FPS", - "RPG" - ] - }, - { - "id": 1047, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 1048, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1049, - "name": "Blues and Bullets: Episode 1", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1050, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1051, - "name": "Borderlands 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 1052, - "name": "Borderlands: The Pre-Sequel!", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 1053, - "name": "Bulb Boy", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 1054, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1055, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1056, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1057, - "name": "Bionic Commando", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1058, - "name": "BioShock 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1059, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1060, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1061, - "name": "Black Mesa", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1062, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1063, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 1064, - "name": "Castlevania: Lords of Shadow 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 1065, - "name": "Bulletstorm", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1066, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1067, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1068, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1069, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1070, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1071, - "name": "Cat Mario", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1072, - "name": "BioShock Infinite", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1073, - "name": "Call of Cthulhu (2018)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1074, - "name": "Boogeyman", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1075, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1076, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1077, - "name": "Black Mesa", - "site": "store_steampowered_com", - "genres": [ - "Early Access", - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 1078, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1079, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1080, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1081, - "name": "BlackSite: Area 51", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1082, - "name": "Cat Quest", - "site": "gameguru_ru", - "genres": [ - "RPG", - "Инди" - ] - }, - { - "id": 1083, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 1084, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1085, - "name": "Borderlands", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1086, - "name": "Child Of Light", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 1087, - "name": "BlackSite: Area 51", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1088, - "name": "Brink", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 1089, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1090, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1091, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 1092, - "name": "Brink", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1093, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1094, - "name": "Call of Duty", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1095, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1096, - "name": "Children of Morta", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG", - "Инди" - ] - }, - { - "id": 1097, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1098, - "name": "Broforce", - "site": "gamer_info_com", - "genres": [ - "инди", - "платформеры" - ] - }, - { - "id": 1099, - "name": "Broforce", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1100, - "name": "Borderlands 2", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 1101, - "name": "Blades of Time", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 1102, - "name": "Call of Duty 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1103, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 1104, - "name": "Choice Chamber", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1105, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1106, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1107, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1108, - "name": "Blades of Time", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1109, - "name": "Borderlands: The Pre-Sequel!", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1110, - "name": "Broken Age", - "site": "gamer_info_com", - "genres": [ - "квесты" - ] - }, - { - "id": 1111, - "name": "Claire", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1112, - "name": "Broken Age", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 1113, - "name": "Blades of Time: Dismal Swamp", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1114, - "name": "Call of Duty 4: Modern Warfare", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1115, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 1116, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1117, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1118, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1119, - "name": "Blades of Time: Dismal Swamp", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1120, - "name": "Clustertruck", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Аркада", - "Платформер" - ] - }, - { - "id": 1121, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1122, - "name": "Brothers: A Tale Of Two Sons", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 1123, - "name": "Blood Omen 2: Legacy of Kain", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1124, - "name": "Call of Duty: Advanced Warfare", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1125, - "name": "Brothers: A Tale Of Two Sons", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл" - ] - }, - { - "id": 1126, - "name": "Bionic Commando", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1127, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 1128, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1129, - "name": "Cold Fear", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1130, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1131, - "name": "Brutal Legend", - "site": "gamer_info_com", - "genres": [ - "приключения", - "action" - ] - }, - { - "id": 1132, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1133, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1134, - "name": "Costume Quest", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 1135, - "name": "Call of Duty: Black Ops", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1136, - "name": "BloodRayne", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 1137, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1138, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры" - ] - }, - { - "id": 1139, - "name": "Blood Omen 2: Legacy of Kain", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1140, - "name": "Black Mesa", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1141, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1142, - "name": "Costume Quest 2", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 1143, - "name": "Buff Knight Advanced", - "site": "gamer_info_com", - "genres": [ - "RPG", - "runner", - "инди" - ] - }, - { - "id": 1144, - "name": "Borderlands: The Pre-Sequel!", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1145, - "name": "Call of Duty: Black Ops II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1146, - "name": "Borderlands: The Pre-Sequel!", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1147, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1148, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1149, - "name": "Brink", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1150, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1151, - "name": "BloodRayne", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1152, - "name": "BloodRayne 2", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 1153, - "name": "BlackSite: Area 51", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1154, - "name": "Brutal Legend", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Стратегия в реальном времени/РТС" - ] - }, - { - "id": 1155, - "name": "Bulb Boy", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы", - "квесты" - ] - }, - { - "id": 1156, - "name": "Call of Duty: Black Ops III", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1157, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1158, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1159, - "name": "Cross of the Dutchman", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1160, - "name": "Broforce", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 1161, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1162, - "name": "Borderlands: The Pre-Sequel!", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 1163, - "name": "Buff Knight Advanced", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 1164, - "name": "Bulletstorm", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1165, - "name": "BloodRayne 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1166, - "name": "Call of Duty: Ghosts", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1167, - "name": "Cry Of Fear", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1168, - "name": "BloodRayne: Betrayal", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1169, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1170, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1171, - "name": "Broken Age", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 1172, - "name": "Crysis", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 1173, - "name": "Blades of Time", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1174, - "name": "Call of Duty: Infinite Warfare", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1175, - "name": "Bulb Boy", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 1176, - "name": "Call of Cthulhu (2018)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1177, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1178, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1179, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1180, - "name": "Crysis 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 1181, - "name": "BloodRayne: Betrayal", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1182, - "name": "Call of Duty: Modern Warfare 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1183, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "gamer_info_com", - "genres": [ - "приключения", - "action" - ] - }, - { - "id": 1184, - "name": "Brothers: A Tale Of Two Sons", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Linear", - "Action Adventure" - ] - }, - { - "id": 1185, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1186, - "name": "Bulletstorm", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 1187, - "name": "Blades of Time: Dismal Swamp", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1188, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1189, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1190, - "name": "Crysis Warhead", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 1191, - "name": "Brink", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1192, - "name": "Call of Duty", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1193, - "name": "Call of Duty: Modern Warfare 3", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1194, - "name": "Call of Cthulhu (2018)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1195, - "name": "Bloodbath kavkaz", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1196, - "name": "Bloodbath kavkaz", - "site": "gamespot_com", - "genres": [ - "Shooter", - "Action", - "2D", - "Fixed-Screen" - ] - }, - { - "id": 1197, - "name": "Blood Omen 2: Legacy of Kain", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1198, - "name": "Brutal Legend", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Open-World", - "Action Adventure" - ] - }, - { - "id": 1199, - "name": "Cube Escape: Arles", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1200, - "name": "Brink", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1201, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1202, - "name": "Call of Duty: United Offensive", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1203, - "name": "Call of Duty 2", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1204, - "name": "Broforce", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1205, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1206, - "name": "Cube Escape: Case 23", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1207, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 1208, - "name": "Bloodline: Линия крови", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1209, - "name": "Bloodline: Линия крови", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1210, - "name": "Buff Knight Advanced", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1211, - "name": "BloodRayne", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1212, - "name": "Call of Duty: WWII", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1213, - "name": "Borderlands: The Pre-Sequel!", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1214, - "name": "Broforce", - "site": "playground_ru", - "genres": [ - "Инди", - "Аркада", - "Ретро", - "Шутер", - "Платформер" - ] - }, - { - "id": 1215, - "name": "Call of Duty 4: Modern Warfare", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1216, - "name": "Cube Escape: Harvey's Box", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1217, - "name": "Broken Age", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1218, - "name": "Brink", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1219, - "name": "Blue Estate", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1220, - "name": "Call of Duty", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1221, - "name": "Call of Duty: World at War", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1222, - "name": "Bulb Boy", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 1223, - "name": "Cube Escape: Seasons", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1224, - "name": "Blue Estate", - "site": "gamespot_com", - "genres": [ - "Action", - "Light-Gun", - "Shooter" - ] - }, - { - "id": 1225, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1226, - "name": "Call of Duty: Advanced Warfare", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1227, - "name": "Broken Age", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Инди", - "Квест" - ] - }, - { - "id": 1228, - "name": "Brothers: A Tale Of Two Sons", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1229, - "name": "BloodRayne 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1230, - "name": "Cube Escape: The Lake", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 1231, - "name": "Blues and Bullets: Episode 1", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1232, - "name": "Canabalt", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Боевик" - ] - }, - { - "id": 1233, - "name": "Bulletstorm", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1234, - "name": "Broforce", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Платформеры" - ] - }, - { - "id": 1235, - "name": "Blues and Bullets: Episode 1", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1236, - "name": "Call of Duty 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1237, - "name": "Call of Duty: Black Ops", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1238, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1239, - "name": "Brothers: A Tale Of Two Sons", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Инди" - ] - }, - { - "id": 1240, - "name": "Brutal Legend", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1241, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1242, - "name": "BloodRayne: Betrayal", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1243, - "name": "Call of Cthulhu (2018)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1244, - "name": "Castlevania: Lords of Shadow", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 1245, - "name": "Broken Age", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Семейные", - "Головоломки", - "Казуальные" - ] - }, - { - "id": 1246, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1247, - "name": "Call of Duty: Black Ops II", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1248, - "name": "Call of Duty 4: Modern Warfare", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1249, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1250, - "name": "Buff Knight Advanced", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1251, - "name": "Brutal Legend", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Ролевая", - "Слэшер" - ] - }, - { - "id": 1252, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1253, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 1254, - "name": "Call of Duty: Black Ops III", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1255, - "name": "Bloodbath kavkaz", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1256, - "name": "Brothers: A Tale Of Two Sons", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Головоломки", - "Приключения" - ] - }, - { - "id": 1257, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1258, - "name": "Buff Knight Advanced", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1259, - "name": "Brink", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1260, - "name": "Bulb Boy", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1261, - "name": "Boogeyman", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie" - ] - }, - { - "id": 1262, - "name": "Call of Duty", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1263, - "name": "Call of Duty: Ghosts", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1264, - "name": "Call of Duty: Advanced Warfare", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1265, - "name": "Bloodline: Линия крови", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1266, - "name": "Bulb Boy", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1267, - "name": "Castlevania: Lords of Shadow 2", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 1268, - "name": "Boogeyman", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 1269, - "name": "Brutal Legend", - "site": "ag_ru", - "genres": [ - "Экшены", - "Стратегии" - ] - }, - { - "id": 1270, - "name": "Broforce", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 1271, - "name": "Bulletstorm", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1272, - "name": "Borderlands", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1273, - "name": "Call of Duty: Infinite Warfare", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1274, - "name": "Cat Mario", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1275, - "name": "Call of Duty 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1276, - "name": "Call of Duty: Black Ops", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Битвы машин" - ] - }, - { - "id": 1277, - "name": "Blue Estate", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1278, - "name": "Bulletstorm", - "site": "playground_ru", - "genres": [ - "Экшен", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 1279, - "name": "Broken Age", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1280, - "name": "Call of Cthulhu (2018)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1281, - "name": "Call of Duty: Modern Warfare 2", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1282, - "name": "Cat Quest", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 1283, - "name": "Buff Knight Advanced", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Аркадные", - "Экшены", - "Казуальные" - ] - }, - { - "id": 1284, - "name": "Call of Duty: Black Ops II", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1285, - "name": "Call of Cthulhu (2018)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1286, - "name": "Blues and Bullets: Episode 1", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1287, - "name": "Call of Duty 4: Modern Warfare", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1288, - "name": "Borderlands 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1289, - "name": "Child Of Light", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер", - "Ролевая игра" - ] - }, - { - "id": 1290, - "name": "Call of Duty: Black Ops III", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1291, - "name": "Call of Duty: Modern Warfare 3", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1292, - "name": "Brothers: A Tale Of Two Sons", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 1293, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1294, - "name": "Bulb Boy", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Инди", - "Приключения" - ] - }, - { - "id": 1295, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1296, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От первого лица", - "Ужасы" - ] - }, - { - "id": 1297, - "name": "Borderlands", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1298, - "name": "Call of Duty: Advanced Warfare", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1299, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1300, - "name": "Children of Morta", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 1301, - "name": "Call of Duty: United Offensive", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1302, - "name": "Bulletstorm", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1303, - "name": "Call of Duty", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1304, - "name": "Brutal Legend", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1305, - "name": "Choice Chamber", - "site": "igromania_ru", - "genres": [ - "MMOG", - "Боевик" - ] - }, - { - "id": 1306, - "name": "Call of Duty", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1307, - "name": "Call of Duty: Black Ops", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1308, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1309, - "name": "Boogeyman", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 1310, - "name": "Call of Duty: WWII", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1311, - "name": "Call of Cthulhu (2018)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1312, - "name": "Buff Knight Advanced", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1313, - "name": "Borderlands 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1314, - "name": "Call of Duty 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1315, - "name": "Claire", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1316, - "name": "Call of Duty 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1317, - "name": "Call of Duty: World at War", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1318, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1319, - "name": "Call of Duty: Black Ops II", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1320, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1321, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "ag_ru", - "genres": [ - "Шутеры" - ] - }, - { - "id": 1322, - "name": "Borderlands", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 1323, - "name": "Clustertruck", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1324, - "name": "Bulb Boy", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1325, - "name": "Call of Duty 4: Modern Warfare", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1326, - "name": "Canabalt", - "site": "gamer_info_com", - "genres": [ - "инди", - "runner", - "аркадные" - ] - }, - { - "id": 1327, - "name": "Call of Duty: Ghosts", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1328, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1329, - "name": "Call of Duty 4: Modern Warfare", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1330, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1331, - "name": "Cold Fear", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1332, - "name": "Call of Duty", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены", - "Симуляторы" - ] - }, - { - "id": 1333, - "name": "Call of Duty: Black Ops III", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1334, - "name": "Bulletstorm", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1335, - "name": "Borderlands 2", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving", - "Role-Playing (RPG)" - ] - }, - { - "id": 1336, - "name": "Call of Duty: Advanced Warfare", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1337, - "name": "Castlevania: Lords of Shadow", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1338, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1339, - "name": "Call of Duty: Infinite Warfare", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1340, - "name": "Costume Quest", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 1341, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1342, - "name": "Call of Duty: Advanced Warfare", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Кооператив", - "Научная фантастика", - "Шутер", - "Экшен", - "От первого лица" - ] - }, - { - "id": 1343, - "name": "Call of Duty: Ghosts", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1344, - "name": "Call of Cthulhu (2018)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1345, - "name": "Call of Duty 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1346, - "name": "Call of Duty: Black Ops", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1347, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1348, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1349, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1350, - "name": "Costume Quest 2", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 1351, - "name": "Call of Duty: Modern Warfare 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 1352, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1353, - "name": "Call of Duty: Black Ops", - "site": "playground_ru", - "genres": [ - "Экшен", - "Зомби", - "Шутер" - ] - }, - { - "id": 1354, - "name": "Call of Duty: Infinite Warfare", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 1355, - "name": "Call of Duty 4: Modern Warfare", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1356, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1357, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "stopgame_ru", - "genres": [ - "adventure", - "action" - ] - }, - { - "id": 1358, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1359, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1360, - "name": "Call of Duty: Black Ops II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1361, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1362, - "name": "Call of Duty: Black Ops II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1363, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1364, - "name": "Call of Duty: Advanced Warfare", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1365, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1366, - "name": "Cross of the Dutchman", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1367, - "name": "Castlevania: Lords of Shadow 2", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1368, - "name": "Call of Duty", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1369, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1370, - "name": "Call of Duty: Modern Warfare 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1371, - "name": "Call of Duty: Black Ops III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1372, - "name": "Call of Duty: Black Ops III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1373, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1374, - "name": "Cry Of Fear", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1375, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1376, - "name": "Cat Mario", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1377, - "name": "Call of Duty: Black Ops", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1378, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1379, - "name": "Call of Duty: Ghosts", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1380, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1381, - "name": "Call of Duty: Modern Warfare 3", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1382, - "name": "Call of Duty: Ghosts", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1383, - "name": "Call of Duty 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1384, - "name": "Crysis", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1385, - "name": "Cat Quest", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди" - ] - }, - { - "id": 1386, - "name": "Call of Duty: Black Ops II", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1387, - "name": "Call of Duty: Infinite Warfare", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1388, - "name": "Call of Duty: Modern Warfare 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 1389, - "name": "Borderlands: The Pre-Sequel!", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 1390, - "name": "Call of Duty 4: Modern Warfare", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1391, - "name": "Call of Duty: United Offensive", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1392, - "name": "Crysis 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1393, - "name": "Call of Duty: Infinite Warfare", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 1394, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1395, - "name": "Child Of Light", - "site": "gamer_info_com", - "genres": [ - "jRPG" - ] - }, - { - "id": 1396, - "name": "Borderlands: The Pre-Sequel!", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1397, - "name": "Call of Duty: Black Ops III", - "site": "ag_ru", - "genres": [ - "Шутеры", - "ММО", - "Экшены" - ] - }, - { - "id": 1398, - "name": "Call of Duty: United Offensive", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1399, - "name": "Crysis Warhead", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1400, - "name": "Call of Duty: Modern Warfare 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1401, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1402, - "name": "Call of Duty: WWII", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1403, - "name": "Call of Duty: Advanced Warfare", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1404, - "name": "Call of Duty: Modern Warfare 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1405, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1406, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1407, - "name": "Children of Morta", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди", - "action" - ] - }, - { - "id": 1408, - "name": "Call of Duty: Ghosts", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1409, - "name": "Cube Escape: Arles", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 1410, - "name": "Call of Duty: WWII", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Многопользоватеский шутер" - ] - }, - { - "id": 1411, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1412, - "name": "Call of Duty: Modern Warfare 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1413, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1414, - "name": "Call of Duty: Black Ops", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1415, - "name": "Call of Duty: World at War", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1416, - "name": "Call of Duty: Modern Warfare 3", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1417, - "name": "Choice Chamber", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1418, - "name": "Cube Escape: Case 23", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 1419, - "name": "Call of Duty: Infinite Warfare", - "site": "ag_ru", - "genres": [ - "Шутеры" - ] - }, - { - "id": 1420, - "name": "Call of Duty: World at War", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1421, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1422, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1423, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1424, - "name": "Call of Duty: United Offensive", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1425, - "name": "Call of Duty: United Offensive", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1426, - "name": "Canabalt", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1427, - "name": "Call of Duty: Black Ops II", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1428, - "name": "Cube Escape: Harvey's Box", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 1429, - "name": "Claire", - "site": "gamer_info_com", - "genres": [ - "инди", - "приключения" - ] - }, - { - "id": 1430, - "name": "Call of Duty: Modern Warfare 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1431, - "name": "Canabalt", - "site": "gamebomb_ru", - "genres": [ - "Платформер" - ] - }, - { - "id": 1432, - "name": "Call of Duty: WWII", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1433, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1434, - "name": "Cube Escape: Seasons", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 1435, - "name": "Clustertruck", - "site": "gamer_info_com", - "genres": [ - "runner" - ] - }, - { - "id": 1436, - "name": "Castlevania: Lords of Shadow", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 1437, - "name": "Call of Duty: WWII", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1438, - "name": "Call of Duty: Black Ops III", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1439, - "name": "Call of Duty: Modern Warfare 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1440, - "name": "Castlevania: Lords of Shadow", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 1441, - "name": "Brink", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1442, - "name": "Brink", - "site": "store_steampowered_com", - "genres": [ - "Free to Play", - "Action" - ] - }, - { - "id": 1443, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1444, - "name": "Call of Duty: World at War", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1445, - "name": "Cold Fear", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 1446, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1447, - "name": "Borderlands: The Pre-Sequel!", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 1448, - "name": "Call of Duty: World at War", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1449, - "name": "Cube Escape: The Lake", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 1450, - "name": "Call of Duty: United Offensive", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1451, - "name": "Call of Duty: Ghosts", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1452, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1453, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1454, - "name": "Costume Quest", - "site": "gamer_info_com", - "genres": [ - "RPG", - "приключения" - ] - }, - { - "id": 1455, - "name": "Canabalt", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1456, - "name": "Canabalt", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1457, - "name": "Cube Escape: The Mill", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 1458, - "name": "Broforce", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Casual", - "Adventure" - ] - }, - { - "id": 1459, - "name": "Call of Duty: WWII", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1460, - "name": "Broforce", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 1461, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1462, - "name": "Call of Duty: Infinite Warfare", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1463, - "name": "DOOM (2016)", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1464, - "name": "Costume Quest 2", - "site": "gamer_info_com", - "genres": [ - "RPG", - "приключения" - ] - }, - { - "id": 1465, - "name": "Castlevania: Lords of Shadow 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 1466, - "name": "Castlevania: Lords of Shadow", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1467, - "name": "Call of Duty: World at War", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1468, - "name": "Castlevania: Lords of Shadow 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 1469, - "name": "Castlevania: Lords of Shadow", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Слэшер" - ] - }, - { - "id": 1470, - "name": "Call of Duty: Modern Warfare 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1471, - "name": "Dark Deception", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1472, - "name": "Broken Age", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 1473, - "name": "Cat Mario", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1474, - "name": "Cat Mario", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1475, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1476, - "name": "Broken Age", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 1477, - "name": "Canabalt", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Инди", - "Аркадные" - ] - }, - { - "id": 1478, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1479, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1480, - "name": "Dark Messiah of Might and Magic", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1481, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1482, - "name": "Call of Duty: Modern Warfare 3", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1483, - "name": "Cat Quest", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 1484, - "name": "Cross of the Dutchman", - "site": "gamer_info_com", - "genres": [ - "инди", - "action" - ] - }, - { - "id": 1485, - "name": "Cat Quest", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 1486, - "name": "Castlevania: Lords of Shadow", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 1487, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1488, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1489, - "name": "Brothers: A Tale Of Two Sons", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1490, - "name": "Dark Sector", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1491, - "name": "Brothers: A Tale Of Two Sons", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 1492, - "name": "Cry Of Fear", - "site": "gamer_info_com", - "genres": [ - "инди", - "FPS", - "ужасы", - "action" - ] - }, - { - "id": 1493, - "name": "Child Of Light", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Role-Playing" - ] - }, - { - "id": 1494, - "name": "Child Of Light", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 1495, - "name": "Call of Duty: United Offensive", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 1496, - "name": "Castlevania: Lords of Shadow 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Слэшер" - ] - }, - { - "id": 1497, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1498, - "name": "Castlevania: Lords of Shadow 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1499, - "name": "Dark Souls II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1500, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1501, - "name": "Brutal Legend", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1502, - "name": "Crysis", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1503, - "name": "Children of Morta", - "site": "gamefaqs_gamespot_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 1504, - "name": "Children of Morta", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 1505, - "name": "Cat Mario", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1506, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1507, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 1508, - "name": "Call of Duty: WWII", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1509, - "name": "Brutal Legend", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Adventure" - ] - }, - { - "id": 1510, - "name": "Cat Mario", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1511, - "name": "Choice Chamber", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1512, - "name": "Crysis 2", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1513, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1514, - "name": "Brink", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1515, - "name": "Choice Chamber", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1516, - "name": "Cat Quest", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая" - ] - }, - { - "id": 1517, - "name": "Castlevania: Lords of Shadow 2", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 1518, - "name": "Cat Quest", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1519, - "name": "Call of Duty: World at War", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1520, - "name": "Claire", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 1521, - "name": "Crysis Warhead", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 1522, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1523, - "name": "Buff Knight Advanced", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 1524, - "name": "Claire", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1525, - "name": "Buff Knight Advanced", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Casual" - ] - }, - { - "id": 1526, - "name": "Child Of Light", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая", - "Платформер" - ] - }, - { - "id": 1527, - "name": "Canabalt", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1528, - "name": "Child Of Light", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1529, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1530, - "name": "Cat Mario", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1531, - "name": "Cube Escape: Arles", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1532, - "name": "Clustertruck", - "site": "gamebomb_ru", - "genres": [ - "Платформер" - ] - }, - { - "id": 1533, - "name": "Bulb Boy", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 1534, - "name": "Clustertruck", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 1535, - "name": "Dark Souls III", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1536, - "name": "Children of Morta", - "site": "playground_ru", - "genres": [ - "Инди", - "Адвенчура", - "Кооператив", - "Ретро", - "Ролевая", - "Экшен" - ] - }, - { - "id": 1537, - "name": "Broforce", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1538, - "name": "Bulb Boy", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 1539, - "name": "Children of Morta", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1540, - "name": "Castlevania: Lords of Shadow", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1541, - "name": "Cube Escape: Case 23", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1542, - "name": "Cat Quest", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 1543, - "name": "Cold Fear", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1544, - "name": "Cold Fear", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 1545, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1546, - "name": "Choice Chamber", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1547, - "name": "Bulletstorm", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1548, - "name": "Choice Chamber", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1549, - "name": "Cube Escape: Harvey's Box", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1550, - "name": "Broken Age", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 1551, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1552, - "name": "Child Of Light", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди" - ] - }, - { - "id": 1553, - "name": "Bulletstorm", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1554, - "name": "Costume Quest", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры" - ] - }, - { - "id": 1555, - "name": "Costume Quest", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 1556, - "name": "Claire", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1557, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1558, - "name": "Cube Escape: Seasons", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1559, - "name": "Call of Cthulhu (2018)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1560, - "name": "Call of Cthulhu (2018)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1561, - "name": "Claire", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1562, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1563, - "name": "Children of Morta", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 1564, - "name": "Costume Quest 2", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 1565, - "name": "Dark Souls: Prepare to Die Edition", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1566, - "name": "Clustertruck", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "От первого лица" - ] - }, - { - "id": 1567, - "name": "Brothers: A Tale Of Two Sons", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1568, - "name": "Costume Quest 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Japanese-Style", - "Role-Playing" - ] - }, - { - "id": 1569, - "name": "Cube Escape: The Lake", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1570, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1571, - "name": "Clustertruck", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1572, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1573, - "name": "Choice Chamber", - "site": "ag_ru", - "genres": [ - "ММО", - "Инди", - "Экшены" - ] - }, - { - "id": 1574, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 1575, - "name": "Castlevania: Lords of Shadow 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1576, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1577, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1578, - "name": "Cold Fear", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 1579, - "name": "Cross of the Dutchman", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1580, - "name": "Cube Escape: The Mill", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1581, - "name": "Dark Souls: Remastered", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1582, - "name": "Cold Fear", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1583, - "name": "Claire", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 1584, - "name": "Cat Mario", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1585, - "name": "Cry Of Fear", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1586, - "name": "Brutal Legend", - "site": "mobygames_com", - "genres": [ - "Action", - "Strategy / tactics" - ] - }, - { - "id": 1587, - "name": "Cross of the Dutchman", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1588, - "name": "Costume Quest", - "site": "playground_ru", - "genres": [ - "Юмор", - "Адвенчура", - "Ролевая", - "Фэнтези" - ] - }, - { - "id": 1589, - "name": "DOOM (2016)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1590, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1591, - "name": "Call of Duty", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1592, - "name": "Costume Quest", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1593, - "name": "Call of Duty", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1594, - "name": "Clustertruck", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Платформеры", - "Аркадные" - ] - }, - { - "id": 1595, - "name": "Crysis", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 1596, - "name": "Cat Quest", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1597, - "name": "Cry Of Fear", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1598, - "name": "Darksiders", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 1599, - "name": "Buff Knight Advanced", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 1600, - "name": "Costume Quest 2", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая" - ] - }, - { - "id": 1601, - "name": "Dark Deception", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1602, - "name": "Costume Quest 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1603, - "name": "Cold Fear", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 1604, - "name": "Darksiders II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1605, - "name": "Child Of Light", - "site": "stopgame_ru", - "genres": [ - "arcade", - "rpg" - ] - }, - { - "id": 1606, - "name": "Crysis 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1607, - "name": "Crysis", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1608, - "name": "Call of Duty 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1609, - "name": "Dark Messiah of Might and Magic", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 1610, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1611, - "name": "Call of Duty 2", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1612, - "name": "Bulb Boy", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 1613, - "name": "Costume Quest", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Приключения", - "Семейные", - "Экшены", - "Казуальные" - ] - }, - { - "id": 1614, - "name": "Darksiders III", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1615, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1616, - "name": "Cross of the Dutchman", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1617, - "name": "Crysis Warhead", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1618, - "name": "Dark Sector", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "action" - ] - }, - { - "id": 1619, - "name": "Children of Morta", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1620, - "name": "Crysis 2", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1621, - "name": "Call of Duty 4: Modern Warfare", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1622, - "name": "Daylight", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1623, - "name": "Cube Escape: Arles", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1624, - "name": "Costume Quest 2", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Ролевые", - "Приключения" - ] - }, - { - "id": 1625, - "name": "Cross of the Dutchman", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1626, - "name": "Cry Of Fear", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1627, - "name": "Bulletstorm", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1628, - "name": "Dark Souls II", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 1629, - "name": "Call of Duty 4: Modern Warfare", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1630, - "name": "Crysis Warhead", - "site": "gamefaqs_gamespot_com", - "genres": [ - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 1631, - "name": "Choice Chamber", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 1632, - "name": "Cube Escape: Case 23", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1633, - "name": "Dead Island", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1634, - "name": "Call of Duty: Advanced Warfare", - "site": "gamespot_com", - "genres": [ - "Tactical", - "3D", - "Action", - "First-Person", - "Shooter" - ] - }, - { - "id": 1635, - "name": "Cry Of Fear", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1636, - "name": "Call of Cthulhu (2018)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1637, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1638, - "name": "Crysis", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 1639, - "name": "Cube Escape: Arles", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1640, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 1641, - "name": "Cube Escape: Harvey's Box", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1642, - "name": "Call of Duty: Advanced Warfare", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1643, - "name": "Claire", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1644, - "name": "Dead Island: Riptide", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1645, - "name": "Cube Escape: Seasons", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1646, - "name": "Cube Escape: Case 23", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1647, - "name": "Crysis", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1648, - "name": "Crysis 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 1649, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1650, - "name": "Call of Duty: Black Ops", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1651, - "name": "Dead Island: Ryder White", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1652, - "name": "Cross of the Dutchman", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 1653, - "name": "Clustertruck", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1654, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1655, - "name": "Cube Escape: The Lake", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1656, - "name": "Cube Escape: Harvey's Box", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1657, - "name": "Crysis 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1658, - "name": "Crysis Warhead", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 1659, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1660, - "name": "Call of Duty: Black Ops", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1661, - "name": "Cube Escape: The Mill", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1662, - "name": "Dead Rising", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1663, - "name": "Cry Of Fear", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 1664, - "name": "Cube Escape: Seasons", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1665, - "name": "Cold Fear", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1666, - "name": "Call of Duty: Black Ops II", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1667, - "name": "Cube Escape: Arles", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1668, - "name": "DOOM (2016)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1669, - "name": "Crysis Warhead", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1670, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1671, - "name": "Call of Duty", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1672, - "name": "Cube Escape: The Lake", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1673, - "name": "Dead Space", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1674, - "name": "Crysis", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1675, - "name": "Costume Quest", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1676, - "name": "Call of Duty: Black Ops III", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1677, - "name": "Dark Deception", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1678, - "name": "Cube Escape: Case 23", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1679, - "name": "Cube Escape: The Mill", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1680, - "name": "Call of Duty: Black Ops II", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1681, - "name": "Dark Souls III", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 1682, - "name": "Dead Space 2", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1683, - "name": "Cube Escape: Arles", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1684, - "name": "Dark Messiah of Might and Magic", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1685, - "name": "Crysis 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1686, - "name": "Call of Duty 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1687, - "name": "Costume Quest 2", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1688, - "name": "DOOM (2016)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1689, - "name": "Cube Escape: Harvey's Box", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1690, - "name": "Dead Space 3", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1691, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1692, - "name": "Call of Duty: Ghosts", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1693, - "name": "Cube Escape: Case 23", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1694, - "name": "Dark Deception", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1695, - "name": "Cube Escape: Seasons", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1696, - "name": "Dark Sector", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1697, - "name": "Dead Space 3: Awakened (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1698, - "name": "Crysis Warhead", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1699, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1700, - "name": "Call of Duty: Black Ops III", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1701, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1702, - "name": "Call of Duty 4: Modern Warfare", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1703, - "name": "Dark Messiah of Might and Magic", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1704, - "name": "Dark Souls II", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1705, - "name": "Cube Escape: Harvey's Box", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1706, - "name": "Cube Escape: The Lake", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1707, - "name": "Call of Duty: Infinite Warfare", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 1708, - "name": "Dead by Daylight", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 1709, - "name": "Cube Escape: Arles", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 1710, - "name": "Dark Sector", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1711, - "name": "Dark Souls: Prepare to Die Edition", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1712, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1713, - "name": "Cross of the Dutchman", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1714, - "name": "Cube Escape: The Mill", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1715, - "name": "Cube Escape: Seasons", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1716, - "name": "Call of Duty: Advanced Warfare", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1717, - "name": "Deadlight", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 1718, - "name": "Dark Souls II", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1719, - "name": "Cube Escape: Case 23", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 1720, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1721, - "name": "Call of Duty: Modern Warfare 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1722, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1723, - "name": "Call of Duty: Ghosts", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1724, - "name": "Cry Of Fear", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 1725, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1726, - "name": "Cube Escape: The Lake", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1727, - "name": "Deadpool", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1728, - "name": "DOOM (2016)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 1729, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1730, - "name": "Cube Escape: Harvey's Box", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 1731, - "name": "Dark Souls: Remastered", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1732, - "name": "Call of Duty: Black Ops", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1733, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1734, - "name": "Crysis", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1735, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1736, - "name": "Call of Duty: Modern Warfare 3", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1737, - "name": "DeathSpank", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Ролевая игра" - ] - }, - { - "id": 1738, - "name": "Dark Deception", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1739, - "name": "Cube Escape: The Mill", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1740, - "name": "Cube Escape: Seasons", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 1741, - "name": "Call of Duty: Infinite Warfare", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1742, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1743, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1744, - "name": "Dark Souls III", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1745, - "name": "Call of Duty: United Offensive", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1746, - "name": "Dark Messiah of Might and Magic", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1747, - "name": "Crysis 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1748, - "name": "DeathSpank: Thongs of Virtue", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 1749, - "name": "DOOM (2016)", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Экшен" - ] - }, - { - "id": 1750, - "name": "Cube Escape: The Lake", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 1751, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1752, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1753, - "name": "Darksiders", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1754, - "name": "Call of Duty: Black Ops II", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1755, - "name": "Deathtrap", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 1756, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1757, - "name": "Crysis Warhead", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1758, - "name": "Dark Sector", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Шутер", - "Киберпанк" - ] - }, - { - "id": 1759, - "name": "Dark Souls III", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1760, - "name": "Call of Duty: WWII", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1761, - "name": "Call of Duty: Modern Warfare 2", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1762, - "name": "Cube Escape: The Mill", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 1763, - "name": "Dark Deception", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1764, - "name": "Darksiders II", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1765, - "name": "Dark Souls: Prepare to Die Edition", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1766, - "name": "Demigod", - "site": "igromania_ru", - "genres": [ - "Стратегия в реальном времени", - "Стратегия", - "Ролевая игра" - ] - }, - { - "id": 1767, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1768, - "name": "Dark Souls II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1769, - "name": "Cube Escape: Arles", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1770, - "name": "Call of Duty: Black Ops III", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1771, - "name": "Dark Messiah of Might and Magic", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1772, - "name": "Call of Duty: World at War", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 1773, - "name": "DOOM (2016)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1774, - "name": "Darksiders III", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1775, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1776, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1777, - "name": "Descenders", - "site": "igromania_ru", - "genres": [ - "Мототехника", - "Симулятор" - ] - }, - { - "id": 1778, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1779, - "name": "Call of Duty: Modern Warfare 3", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1780, - "name": "Cube Escape: Case 23", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1781, - "name": "Dark Sector", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1782, - "name": "Daylight", - "site": "gamer_info_com", - "genres": [ - "ужасы" - ] - }, - { - "id": 1783, - "name": "Dark Souls: Prepare to Die Edition", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1784, - "name": "Dark Souls: Remastered", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 1785, - "name": "Detention", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1786, - "name": "Call of Duty: Ghosts", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1787, - "name": "Dark Deception", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 1788, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1789, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1790, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1791, - "name": "Canabalt", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 1792, - "name": "Cube Escape: Harvey's Box", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1793, - "name": "Dead Island", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1794, - "name": "Dark Souls II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1795, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1796, - "name": "Dark Messiah of Might and Magic", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1797, - "name": "Call of Duty: United Offensive", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1798, - "name": "Dark Souls: Remastered", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1799, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1800, - "name": "Darksiders", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 1801, - "name": "Call of Duty: Infinite Warfare", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1802, - "name": "Cube Escape: Seasons", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1803, - "name": "Dead Island: Riptide", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1804, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1805, - "name": "Dark Sector", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 1806, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1807, - "name": "Devil May Cry 4", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1808, - "name": "Darksiders II", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1809, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1810, - "name": "Dead Island: Ryder White", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1811, - "name": "Cube Escape: The Lake", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1812, - "name": "Castlevania: Lords of Shadow", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1813, - "name": "Darksiders", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1814, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1815, - "name": "Call of Duty: WWII", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1816, - "name": "Devil May Cry 5", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 1817, - "name": "Darksiders III", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1818, - "name": "Call of Duty: Modern Warfare 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1819, - "name": "Dark Souls III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1820, - "name": "Dark Souls II", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 1821, - "name": "Darksiders II", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1822, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1823, - "name": "Dead Rising", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1824, - "name": "Diablo II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1825, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1826, - "name": "Cube Escape: The Mill", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1827, - "name": "Call of Duty: Modern Warfare 3", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1828, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1829, - "name": "Daylight", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 1830, - "name": "Darksiders III", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1831, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1832, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 1833, - "name": "Call of Duty: World at War", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 1834, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1835, - "name": "Dead Space", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 1836, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1837, - "name": "DOOM (2016)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1838, - "name": "Daylight", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1839, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1840, - "name": "Diablo III", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1841, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1842, - "name": "Call of Duty: United Offensive", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 1843, - "name": "Castlevania: Lords of Shadow 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1844, - "name": "Dead Space 2", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 1845, - "name": "Dead Island", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 1846, - "name": "Dead Island", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1847, - "name": "Dark Souls III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1848, - "name": "Dark Deception", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1849, - "name": "Dark Souls: Prepare to Die Edition", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1850, - "name": "Canabalt", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 1851, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1852, - "name": "Cat Mario", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1853, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1854, - "name": "Dead Island: Riptide", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1855, - "name": "Dead Space 3", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 1856, - "name": "Dig or Die", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1857, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1858, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1859, - "name": "Castlevania: Lords of Shadow", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1860, - "name": "Dark Messiah of Might and Magic", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1861, - "name": "Call of Duty: WWII", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1862, - "name": "Dead Island: Ryder White", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1863, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1864, - "name": "Cat Quest", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 1865, - "name": "Dead Space 3: Awakened (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1866, - "name": "Disciples II: Dark Prophecy", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1867, - "name": "Dark Souls: Remastered", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1868, - "name": "Dead Island: Riptide", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1869, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1870, - "name": "Dead Rising", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1871, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1872, - "name": "Dark Sector", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 1873, - "name": "Disciples III: Renaissance", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1874, - "name": "Dark Souls III", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 1875, - "name": "Dead by Daylight", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 1876, - "name": "Dead Island: Ryder White", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1877, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1878, - "name": "Child Of Light", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 1879, - "name": "Dead Space", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1880, - "name": "Dark Souls: Prepare to Die Edition", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1881, - "name": "Call of Duty: World at War", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1882, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1883, - "name": "Dishonored", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1884, - "name": "Dark Souls II", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1885, - "name": "Deadlight", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 1886, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 1887, - "name": "Dead Space 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1888, - "name": "Dead Rising", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 1889, - "name": "Darksiders", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Слэшер", - "Фэнтези" - ] - }, - { - "id": 1890, - "name": "Children of Morta", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 1891, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1892, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1893, - "name": "Dead Space 3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1894, - "name": "Deadpool", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1895, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 1896, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1897, - "name": "Canabalt", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1898, - "name": "Darksiders II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1899, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1900, - "name": "Dead Space", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер" - ] - }, - { - "id": 1901, - "name": "Choice Chamber", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 1902, - "name": "Dead Space 3: Awakened (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1903, - "name": "Castlevania: Lords of Shadow 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1904, - "name": "Dark Souls: Remastered", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "РПГ", - "Экшен" - ] - }, - { - "id": 1905, - "name": "DeathSpank", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 1906, - "name": "Darksiders III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1907, - "name": "Dark Souls: Prepare to Die Edition", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 1908, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1909, - "name": "Castlevania: Lords of Shadow", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1910, - "name": "Dead by Daylight", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1911, - "name": "Distraint", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1912, - "name": "Cat Mario", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 1913, - "name": "Dead Space 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер" - ] - }, - { - "id": 1914, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1915, - "name": "DeathSpank: Thongs of Virtue", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 1916, - "name": "Deadlight", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1917, - "name": "Disturbed", - "site": "igromania_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 1918, - "name": "Daylight", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 1919, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1920, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1921, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1922, - "name": "Claire", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 1923, - "name": "Deathtrap", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1924, - "name": "Darksiders", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1925, - "name": "Deadpool", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1926, - "name": "DmC: Devil May Cry", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 1927, - "name": "Dark Souls: Remastered", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 1928, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1929, - "name": "Dead Island", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "От первого лица", - "Ужасы" - ] - }, - { - "id": 1930, - "name": "Cat Quest", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 1931, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1932, - "name": "Demigod", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1933, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1934, - "name": "DeathSpank", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1935, - "name": "Darksiders II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1936, - "name": "Clustertruck", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 1937, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 1938, - "name": "Dark Souls III", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1939, - "name": "Dead Space 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 1940, - "name": "Dead Island: Riptide", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 1941, - "name": "DeathSpank: Thongs of Virtue", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1942, - "name": "Doki Doki Literature Club", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1943, - "name": "Descenders", - "site": "gamer_info_com", - "genres": [ - "симуляторы", - "спорт" - ] - }, - { - "id": 1944, - "name": "Castlevania: Lords of Shadow 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 1945, - "name": "Darksiders III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1946, - "name": "Child Of Light", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 1947, - "name": "Dead Space 3: Awakened (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 1948, - "name": "Deathtrap", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1949, - "name": "Dead Island: Ryder White", - "site": "playground_ru", - "genres": [] - }, - { - "id": 1950, - "name": "Don't Knock Twice", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 1951, - "name": "Cold Fear", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 1952, - "name": "Detention", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 1953, - "name": "Cat Mario", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 1954, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1955, - "name": "Darksiders", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 1956, - "name": "Daylight", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1957, - "name": "Demigod", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1958, - "name": "Dead by Daylight", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 1959, - "name": "Doom 3", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 1960, - "name": "Dead Rising", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Ужасы", - "Адвенчура", - "Песочница", - "Выживание", - "Зомби", - "Экшен", - "Открытый мир" - ] - }, - { - "id": 1961, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1962, - "name": "Children of Morta", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 1963, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1964, - "name": "Descenders", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1965, - "name": "Darksiders II", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 1966, - "name": "Costume Quest", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 1967, - "name": "Dead Island", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1968, - "name": "Double Dragon Neon", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1969, - "name": "Cat Quest", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 1970, - "name": "Deadlight", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 1971, - "name": "Detention", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1972, - "name": "Devil May Cry 4", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1973, - "name": "Dead Space", - "site": "playground_ru", - "genres": [ - "Экшен", - "Космос", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 1974, - "name": "Dark Souls: Prepare to Die Edition", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1975, - "name": "Darksiders III", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 1976, - "name": "Dead Island: Riptide", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1977, - "name": "Costume Quest 2", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 1978, - "name": "Down Stairs", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1979, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1980, - "name": "Choice Chamber", - "site": "store_steampowered_com", - "genres": [ - "Massively Multiplayer", - "Action", - "Indie" - ] - }, - { - "id": 1981, - "name": "Devil May Cry 5", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 1982, - "name": "Dead Space 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Космос", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 1983, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 1984, - "name": "Devil May Cry 4", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1985, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 1986, - "name": "Downfall", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 1987, - "name": "Dead Island: Ryder White", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1988, - "name": "Child Of Light", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 1989, - "name": "Daylight", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Инди", - "Приключения" - ] - }, - { - "id": 1990, - "name": "Diablo II", - "site": "gamer_info_com", - "genres": [ - "RPG", - "hack & slash" - ] - }, - { - "id": 1991, - "name": "Devil May Cry 5", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 1992, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 1993, - "name": "Dead Space 3", - "site": "playground_ru", - "genres": [ - "Космос", - "От третьего лица", - "Ужасы", - "Кооператив", - "Экшен" - ] - }, - { - "id": 1994, - "name": "Dead Island", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 1995, - "name": "Cross of the Dutchman", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 1996, - "name": "Dark Souls: Remastered", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 1997, - "name": "Dead Rising", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 1998, - "name": "Claire", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 1999, - "name": "Children of Morta", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2000, - "name": "Deadpool", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 2001, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2002, - "name": "Diablo II", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2003, - "name": "Dragon Age II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2004, - "name": "Dead Space", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2005, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2006, - "name": "Cry Of Fear", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 2007, - "name": "Dead Space 3: Awakened (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 2008, - "name": "Dead Island: Riptide", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 2009, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2010, - "name": "DeathSpank", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 2011, - "name": "Diablo III", - "site": "gamer_info_com", - "genres": [ - "RPG", - "hack & slash" - ] - }, - { - "id": 2012, - "name": "Dragon Age: Inquisition", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2013, - "name": "Clustertruck", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 2014, - "name": "Choice Chamber", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2015, - "name": "Diablo III", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2016, - "name": "Dead Space 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2017, - "name": "Dead Island: Ryder White", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2018, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2019, - "name": "Darksiders", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2020, - "name": "Dead by Daylight", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "От третьего лица", - "Ужасы", - "Экшен", - "От первого лица" - ] - }, - { - "id": 2021, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2022, - "name": "DeathSpank: Thongs of Virtue", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Приключения", - "Головоломки/Пазл", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 2023, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2024, - "name": "Claire", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2025, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2026, - "name": "Dead Space 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2027, - "name": "Cold Fear", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2028, - "name": "Crysis", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 2029, - "name": "Dig or Die", - "site": "gamer_info_com", - "genres": [ - "инди", - "аркадные" - ] - }, - { - "id": 2030, - "name": "Deathtrap", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2031, - "name": "Darksiders II", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2032, - "name": "Deadlight", - "site": "playground_ru", - "genres": [ - "Экшен", - "Инди", - "Зомби" - ] - }, - { - "id": 2033, - "name": "Dig or Die", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2034, - "name": "Dead Rising", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2035, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2036, - "name": "Disciples II: Dark Prophecy", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2037, - "name": "Dead Space 3: Awakened (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2038, - "name": "Disciples II: Dark Prophecy", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2039, - "name": "Demigod", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Стратегия в реальном времени/РТС", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 2040, - "name": "Darksiders III", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 2041, - "name": "Deadpool", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Юмор" - ] - }, - { - "id": 2042, - "name": "Crysis 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 2043, - "name": "Clustertruck", - "site": "mobygames_com", - "genres": [ - "Racing / driving" - ] - }, - { - "id": 2044, - "name": "Costume Quest", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "RPG", - "Adventure" - ] - }, - { - "id": 2045, - "name": "Dead Space", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2046, - "name": "Dragon Age: Origins", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2047, - "name": "Disciples III: Renaissance", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2048, - "name": "Disciples III: Renaissance", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2049, - "name": "Descenders", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг", - "Симулятор", - "Спортивные игры" - ] - }, - { - "id": 2050, - "name": "Dead by Daylight", - "site": "iwantgames_ru", - "genres": [ - "Ужасы" - ] - }, - { - "id": 2051, - "name": "DeathSpank", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ролевая" - ] - }, - { - "id": 2052, - "name": "Daylight", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2053, - "name": "Dragon Age: Origins - Awakening", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2054, - "name": "Dishonored", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2055, - "name": "Dead Space 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2056, - "name": "Cold Fear", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2057, - "name": "Dishonored", - "site": "gamer_info_com", - "genres": [ - "stealth" - ] - }, - { - "id": 2058, - "name": "Detention", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл" - ] - }, - { - "id": 2059, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2060, - "name": "Deadlight", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2061, - "name": "DeathSpank: Thongs of Virtue", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ролевая" - ] - }, - { - "id": 2062, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2063, - "name": "Dead Island", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2064, - "name": "Costume Quest 2", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "RPG", - "Adventure" - ] - }, - { - "id": 2065, - "name": "Crysis Warhead", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 2066, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2067, - "name": "Dead Space 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2068, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2069, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2070, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 2071, - "name": "Deadpool", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2072, - "name": "Costume Quest", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 2073, - "name": "Cube Escape: Arles", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2074, - "name": "Deathtrap", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая", - "Стратегия" - ] - }, - { - "id": 2075, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2076, - "name": "Dead Island: Riptide", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2077, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2078, - "name": "Distraint", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2079, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2080, - "name": "Cube Escape: Case 23", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2081, - "name": "Dead Space 3: Awakened (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2082, - "name": "Devil May Cry 4", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 2083, - "name": "DeathSpank", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2084, - "name": "Demigod", - "site": "playground_ru", - "genres": [ - "Экшен", - "MOBA", - "Ролевая", - "Стратегия" - ] - }, - { - "id": 2085, - "name": "Disturbed", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2086, - "name": "Costume Quest 2", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 2087, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2088, - "name": "Dead Island: Ryder White", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2089, - "name": "Distraint", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы", - "приключения" - ] - }, - { - "id": 2090, - "name": "Cube Escape: Harvey's Box", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2091, - "name": "Devil May Cry 5", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 2092, - "name": "Cross of the Dutchman", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 2093, - "name": "Dead by Daylight", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2094, - "name": "DeathSpank: Thongs of Virtue", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2095, - "name": "DmC: Devil May Cry", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2096, - "name": "Descenders", - "site": "playground_ru", - "genres": [ - "Аркада", - "Гонки", - "Симулятор", - "Спорт" - ] - }, - { - "id": 2097, - "name": "Dragon's Dogma: Dark Arisen", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 2098, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2099, - "name": "Cube Escape: Seasons", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2100, - "name": "Disturbed", - "site": "gamer_info_com", - "genres": [ - "инди", - "визуальные новеллы", - "ужасы", - "приключения" - ] - }, - { - "id": 2101, - "name": "Dead Rising", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2102, - "name": "Diablo II", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 2103, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2104, - "name": "Deadlight", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Платформеры" - ] - }, - { - "id": 2105, - "name": "Deathtrap", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2106, - "name": "Draw a Stickman: Episode 1", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2107, - "name": "Detention", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "Выживание" - ] - }, - { - "id": 2108, - "name": "Cube Escape: The Lake", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2109, - "name": "DmC: Devil May Cry", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2110, - "name": "Cry Of Fear", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 2111, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2112, - "name": "Doki Doki Literature Club", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2113, - "name": "Dead Space", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2114, - "name": "Draw a Stickman: Episode 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2115, - "name": "Cube Escape: The Mill", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2116, - "name": "Cross of the Dutchman", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2117, - "name": "Demigod", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2118, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Слэшер" - ] - }, - { - "id": 2119, - "name": "Don't Knock Twice", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2120, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2121, - "name": "DreadOut", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 2122, - "name": "DOOM (2016)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2123, - "name": "Dead Space 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2124, - "name": "Crysis", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2125, - "name": "Doom 3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2126, - "name": "Descenders", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2127, - "name": "Deadpool", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 2128, - "name": "Devil May Cry 4", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Слэшер" - ] - }, - { - "id": 2129, - "name": "Doki Doki Literature Club", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2130, - "name": "Dropsy", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 2131, - "name": "Cry Of Fear", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2132, - "name": "Diablo III", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 2133, - "name": "Double Dragon Neon", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2134, - "name": "Dead Space 3", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2135, - "name": "DeathSpank", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения", - "Файтинги" - ] - }, - { - "id": 2136, - "name": "Crysis 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2137, - "name": "Detention", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2138, - "name": "Don't Knock Twice", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 2139, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2140, - "name": "Devil May Cry 5", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Слэшер" - ] - }, - { - "id": 2141, - "name": "Dude, Stop", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Логика" - ] - }, - { - "id": 2142, - "name": "Down Stairs", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2143, - "name": "Dark Deception", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 2144, - "name": "Dead Space 3: Awakened (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2145, - "name": "Crysis", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2146, - "name": "Dig or Die", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2147, - "name": "Diablo II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2148, - "name": "Doom 3", - "site": "gamer_info_com", - "genres": [ - "FPS", - "ужасы", - "action" - ] - }, - { - "id": 2149, - "name": "DeathSpank: Thongs of Virtue", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 2150, - "name": "Downfall", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2151, - "name": "Duke Nukem Forever", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2152, - "name": "Crysis Warhead", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2153, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2154, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2155, - "name": "Dark Messiah of Might and Magic", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 2156, - "name": "Dead by Daylight", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2157, - "name": "Double Dragon Neon", - "site": "gamer_info_com", - "genres": [ - "beat 'em up" - ] - }, - { - "id": 2158, - "name": "Disciples II: Dark Prophecy", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 2159, - "name": "Deathtrap", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Приключения", - "Экшены", - "Стратегии" - ] - }, - { - "id": 2160, - "name": "Dungeon Nightmares", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2161, - "name": "Cube Escape: Arles", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2162, - "name": "Crysis 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2163, - "name": "Diablo III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2164, - "name": "Dragon Age II", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2165, - "name": "Disciples III: Renaissance", - "site": "gamebomb_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 2166, - "name": "Down Stairs", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2167, - "name": "Dying Light", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2168, - "name": "Demigod", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2169, - "name": "Deadlight", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 2170, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2171, - "name": "Dark Sector", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 2172, - "name": "Dragon Age: Inquisition", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2173, - "name": "Cube Escape: Case 23", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2174, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2175, - "name": "Dying Light: The Following (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2176, - "name": "Downfall", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2177, - "name": "Crysis Warhead", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2178, - "name": "Deadpool", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2179, - "name": "Devil May Cry 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2180, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2181, - "name": "Dig or Die", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2182, - "name": "Descenders", - "site": "ag_ru", - "genres": [ - "Экшены", - "Гонки", - "Спортивные" - ] - }, - { - "id": 2183, - "name": "Cube Escape: Harvey's Box", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2184, - "name": "Dark Souls II", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 2185, - "name": "Emily Wants To Play", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2186, - "name": "Cube Escape: Arles", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2187, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2188, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "gamer_info_com", - "genres": [ - "инди" - ] - }, - { - "id": 2189, - "name": "DeathSpank", - "site": "stopgame_ru", - "genres": [ - "adventure", - "rpg" - ] - }, - { - "id": 2190, - "name": "Devil May Cry 5", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 2191, - "name": "Disciples II: Dark Prophecy", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2192, - "name": "Dishonored", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 2193, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2194, - "name": "Escape Dead Island", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 2195, - "name": "Detention", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Инди", - "Приключения" - ] - }, - { - "id": 2196, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2197, - "name": "Cube Escape: Seasons", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2198, - "name": "Cube Escape: Case 23", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2199, - "name": "Dragon Age II", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 2200, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2201, - "name": "Disciples III: Renaissance", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2202, - "name": "DeathSpank: Thongs of Virtue", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 2203, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2204, - "name": "Diablo II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2205, - "name": "Dragon Age: Origins", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2206, - "name": "Evil Defenders", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 2207, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2208, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2209, - "name": "Cube Escape: The Lake", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2210, - "name": "Dragon Age: Inquisition", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2211, - "name": "Dragon Age: Origins - Awakening", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2212, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2213, - "name": "Cube Escape: Harvey's Box", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2214, - "name": "Dishonored", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Стимпанк", - "Стелс" - ] - }, - { - "id": 2215, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2216, - "name": "Evoland", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2217, - "name": "Deathtrap", - "site": "stopgame_ru", - "genres": [ - "strategy", - "rpg" - ] - }, - { - "id": 2218, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2219, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2220, - "name": "Distraint", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 2221, - "name": "Devil May Cry 4", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 2222, - "name": "Cube Escape: The Mill", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2223, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2224, - "name": "Cube Escape: Seasons", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2225, - "name": "Evoland 2", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2226, - "name": "Diablo III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2227, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 2228, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2229, - "name": "Disturbed", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2230, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2231, - "name": "Demigod", - "site": "stopgame_ru", - "genres": [ - "strategy", - "rpg" - ] - }, - { - "id": 2232, - "name": "Devil May Cry 5", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2233, - "name": "DOOM (2016)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2234, - "name": "Eyes The Horror Game", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2235, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2236, - "name": "Cube Escape: The Lake", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2237, - "name": "Dark Souls III", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 2238, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2239, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 2240, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2241, - "name": "Descenders", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2242, - "name": "F.E.A.R.", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2243, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2244, - "name": "Diablo II", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2245, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2246, - "name": "Dig or Die", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2247, - "name": "Cube Escape: The Mill", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2248, - "name": "Dragon Age: Origins", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2249, - "name": "Dark Deception", - "site": "store_steampowered_com", - "genres": [ - "Free to Play", - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 2250, - "name": "Distraint", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Аркада", - "Инди" - ] - }, - { - "id": 2251, - "name": "F.E.A.R. 2: Project Origin", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2252, - "name": "Dragon's Dogma: Dark Arisen", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2253, - "name": "Detention", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2254, - "name": "DmC: Devil May Cry", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 2255, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2256, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2257, - "name": "DOOM (2016)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2258, - "name": "Dragon Age: Origins - Awakening", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2259, - "name": "Disciples II: Dark Prophecy", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2260, - "name": "F.E.A.R. 2: Reborn", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2261, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2262, - "name": "Dark Messiah of Might and Magic", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2263, - "name": "Draw a Stickman: Episode 1", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2264, - "name": "Disturbed", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Аркада", - "Адвенчура", - "Инди" - ] - }, - { - "id": 2265, - "name": "Dark Souls: Prepare to Die Edition", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2266, - "name": "Diablo III", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2267, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2268, - "name": "Draw a Stickman: Episode 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2269, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2270, - "name": "F.E.A.R. 3", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2271, - "name": "Disciples III: Renaissance", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2272, - "name": "Doki Doki Literature Club", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 2273, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2274, - "name": "DmC: Devil May Cry", - "site": "playground_ru", - "genres": [ - "Экшен", - "Слэшер" - ] - }, - { - "id": 2275, - "name": "Dark Deception", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2276, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2277, - "name": "DreadOut", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2278, - "name": "Dark Sector", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2279, - "name": "Devil May Cry 4", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2280, - "name": "F.E.A.R. Extraction Point", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2281, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2282, - "name": "Dark Souls: Remastered", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2283, - "name": "Don't Knock Twice", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 2284, - "name": "Dishonored", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Приключения" - ] - }, - { - "id": 2285, - "name": "Dropsy", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2286, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2287, - "name": "Dig or Die", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Стратегии" - ] - }, - { - "id": 2288, - "name": "Dark Messiah of Might and Magic", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2289, - "name": "F.E.A.R. Perseus Mandate", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2290, - "name": "Devil May Cry 5", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2291, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2292, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2293, - "name": "Dude, Stop", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2294, - "name": "Doom 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 2295, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2296, - "name": "Dark Souls II", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 2297, - "name": "Doki Doki Literature Club", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2298, - "name": "Disciples II: Dark Prophecy", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2299, - "name": "Fable (2004)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2300, - "name": "Diablo II", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2301, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2302, - "name": "Duke Nukem Forever", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2303, - "name": "Double Dragon Neon", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 2304, - "name": "Fable III", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2305, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2306, - "name": "Don't Knock Twice", - "site": "playground_ru", - "genres": [ - "Инди", - "Ужасы", - "Адвенчура", - "Виртуальная реальность", - "От первого лица" - ] - }, - { - "id": 2307, - "name": "Dark Sector", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2308, - "name": "Disciples III: Renaissance", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2309, - "name": "Dungeon Nightmares", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2310, - "name": "Darksiders", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2311, - "name": "Down Stairs", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2312, - "name": "Dragon's Dogma: Dark Arisen", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 2313, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2314, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2315, - "name": "Fahrenheit", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2316, - "name": "Distraint", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2317, - "name": "Dying Light", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2318, - "name": "Downfall", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2319, - "name": "Doom 3", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Ужасы" - ] - }, - { - "id": 2320, - "name": "Draw a Stickman: Episode 1", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2321, - "name": "Dishonored", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Приключения", - "Экшены" - ] - }, - { - "id": 2322, - "name": "Diablo III", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 2323, - "name": "Failman", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2324, - "name": "Dark Souls II", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2325, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2326, - "name": "Dying Light: The Following (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2327, - "name": "Darksiders II", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2328, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2329, - "name": "Disturbed", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2330, - "name": "Double Dragon Neon", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 2331, - "name": "Draw a Stickman: Episode 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2332, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2333, - "name": "Fallout 3", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 2334, - "name": "Emily Wants To Play", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2335, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2336, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2337, - "name": "Dragon Age II", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2338, - "name": "DmC: Devil May Cry", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2339, - "name": "Darksiders III", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2340, - "name": "Down Stairs", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2341, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2342, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2343, - "name": "Escape Dead Island", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2344, - "name": "DreadOut", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 2345, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2346, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2347, - "name": "Downfall", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2348, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2349, - "name": "Evil Defenders", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2350, - "name": "Dig or Die", - "site": "stopgame_ru", - "genres": [ - "strategy", - "arcade" - ] - }, - { - "id": 2351, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2352, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2353, - "name": "Dropsy", - "site": "gamer_info_com", - "genres": [ - "инди", - "квесты" - ] - }, - { - "id": 2354, - "name": "Distraint", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Приключения" - ] - }, - { - "id": 2355, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2356, - "name": "Evoland", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2357, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2358, - "name": "Dragon Age: Inquisition", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2359, - "name": "Daylight", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 2360, - "name": "Disciples II: Dark Prophecy", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2361, - "name": "Doki Doki Literature Club", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2362, - "name": "Dude, Stop", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2363, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2364, - "name": "Disturbed", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 2365, - "name": "Dark Souls III", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2366, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2367, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2368, - "name": "Evoland 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2369, - "name": "Dragon Age II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2370, - "name": "Duke Nukem Forever", - "site": "gamer_info_com", - "genres": [ - "для взрослых", - "FPS" - ] - }, - { - "id": 2371, - "name": "Don't Knock Twice", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2372, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2373, - "name": "Disciples III: Renaissance", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2374, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2375, - "name": "Eyes The Horror Game", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2376, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2377, - "name": "Dead Island", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 2378, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2379, - "name": "Dragon Age: Inquisition", - "site": "playground_ru", - "genres": [ - "Ролевая", - "Фэнтези" - ] - }, - { - "id": 2380, - "name": "DmC: Devil May Cry", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2381, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2382, - "name": "Dungeon Nightmares", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2383, - "name": "F.E.A.R.", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2384, - "name": "Doom 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2385, - "name": "Dishonored", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2386, - "name": "Fallout 4", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 2387, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2388, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2389, - "name": "Dark Souls III", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2390, - "name": "Dead Island: Riptide", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 2391, - "name": "F.E.A.R. 2: Project Origin", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2392, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2393, - "name": "Dragon Age: Origins", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2394, - "name": "Dying Light", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 2395, - "name": "Fallout: New Vegas", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2396, - "name": "Double Dragon Neon", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2397, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2398, - "name": "F.E.A.R. 2: Reborn", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2399, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2400, - "name": "Dead Island: Ryder White", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2401, - "name": "Family Trouble", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2402, - "name": "Doki Doki Literature Club", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 2403, - "name": "Dying Light: The Following (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2404, - "name": "Dragon Age: Origins - Awakening", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2405, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 2406, - "name": "Down Stairs", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2407, - "name": "F.E.A.R. 3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2408, - "name": "Dark Souls: Prepare to Die Edition", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 2409, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2410, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2411, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2412, - "name": "Dead Rising", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2413, - "name": "Far Cry", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2414, - "name": "Emily Wants To Play", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы", - "survival" - ] - }, - { - "id": 2415, - "name": "F.E.A.R. Extraction Point", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2416, - "name": "Downfall", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2417, - "name": "Don't Knock Twice", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 2418, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2419, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2420, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 2421, - "name": "Dragon Age: Origins", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Тактика", - "Ролевая", - "Экшен", - "Фэнтези" - ] - }, - { - "id": 2422, - "name": "Distraint", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 2423, - "name": "Far Cry 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2424, - "name": "F.E.A.R. Perseus Mandate", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2425, - "name": "Escape Dead Island", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 2426, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2427, - "name": "Dead Space", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 2428, - "name": "Doom 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2429, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2430, - "name": "Fable (2004)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2431, - "name": "Far Cry 3: Blood Dragon", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2432, - "name": "Disturbed", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2433, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2434, - "name": "Dragon Age: Origins - Awakening", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2435, - "name": "Evil Defenders", - "site": "gamer_info_com", - "genres": [ - "инди", - "tower defense" - ] - }, - { - "id": 2436, - "name": "Dark Souls: Remastered", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2437, - "name": "Fable III", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2438, - "name": "FarSky", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2439, - "name": "Dragon Age II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2440, - "name": "Double Dragon Neon", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди" - ] - }, - { - "id": 2441, - "name": "Dead Space 2", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 2442, - "name": "Dark Souls: Prepare to Die Edition", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2443, - "name": "DmC: Devil May Cry", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2444, - "name": "Evoland", - "site": "gamer_info_com", - "genres": [ - "jRPG", - "приключения", - "action" - ] - }, - { - "id": 2445, - "name": "Dragon's Dogma: Dark Arisen", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 2446, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2447, - "name": "Fahrenheit", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2448, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2449, - "name": "Final Fantasy III (Remake)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2450, - "name": "Dragon Age: Inquisition", - "site": "iwantgames_ru", - "genres": [ - "РПГ" - ] - }, - { - "id": 2451, - "name": "Draw a Stickman: Episode 1", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2452, - "name": "Evoland 2", - "site": "gamer_info_com", - "genres": [ - "RTS", - "инди", - "приключения" - ] - }, - { - "id": 2453, - "name": "Failman", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2454, - "name": "Down Stairs", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2455, - "name": "Dead Space 3", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2456, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2457, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2458, - "name": "Final Fantasy IV (Remake)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2459, - "name": "Draw a Stickman: Episode 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2460, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2461, - "name": "Fallout 3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2462, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2463, - "name": "Darksiders", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2464, - "name": "Dead Space 3: Awakened (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2465, - "name": "Eyes The Horror Game", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2466, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2467, - "name": "Doki Doki Literature Club", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 2468, - "name": "Downfall", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 2469, - "name": "DreadOut", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 2470, - "name": "Dark Souls: Remastered", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2471, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2472, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2473, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2474, - "name": "F.E.A.R.", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 2475, - "name": "Final Fantasy X HD Remaster", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2476, - "name": "Darksiders II", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2477, - "name": "Dropsy", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2478, - "name": "Don't Knock Twice", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2479, - "name": "Dead by Daylight", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 2480, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2481, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 2482, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2483, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2484, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2485, - "name": "Dude, Stop", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2486, - "name": "F.E.A.R. 2: Project Origin", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 2487, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2488, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2489, - "name": "Doom 3", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2490, - "name": "Darksiders III", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Adventure" - ] - }, - { - "id": 2491, - "name": "Final Fantasy XIII", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2492, - "name": "Dragon Age II", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2493, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2494, - "name": "Dragon Age: Origins", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2495, - "name": "F.E.A.R. 2: Reborn", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2496, - "name": "Duke Nukem Forever", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 2497, - "name": "Darksiders", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2498, - "name": "Deadlight", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 2499, - "name": "Dragon's Dogma: Dark Arisen", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 2500, - "name": "Double Dragon Neon", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 2501, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2502, - "name": "Final Fantasy XIII-2", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2503, - "name": "Dragon Age: Inquisition", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2504, - "name": "Dungeon Nightmares", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2505, - "name": "Dragon Age: Origins - Awakening", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2506, - "name": "F.E.A.R. 3", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2507, - "name": "Draw a Stickman: Episode 1", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2508, - "name": "Daylight", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2509, - "name": "Fallout 4", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2510, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2511, - "name": "Down Stairs", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2512, - "name": "F.E.A.R. Extraction Point", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2513, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2514, - "name": "Darksiders II", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2515, - "name": "Draw a Stickman: Episode 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2516, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 2517, - "name": "Fallout: New Vegas", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2518, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2519, - "name": "Downfall", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 2520, - "name": "Dead Island", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2521, - "name": "Deadpool", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2522, - "name": "F.E.A.R. Perseus Mandate", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2523, - "name": "Dying Light", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 2524, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2525, - "name": "Family Trouble", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2526, - "name": "DreadOut", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "От третьего лица", - "Инди" - ] - }, - { - "id": 2527, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2528, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2529, - "name": "Darksiders III", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2530, - "name": "Dead Island: Riptide", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2531, - "name": "Dying Light: The Following (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2532, - "name": "Far Cry", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2533, - "name": "DeathSpank", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 2534, - "name": "Fable (2004)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2535, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2536, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2537, - "name": "Dropsy", - "site": "playground_ru", - "genres": [ - "Аркада", - "Адвенчура", - "Инди" - ] - }, - { - "id": 2538, - "name": "Final Fantasy XV", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 2539, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 2540, - "name": "Far Cry 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2541, - "name": "Dead Island: Ryder White", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2542, - "name": "Emily Wants To Play", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 2543, - "name": "Daylight", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2544, - "name": "Fable III", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 2545, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2546, - "name": "Dude, Stop", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2547, - "name": "DeathSpank: Thongs of Virtue", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 2548, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2549, - "name": "Far Cry 3: Blood Dragon", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2550, - "name": "Dragon Age II", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2551, - "name": "Dragon Age: Origins", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2552, - "name": "Escape Dead Island", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 2553, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2554, - "name": "Fahrenheit", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2555, - "name": "FarSky", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2556, - "name": "Duke Nukem Forever", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "От первого лица", - "Шутер" - ] - }, - { - "id": 2557, - "name": "Dragon's Dogma: Dark Arisen", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2558, - "name": "Dead Island", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2559, - "name": "Dead Rising", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2560, - "name": "Dragon Age: Inquisition", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 2561, - "name": "Dragon Age: Origins - Awakening", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2562, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2563, - "name": "Failman", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2564, - "name": "Evil Defenders", - "site": "gamebomb_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 2565, - "name": "Final Fantasy III (Remake)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2566, - "name": "Deathtrap", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2567, - "name": "Dungeon Nightmares", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2568, - "name": "Draw a Stickman: Episode 1", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2569, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2570, - "name": "Final Fantasy IV (Remake)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2571, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2572, - "name": "Fallout 3", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 2573, - "name": "Evoland", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 2574, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2575, - "name": "Dead Space", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2576, - "name": "Dying Light", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Кооператив", - "Зомби", - "Экшен", - "От первого лица" - ] - }, - { - "id": 2577, - "name": "Dead Island: Riptide", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving", - "Role-Playing (RPG)" - ] - }, - { - "id": 2578, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2579, - "name": "Draw a Stickman: Episode 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2580, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2581, - "name": "Evoland 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2582, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2583, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2584, - "name": "Demigod", - "site": "gamespot_com", - "genres": [ - "MOBA", - "Strategy" - ] - }, - { - "id": 2585, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2586, - "name": "Final Fantasy X HD Remaster", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2587, - "name": "Eyes The Horror Game", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2588, - "name": "Firewatch", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 2589, - "name": "Dead Space 2", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2590, - "name": "DreadOut", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2591, - "name": "Dying Light: The Following (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2592, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2593, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2594, - "name": "F.E.A.R.", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2595, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2596, - "name": "Dead Island: Ryder White", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 2597, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2598, - "name": "First Winter", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2599, - "name": "Emily Wants To Play", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2600, - "name": "Dropsy", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2601, - "name": "Dead Space 3", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2602, - "name": "Descenders", - "site": "gamespot_com", - "genres": [ - "Arcade", - "Driving/Racing" - ] - }, - { - "id": 2603, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2604, - "name": "Final Fantasy XIII", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2605, - "name": "Dragon Age: Origins", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 2606, - "name": "Five Minutes", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2607, - "name": "F.E.A.R. 2: Project Origin", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 2608, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2609, - "name": "Escape Dead Island", - "site": "playground_ru", - "genres": [ - "Экшен", - "Выживание", - "Вид сверху" - ] - }, - { - "id": 2610, - "name": "Final Fantasy XIII-2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2611, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2612, - "name": "Dude, Stop", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2613, - "name": "Dead Space 3: Awakened (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2614, - "name": "Dead Rising", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2615, - "name": "F.E.A.R. 2: Reborn", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2616, - "name": "Dragon Age: Origins - Awakening", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2617, - "name": "Five Nights At Freddy's", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 2618, - "name": "Dragon's Dogma: Dark Arisen", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2619, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2620, - "name": "Evil Defenders", - "site": "playground_ru", - "genres": [ - "Аркада", - "Стратегия", - "Tower defence" - ] - }, - { - "id": 2621, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2622, - "name": "Duke Nukem Forever", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2623, - "name": "Detention", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 2624, - "name": "Five Nights At Freddy's 2", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 2625, - "name": "F.E.A.R. 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 2626, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2627, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2628, - "name": "Dead by Daylight", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2629, - "name": "Draw a Stickman: Episode 1", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2630, - "name": "Dead Space", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2631, - "name": "Fallout 4", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 2632, - "name": "Evoland", - "site": "playground_ru", - "genres": [ - "Инди", - "Вид сверху", - "Адвенчура", - "Ретро", - "Экшен" - ] - }, - { - "id": 2633, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2634, - "name": "Five Nights At Freddy's 3", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик" - ] - }, - { - "id": 2635, - "name": "Dungeon Nightmares", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2636, - "name": "F.E.A.R. Extraction Point", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 2637, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2638, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2639, - "name": "Evoland 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2640, - "name": "Draw a Stickman: Episode 2", - "site": "ag_ru", - "genres": [ - "Приключения", - "Семейные" - ] - }, - { - "id": 2641, - "name": "Fallout: New Vegas", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 2642, - "name": "Final Fantasy XV", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2643, - "name": "Five Nights At Freddy's 4", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2644, - "name": "F.E.A.R. Perseus Mandate", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 2645, - "name": "Dying Light", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Экшен" - ] - }, - { - "id": 2646, - "name": "Deadlight", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 2647, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2648, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2649, - "name": "Dead Space 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2650, - "name": "Eyes The Horror Game", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2651, - "name": "Five Nights at Freddy's 3D", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2652, - "name": "Family Trouble", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2653, - "name": "DreadOut", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 2654, - "name": "Fable (2004)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2655, - "name": "Devil May Cry 4", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2656, - "name": "Dying Light: The Following (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2657, - "name": "Deadpool", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2658, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2659, - "name": "Far Cry", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 2660, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2661, - "name": "Five Nights in Anime v3", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2662, - "name": "Dropsy", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Инди", - "Приключения" - ] - }, - { - "id": 2663, - "name": "F.E.A.R.", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Ужасы" - ] - }, - { - "id": 2664, - "name": "Fable III", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 2665, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2666, - "name": "Devil May Cry 5", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2667, - "name": "Far Cry 2", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 2668, - "name": "Fran Bow", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 2669, - "name": "Emily Wants To Play", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2670, - "name": "Dragon's Dogma: Dark Arisen", - "site": "stopgame_ru", - "genres": [ - "add-on", - "rpg" - ] - }, - { - "id": 2671, - "name": "Dude, Stop", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Приключения" - ] - }, - { - "id": 2672, - "name": "Dead Space 3", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2673, - "name": "Fahrenheit", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2674, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2675, - "name": "DeathSpank", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 2676, - "name": "F.E.A.R. 2: Project Origin", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Ужасы" - ] - }, - { - "id": 2677, - "name": "Front Mission Evolved", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 2678, - "name": "Diablo II", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 2679, - "name": "Far Cry 3: Blood Dragon", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 2680, - "name": "Escape Dead Island", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Экшен" - ] - }, - { - "id": 2681, - "name": "Failman", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2682, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2683, - "name": "Duke Nukem Forever", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2684, - "name": "Draw a Stickman: Episode 1", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2685, - "name": "F.E.A.R. 2: Reborn", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2686, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2687, - "name": "Firewatch", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2688, - "name": "FarSky", - "site": "gamer_info_com", - "genres": [ - "инди", - "survival" - ] - }, - { - "id": 2689, - "name": "Dead Space 3: Awakened (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 2690, - "name": "GUN", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2691, - "name": "DeathSpank: Thongs of Virtue", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 2692, - "name": "Evil Defenders", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2693, - "name": "Fallout 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 2694, - "name": "Dungeon Nightmares", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 2695, - "name": "Draw a Stickman: Episode 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2696, - "name": "First Winter", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2697, - "name": "F.E.A.R. 3", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Кооператив", - "Шутер", - "Экшен", - "От первого лица" - ] - }, - { - "id": 2698, - "name": "Final Fantasy III (Remake)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2699, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2700, - "name": "Gemini Rue", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 2701, - "name": "Diablo III", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 2702, - "name": "Evoland", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2703, - "name": "Five Minutes", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2704, - "name": "Dying Light", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2705, - "name": "Dead by Daylight", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2706, - "name": "F.E.A.R. Extraction Point", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2707, - "name": "DreadOut", - "site": "stopgame_ru", - "genres": [ - "adventure", - "action" - ] - }, - { - "id": 2708, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2709, - "name": "Deathtrap", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy", - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 2710, - "name": "Geometry Dash", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2711, - "name": "Final Fantasy IV (Remake)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2712, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2713, - "name": "Five Nights At Freddy's", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2714, - "name": "F.E.A.R. Perseus Mandate", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2715, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2716, - "name": "Evoland 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2717, - "name": "Dying Light: The Following (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 2718, - "name": "Ghost of a Tale", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 2719, - "name": "Dropsy", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 2720, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2721, - "name": "Deadlight", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2722, - "name": "Five Nights At Freddy's 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2723, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2724, - "name": "Fable (2004)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2725, - "name": "Eyes The Horror Game", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2726, - "name": "Demigod", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Strategy", - "Indie" - ] - }, - { - "id": 2727, - "name": "Goat Simulator", - "site": "igromania_ru", - "genres": [ - "Прочее", - "Симулятор" - ] - }, - { - "id": 2728, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2729, - "name": "Five Nights At Freddy's 3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2730, - "name": "Final Fantasy X HD Remaster", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2731, - "name": "Dude, Stop", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 2732, - "name": "Dig or Die", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2733, - "name": "Emily Wants To Play", - "site": "ag_ru", - "genres": [ - "Приключения", - "Инди", - "Экшены", - "Казуальные", - "Головоломки", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 2734, - "name": "Fable III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2735, - "name": "Deadpool", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2736, - "name": "F.E.A.R.", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2737, - "name": "Grand Theft Auto III", - "site": "igromania_ru", - "genres": [ - "Автомобили", - "Боевик от третьего лица", - "Симулятор", - "Боевик" - ] - }, - { - "id": 2738, - "name": "Five Nights At Freddy's 4", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2739, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2740, - "name": "Escape Dead Island", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 2741, - "name": "Fallout 4", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2742, - "name": "Duke Nukem Forever", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2743, - "name": "Descenders", - "site": "store_steampowered_com", - "genres": [ - "Sports", - "Action", - "Racing" - ] - }, - { - "id": 2744, - "name": "Fahrenheit", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура" - ] - }, - { - "id": 2745, - "name": "Disciples II: Dark Prophecy", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 2746, - "name": "Grand Theft Auto: San Andreas", - "site": "igromania_ru", - "genres": [ - "Автомобили", - "Боевик от третьего лица", - "Симулятор", - "Боевик" - ] - }, - { - "id": 2747, - "name": "Five Nights at Freddy's 3D", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2748, - "name": "F.E.A.R. 2: Project Origin", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2749, - "name": "Final Fantasy XIII", - "site": "gamer_info_com", - "genres": [ - "jRPG" - ] - }, - { - "id": 2750, - "name": "DeathSpank", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 2751, - "name": "Failman", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2752, - "name": "Dungeon Nightmares", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2753, - "name": "Grand Theft Auto: Vice City", - "site": "igromania_ru", - "genres": [ - "Автомобили", - "Боевик от третьего лица", - "Симулятор", - "Боевик" - ] - }, - { - "id": 2754, - "name": "Five Nights in Anime v3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2755, - "name": "Fallout: New Vegas", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 2756, - "name": "Disciples III: Renaissance", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 2757, - "name": "Evil Defenders", - "site": "ag_ru", - "genres": [ - "Приключения", - "Казуальные", - "Инди", - "Стратегии" - ] - }, - { - "id": 2758, - "name": "F.E.A.R. 2: Reborn", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2759, - "name": "Detention", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 2760, - "name": "Final Fantasy XIII-2", - "site": "gamer_info_com", - "genres": [ - "jRPG" - ] - }, - { - "id": 2761, - "name": "Family Trouble", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2762, - "name": "Fallout 3", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Открытый мир", - "Постапокалипсис", - "Ролевая" - ] - }, - { - "id": 2763, - "name": "Fran Bow", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2764, - "name": "Grandpa", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2765, - "name": "DeathSpank: Thongs of Virtue", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2766, - "name": "Dying Light", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2767, - "name": "Evoland", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 2768, - "name": "F.E.A.R. 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2769, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2770, - "name": "Front Mission Evolved", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2771, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2772, - "name": "Far Cry", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Битвы машин", - "Боевик/Экшн" - ] - }, - { - "id": 2773, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 2774, - "name": "Grey", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2775, - "name": "Dishonored", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2776, - "name": "Dying Light: The Following (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2777, - "name": "GUN", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2778, - "name": "Evoland 2", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Приключения", - "Аркадные", - "Инди", - "Экшены", - "Файтинги" - ] - }, - { - "id": 2779, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2780, - "name": "F.E.A.R. Extraction Point", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2781, - "name": "Deathtrap", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 2782, - "name": "Grim Fandango Remastered", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 2783, - "name": "Far Cry 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 2784, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2785, - "name": "Gemini Rue", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2786, - "name": "Devil May Cry 4", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2787, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 2788, - "name": "Emily Wants To Play", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2789, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2790, - "name": "Eyes The Horror Game", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2791, - "name": "F.E.A.R. Perseus Mandate", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2792, - "name": "Grow Home", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 2793, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2794, - "name": "Geometry Dash", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2795, - "name": "Demigod", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 2796, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 2797, - "name": "Final Fantasy XV", - "site": "gamer_info_com", - "genres": [ - "jRPG", - "action" - ] - }, - { - "id": 2798, - "name": "Ghost of a Tale", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2799, - "name": "Escape Dead Island", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2800, - "name": "Grow Up", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 2801, - "name": "Fable (2004)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2802, - "name": "Far Cry 3: Blood Dragon", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 2803, - "name": "Devil May Cry 5", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 2804, - "name": "F.E.A.R.", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2805, - "name": "Goat Simulator", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2806, - "name": "Descenders", - "site": "mobygames_com", - "genres": [ - "Racing / driving" - ] - }, - { - "id": 2807, - "name": "Evil Defenders", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 2808, - "name": "Guns, Gore & Cannoli", - "site": "igromania_ru", - "genres": [ - "Платформер" - ] - }, - { - "id": 2809, - "name": "FarSky", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2810, - "name": "Distraint", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 2811, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 2812, - "name": "Fable III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2813, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2814, - "name": "F.E.A.R. 2: Project Origin", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2815, - "name": "Diablo II", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2816, - "name": "Grand Theft Auto III", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2817, - "name": "Final Fantasy III (Remake)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2818, - "name": "Guns, Gore & Cannoli 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2819, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2820, - "name": "Fahrenheit", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2821, - "name": "Evoland", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 2822, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 2823, - "name": "Grand Theft Auto: San Andreas", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2824, - "name": "Final Fantasy IV (Remake)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2825, - "name": "Detention", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 2826, - "name": "F.E.A.R. 2: Reborn", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2827, - "name": "Half-Life", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2828, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2829, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2830, - "name": "Grand Theft Auto: Vice City", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2831, - "name": "Failman", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2832, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2833, - "name": "Evoland 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2834, - "name": "Fallout 4", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Постапокалипсис", - "Ролевая", - "Экшен", - "Открытый мир", - "От первого лица" - ] - }, - { - "id": 2835, - "name": "Half-Life 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2836, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 2837, - "name": "Disturbed", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 2838, - "name": "Diablo III", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2839, - "name": "F.E.A.R. 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 2840, - "name": "Final Fantasy X HD Remaster", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2841, - "name": "Grandpa", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2842, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2843, - "name": "Fallout 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2844, - "name": "Eyes The Horror Game", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2845, - "name": "Half-Life 2: Episode One", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2846, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2847, - "name": "Grey", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2848, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2849, - "name": "Fallout: New Vegas", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Открытый мир", - "Постапокалипсис", - "Ролевая" - ] - }, - { - "id": 2850, - "name": "DmC: Devil May Cry", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2851, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2852, - "name": "Devil May Cry 4", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2853, - "name": "Half-Life 2: Episode Two", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2854, - "name": "F.E.A.R. Extraction Point", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2855, - "name": "Grim Fandango Remastered", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2856, - "name": "F.E.A.R.", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2857, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2858, - "name": "Final Fantasy XIII", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2859, - "name": "Family Trouble", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2860, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2861, - "name": "Firewatch", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 2862, - "name": "Grow Home", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2863, - "name": "Half-Life: Blue Shift", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2864, - "name": "F.E.A.R. Perseus Mandate", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2865, - "name": "Dig or Die", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Strategy", - "Indie" - ] - }, - { - "id": 2866, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2867, - "name": "F.E.A.R. 2: Project Origin", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2868, - "name": "Final Fantasy XIII-2", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 2869, - "name": "Devil May Cry 5", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2870, - "name": "Far Cry", - "site": "playground_ru", - "genres": [ - "Песочница", - "Шутер", - "Экшен", - "Открытый мир", - "От первого лица" - ] - }, - { - "id": 2871, - "name": "Grow Up", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2872, - "name": "First Winter", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2873, - "name": "Half-Life: Opposing Force", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2874, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2875, - "name": "Fable (2004)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2876, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2877, - "name": "F.E.A.R. 2: Reborn", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 2878, - "name": "Disciples II: Dark Prophecy", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2879, - "name": "Doki Doki Literature Club", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 2880, - "name": "Guns, Gore & Cannoli", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2881, - "name": "Five Minutes", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2882, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2883, - "name": "Half-Life: Paranoia", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2884, - "name": "Diablo II", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 2885, - "name": "Far Cry 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Песочница", - "От первого лица" - ] - }, - { - "id": 2886, - "name": "Guns, Gore & Cannoli 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2887, - "name": "Fable III", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2888, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2889, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2890, - "name": "Disciples III: Renaissance", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2891, - "name": "F.E.A.R. 3", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 2892, - "name": "Don't Knock Twice", - "site": "gamespot_com", - "genres": [ - "VR", - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 2893, - "name": "Five Nights At Freddy's", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 2894, - "name": "Half-Life: The Xeno Project", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2895, - "name": "Half-Life", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2896, - "name": "Far Cry 3: Blood Dragon", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Песочница" - ] - }, - { - "id": 2897, - "name": "Fahrenheit", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 2898, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2899, - "name": "F.E.A.R. Extraction Point", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 2900, - "name": "Five Nights At Freddy's 2", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 2901, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2902, - "name": "Final Fantasy XV", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 2903, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 2904, - "name": "Half-Life 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2905, - "name": "Doom 3", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 2906, - "name": "FarSky", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2907, - "name": "Dishonored", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2908, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2909, - "name": "Halo: Combat Evolved", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2910, - "name": "Half-Life 2: Episode One", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2911, - "name": "Five Nights At Freddy's 3", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 2912, - "name": "Failman", - "site": "ag_ru", - "genres": [] - }, - { - "id": 2913, - "name": "F.E.A.R. Perseus Mandate", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 2914, - "name": "Final Fantasy III (Remake)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2915, - "name": "Fallout 4", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "РПГ" - ] - }, - { - "id": 2916, - "name": "Double Dragon Neon", - "site": "gamespot_com", - "genres": [ - "Beat-'Em-Up", - "Action", - "2D" - ] - }, - { - "id": 2917, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2918, - "name": "Half-Life 2: Episode Two", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2919, - "name": "Diablo III", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 2920, - "name": "Hand of Fate", - "site": "igromania_ru", - "genres": [ - "Карточная игра", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 2921, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2922, - "name": "Five Nights At Freddy's 4", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 2923, - "name": "Final Fantasy IV (Remake)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2924, - "name": "Fallout 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые" - ] - }, - { - "id": 2925, - "name": "Fable (2004)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2926, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2927, - "name": "Fallout: New Vegas", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2928, - "name": "Down Stairs", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 2929, - "name": "Half-Life: Blue Shift", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2930, - "name": "Hand of Fate 2", - "site": "igromania_ru", - "genres": [ - "Карточная игра", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 2931, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2932, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2933, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2934, - "name": "Five Nights at Freddy's 3D", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2935, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2936, - "name": "Half-Life: Opposing Force", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2937, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 2938, - "name": "Fable III", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2939, - "name": "Family Trouble", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2940, - "name": "Happy Room", - "site": "igromania_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 2941, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2942, - "name": "Final Fantasy X HD Remaster", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2943, - "name": "Half-Life: Paranoia", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2944, - "name": "Five Nights in Anime v3", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2945, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2946, - "name": "Distraint", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2947, - "name": "Happy Wheels", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2948, - "name": "Fahrenheit", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 2949, - "name": "Dig or Die", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2950, - "name": "Far Cry", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2951, - "name": "Half-Life: The Xeno Project", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2952, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2953, - "name": "Fran Bow", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "квесты" - ] - }, - { - "id": 2954, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 2955, - "name": "Hard Reset", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 2956, - "name": "Downfall", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 2957, - "name": "Firewatch", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 2958, - "name": "Failman", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2959, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2960, - "name": "Disciples II: Dark Prophecy", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 2961, - "name": "Final Fantasy XIII", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2962, - "name": "Far Cry 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2963, - "name": "Front Mission Evolved", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 2964, - "name": "Disturbed", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Free to Play", - "Indie", - "Adventure", - "Simulation" - ] - }, - { - "id": 2965, - "name": "First Winter", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2966, - "name": "Hard Reset: Exile (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2967, - "name": "Halo: Combat Evolved", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2968, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2969, - "name": "Final Fantasy XIII-2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2970, - "name": "Fallout 3", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 2971, - "name": "Disciples III: Renaissance", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 2972, - "name": "Five Minutes", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 2973, - "name": "Far Cry 3: Blood Dragon", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2974, - "name": "Harts", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 2975, - "name": "Hand of Fate", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2976, - "name": "GUN", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2977, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2978, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2979, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2980, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 2981, - "name": "Five Nights At Freddy's", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Спортивные игры" - ] - }, - { - "id": 2982, - "name": "DmC: Devil May Cry", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 2983, - "name": "Hand of Fate 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2984, - "name": "Hatred", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 2985, - "name": "Dishonored", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 2986, - "name": "FarSky", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2987, - "name": "Gemini Rue", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2988, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 2989, - "name": "Fallout 4", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 2990, - "name": "Happy Room", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 2991, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 2992, - "name": "Hellblade: Senua's Sacrifice", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 2993, - "name": "Five Nights At Freddy's 2", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Приключения" - ] - }, - { - "id": 2994, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 2995, - "name": "Geometry Dash", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 2996, - "name": "Dragon Age II", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 2997, - "name": "Final Fantasy III (Remake)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 2998, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 2999, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3000, - "name": "Happy Wheels", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3001, - "name": "Cube Escape: The Mill", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3002, - "name": "Hellgate: London", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик от третьего лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 3003, - "name": "Fallout: New Vegas", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 3004, - "name": "Five Nights At Freddy's 3", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3005, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3006, - "name": "Ghost of a Tale", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 3007, - "name": "Final Fantasy XV", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3008, - "name": "Hard Reset", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3009, - "name": "DOOM (2016)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3010, - "name": "Final Fantasy IV (Remake)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3011, - "name": "Doki Doki Literature Club", - "site": "store_steampowered_com", - "genres": [ - "Free to Play", - "Casual", - "Indie" - ] - }, - { - "id": 3012, - "name": "Her Story", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3013, - "name": "Dragon Age: Inquisition", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "3D", - "Action", - "Role-Playing", - "Adventure" - ] - }, - { - "id": 3014, - "name": "Family Trouble", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3015, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3016, - "name": "Five Nights At Freddy's 4", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3017, - "name": "Dark Deception", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3018, - "name": "Hard Reset: Exile (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3019, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3020, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3021, - "name": "Goat Simulator", - "site": "gamer_info_com", - "genres": [ - "инди" - ] - }, - { - "id": 3022, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3023, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3024, - "name": "Hero Academy", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Пошаговая стратегия" - ] - }, - { - "id": 3025, - "name": "Far Cry", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3026, - "name": "Dark Messiah of Might and Magic", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3027, - "name": "Five Nights at Freddy's 3D", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3028, - "name": "Harts", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3029, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3030, - "name": "Don't Knock Twice", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 3031, - "name": "Grand Theft Auto III", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3032, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3033, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3034, - "name": "Distraint", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3035, - "name": "Dark Sector", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3036, - "name": "Heroes of Might and Magic III", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3037, - "name": "Five Nights in Anime v3", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3038, - "name": "Hatred", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3039, - "name": "Final Fantasy X HD Remaster", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3040, - "name": "Far Cry 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3041, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3042, - "name": "Grand Theft Auto: San Andreas", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "приключения", - "action" - ] - }, - { - "id": 3043, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3044, - "name": "Fran Bow", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3045, - "name": "Dark Souls II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3046, - "name": "Heroes of Might and Magic IV", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3047, - "name": "Fallout 4", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 3048, - "name": "Hellblade: Senua's Sacrifice", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3049, - "name": "Disturbed", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3050, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3051, - "name": "Far Cry 3: Blood Dragon", - "site": "ag_ru", - "genres": [ - "Шутеры" - ] - }, - { - "id": 3052, - "name": "Doom 3", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 3053, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3054, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3055, - "name": "Hellgate: London", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3056, - "name": "Heroes of Might and Magic V", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3057, - "name": "Front Mission Evolved", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер", - "Битвы машин" - ] - }, - { - "id": 3058, - "name": "Grand Theft Auto: Vice City", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "приключения", - "action" - ] - }, - { - "id": 3059, - "name": "Dragon Age: Origins", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 3060, - "name": "Fallout: New Vegas", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 3061, - "name": "FarSky", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 3062, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3063, - "name": "Final Fantasy XIII", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3064, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3065, - "name": "DmC: Devil May Cry", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3066, - "name": "Her Story", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3067, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3068, - "name": "GUN", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 3069, - "name": "Grandpa", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3070, - "name": "Dragon Age: Origins - Awakening", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3071, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3072, - "name": "Double Dragon Neon", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 3073, - "name": "Family Trouble", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3074, - "name": "Final Fantasy III (Remake)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3075, - "name": "Hero Academy", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3076, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3077, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3078, - "name": "Firewatch", - "site": "playground_ru", - "genres": [ - "Логические", - "Адвенчура", - "Инди", - "От первого лица" - ] - }, - { - "id": 3079, - "name": "Final Fantasy XIII-2", - "site": "iwantgames_ru", - "genres": [ - "РПГ" - ] - }, - { - "id": 3080, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3081, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3082, - "name": "Gemini Rue", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3083, - "name": "Grey", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3084, - "name": "Heroes of Might and Magic III", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3085, - "name": "Down Stairs", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3086, - "name": "Homefront", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 3087, - "name": "Final Fantasy IV (Remake)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3088, - "name": "First Winter", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3089, - "name": "Far Cry", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3090, - "name": "Dark Souls III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3091, - "name": "Geometry Dash", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3092, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3093, - "name": "Doki Doki Literature Club", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3094, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3095, - "name": "Heroes of Might and Magic IV", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3096, - "name": "Grim Fandango Remastered", - "site": "gamer_info_com", - "genres": [ - "квесты" - ] - }, - { - "id": 3097, - "name": "Hotel Remorse", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3098, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3099, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3100, - "name": "Five Minutes", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3101, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3102, - "name": "Far Cry 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3103, - "name": "Heroes of Might and Magic V", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3104, - "name": "Ghost of a Tale", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 3105, - "name": "Grow Home", - "site": "gamer_info_com", - "genres": [ - "аркадные", - "платформеры" - ] - }, - { - "id": 3106, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3107, - "name": "Downfall", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 3108, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3109, - "name": "Hotline Miami", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Боевик", - "Боевик от третьего лица" - ] - }, - { - "id": 3110, - "name": "Final Fantasy X HD Remaster", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3111, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3112, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3113, - "name": "Five Nights At Freddy's", - "site": "playground_ru", - "genres": [ - "Инди", - "Ужасы", - "Адвенчура", - "Симулятор", - "От первого лица" - ] - }, - { - "id": 3114, - "name": "Don't Knock Twice", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3115, - "name": "Dark Souls: Prepare to Die Edition", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 3116, - "name": "Grow Up", - "site": "gamer_info_com", - "genres": [ - "аркадные", - "платформеры" - ] - }, - { - "id": 3117, - "name": "Hotline Miami 2: Wrong Number", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3118, - "name": "Far Cry 3: Blood Dragon", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3119, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3120, - "name": "Goat Simulator", - "site": "gamebomb_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 3121, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3122, - "name": "Dragon's Dogma: Dark Arisen", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3123, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3124, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3125, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "store_steampowered_com", - "genres": [ - "Free to Play", - "Indie" - ] - }, - { - "id": 3126, - "name": "Five Nights At Freddy's 2", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Аркада", - "От первого лица", - "Инди" - ] - }, - { - "id": 3127, - "name": "Guns, Gore & Cannoli", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up", - "платформеры" - ] - }, - { - "id": 3128, - "name": "House Flipper", - "site": "igromania_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 3129, - "name": "Homefront", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3130, - "name": "Draw a Stickman: Episode 1", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3131, - "name": "Dark Souls: Remastered", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG", - "Ремастер" - ] - }, - { - "id": 3132, - "name": "Grand Theft Auto III", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 3133, - "name": "Final Fantasy XV", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "РПГ" - ] - }, - { - "id": 3134, - "name": "FarSky", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3135, - "name": "Doom 3", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3136, - "name": "Final Fantasy XIII", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 3137, - "name": "Dragon Age II", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3138, - "name": "Hotel Remorse", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3139, - "name": "Five Nights At Freddy's 3", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Аркада", - "От первого лица", - "Инди" - ] - }, - { - "id": 3140, - "name": "Guns, Gore & Cannoli 2", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up", - "платформеры" - ] - }, - { - "id": 3141, - "name": "I Am Alive", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 3142, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3143, - "name": "Draw a Stickman: Episode 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3144, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3145, - "name": "Grand Theft Auto: San Andreas", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 3146, - "name": "Final Fantasy III (Remake)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3147, - "name": "Final Fantasy XIII-2", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 3148, - "name": "Hotline Miami", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3149, - "name": "Darksiders", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3150, - "name": "I am Setsuna", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 3151, - "name": "Dragon Age: Inquisition", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3152, - "name": "Half-Life", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3153, - "name": "Five Nights At Freddy's 4", - "site": "playground_ru", - "genres": [ - "Ужасы", - "От первого лица", - "Инди" - ] - }, - { - "id": 3154, - "name": "Double Dragon Neon", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3155, - "name": "Grand Theft Auto: Vice City", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3156, - "name": "DreadOut", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 3157, - "name": "Darksiders II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3158, - "name": "Hotline Miami 2: Wrong Number", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3159, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3160, - "name": "Final Fantasy IV (Remake)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3161, - "name": "I hate this game", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3162, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3163, - "name": "Half-Life 2", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3164, - "name": "Grandpa", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3165, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3166, - "name": "Five Nights at Freddy's 3D", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3167, - "name": "Darksiders III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3168, - "name": "Down Stairs", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3169, - "name": "House Flipper", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3170, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3171, - "name": "Grey", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3172, - "name": "INSIDE", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 3173, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3174, - "name": "Daylight", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 3175, - "name": "Half-Life 2: Episode One", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3176, - "name": "Five Nights in Anime v3", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3177, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3178, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3179, - "name": "I Am Alive", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3180, - "name": "Dropsy", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3181, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3182, - "name": "Downfall", - "site": "mobygames_com", - "genres": [ - "Puzzle", - "Strategy / tactics" - ] - }, - { - "id": 3183, - "name": "Indivisible", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 3184, - "name": "Grim Fandango Remastered", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3185, - "name": "I am Setsuna", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3186, - "name": "Dead Island", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3187, - "name": "Half-Life 2: Episode Two", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3188, - "name": "Fran Bow", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "Платформер" - ] - }, - { - "id": 3189, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3190, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3191, - "name": "Final Fantasy X HD Remaster", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3192, - "name": "Injustice: Gods Among Us", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 3193, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3194, - "name": "Dead Island: Riptide", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3195, - "name": "I hate this game", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3196, - "name": "Half-Life: Blue Shift", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3197, - "name": "Grow Home", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Приключения" - ] - }, - { - "id": 3198, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3199, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3200, - "name": "Dude, Stop", - "site": "gamespot_com", - "genres": [ - "Party/Minigame" - ] - }, - { - "id": 3201, - "name": "Front Mission Evolved", - "site": "playground_ru", - "genres": [ - "Экшен", - "Симулятор" - ] - }, - { - "id": 3202, - "name": "Final Fantasy XV", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 3203, - "name": "Dead Island: Ryder White", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3204, - "name": "INSIDE", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3205, - "name": "Into the Breach", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Симулятор", - "Ролевая игра" - ] - }, - { - "id": 3206, - "name": "Firewatch", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 3207, - "name": "Half-Life: Opposing Force", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3208, - "name": "Dragon Age: Origins", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3209, - "name": "Grow Up", - "site": "gamebomb_ru", - "genres": [ - "Логические игры", - "Платформер" - ] - }, - { - "id": 3210, - "name": "Dead Rising", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3211, - "name": "Indivisible", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3212, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3213, - "name": "Final Fantasy XIII", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3214, - "name": "Jade Empire", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 3215, - "name": "GUN", - "site": "playground_ru", - "genres": [ - "Экшен", - "Вестерн", - "От третьего лица", - "От первого лица" - ] - }, - { - "id": 3216, - "name": "Dragon Age II", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3217, - "name": "Guns, Gore & Cannoli", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3218, - "name": "Duke Nukem Forever", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3219, - "name": "First Winter", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3220, - "name": "Half-Life: Paranoia", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3221, - "name": "Dead Space", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3222, - "name": "Injustice: Gods Among Us", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3223, - "name": "Guns, Gore & Cannoli 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3224, - "name": "Jotun", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3225, - "name": "Dragon Age: Origins - Awakening", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3226, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3227, - "name": "Final Fantasy XIII-2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3228, - "name": "Gemini Rue", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Квест" - ] - }, - { - "id": 3229, - "name": "Dead Space 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3230, - "name": "Into the Breach", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3231, - "name": "Five Minutes", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3232, - "name": "Half-Life: The Xeno Project", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3233, - "name": "Just Cause", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3234, - "name": "Dragon Age: Inquisition", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3235, - "name": "Half-Life", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 3236, - "name": "Dead Space 3", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3237, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 3238, - "name": "Jade Empire", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3239, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3240, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3241, - "name": "Geometry Dash", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 3242, - "name": "Dungeon Nightmares", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 3243, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3244, - "name": "Five Nights At Freddy's", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3245, - "name": "Just Cause 2", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3246, - "name": "Dead Space 3: Awakened (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3247, - "name": "Half-Life 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 3248, - "name": "Jotun", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3249, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3250, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 3251, - "name": "Halo: Combat Evolved", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3252, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3253, - "name": "Ghost of a Tale", - "site": "playground_ru", - "genres": [ - "Инди", - "От третьего лица", - "Адвенчура", - "Ролевая", - "Экшен", - "Фэнтези" - ] - }, - { - "id": 3254, - "name": "Kane & Lynch 2: Dog Days", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3255, - "name": "Five Nights At Freddy's 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3256, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3257, - "name": "Dying Light", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 3258, - "name": "Dead by Daylight", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3259, - "name": "Just Cause", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3260, - "name": "Half-Life 2: Episode One", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3261, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Приключения", - "Экшены" - ] - }, - { - "id": 3262, - "name": "Hand of Fate", - "site": "gamer_info_com", - "genres": [ - "инди", - "RPG", - "карточные", - "action" - ] - }, - { - "id": 3263, - "name": "Deadlight", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада" - ] - }, - { - "id": 3264, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3265, - "name": "Dying Light: The Following (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3266, - "name": "Five Nights At Freddy's 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3267, - "name": "Killer is Dead: Nightmare Edition", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 3268, - "name": "Goat Simulator", - "site": "playground_ru", - "genres": [ - "Юмор", - "Инди", - "Песочница", - "Экшен", - "Открытый мир" - ] - }, - { - "id": 3269, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3270, - "name": "Just Cause 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3271, - "name": "Half-Life 2: Episode Two", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3272, - "name": "Deadpool", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3273, - "name": "Firewatch", - "site": "ag_ru", - "genres": [ - "Инди", - "Симуляторы" - ] - }, - { - "id": 3274, - "name": "Kane & Lynch 2: Dog Days", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3275, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3276, - "name": "Hand of Fate 2", - "site": "gamer_info_com", - "genres": [ - "инди", - "RPG", - "карточные", - "action" - ] - }, - { - "id": 3277, - "name": "King's Bounty: Armored Princess", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3278, - "name": "Grand Theft Auto III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3279, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3280, - "name": "Five Nights At Freddy's 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3281, - "name": "Emily Wants To Play", - "site": "gamespot_com", - "genres": [ - "VR", - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 3282, - "name": "Final Fantasy XV", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 3283, - "name": "DeathSpank", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3284, - "name": "Half-Life: Blue Shift", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3285, - "name": "First Winter", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 3286, - "name": "Killer is Dead: Nightmare Edition", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3287, - "name": "King's Bounty: Crossworlds", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3288, - "name": "Happy Room", - "site": "gamer_info_com", - "genres": [ - "инди", - "аркадные" - ] - }, - { - "id": 3289, - "name": "DeathSpank: Thongs of Virtue", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Гонки/вождение" - ] - }, - { - "id": 3290, - "name": "Grand Theft Auto: San Andreas", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "От третьего лица", - "Песочница" - ] - }, - { - "id": 3291, - "name": "Five Nights at Freddy's 3D", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3292, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3293, - "name": "King's Bounty: Armored Princess", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3294, - "name": "Escape Dead Island", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 3295, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3296, - "name": "Half-Life: Opposing Force", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3297, - "name": "King's Bounty: Dark Side", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3298, - "name": "Dragon's Dogma: Dark Arisen", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Adventure" - ] - }, - { - "id": 3299, - "name": "Deathtrap", - "site": "gameguru_ru", - "genres": [ - "В реальном времени", - "Стратегия" - ] - }, - { - "id": 3300, - "name": "Five Minutes", - "site": "ag_ru", - "genres": [ - "Головоломки" - ] - }, - { - "id": 3301, - "name": "Happy Wheels", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3302, - "name": "King's Bounty: Crossworlds", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3303, - "name": "Five Nights in Anime v3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3304, - "name": "Grand Theft Auto: Vice City", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "От третьего лица", - "Песочница" - ] - }, - { - "id": 3305, - "name": "Half-Life: Paranoia", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3306, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3307, - "name": "Demigod", - "site": "gameguru_ru", - "genres": [ - "RPG", - "Стратегия" - ] - }, - { - "id": 3308, - "name": "King's Bounty: The Legend", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3309, - "name": "Five Nights At Freddy's", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 3310, - "name": "Hard Reset", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3311, - "name": "King's Bounty: Dark Side", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3312, - "name": "Draw a Stickman: Episode 1", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3313, - "name": "Half-Life: The Xeno Project", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3314, - "name": "Descenders", - "site": "gameguru_ru", - "genres": [ - "Спорт", - "Велоспорт" - ] - }, - { - "id": 3315, - "name": "Grandpa", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3316, - "name": "King's Bounty: Warriors of the North", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3317, - "name": "Fran Bow", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3318, - "name": "Evil Defenders", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 3319, - "name": "Dragon Age: Origins", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3320, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3321, - "name": "King's Bounty: The Legend", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3322, - "name": "Detention", - "site": "gameguru_ru", - "genres": [ - "Point-and-click", - "Квест" - ] - }, - { - "id": 3323, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3324, - "name": "Hard Reset: Exile (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3325, - "name": "Grey", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3326, - "name": "Five Nights At Freddy's 2", - "site": "ag_ru", - "genres": [ - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 3327, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3328, - "name": "Draw a Stickman: Episode 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3329, - "name": "Front Mission Evolved", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3330, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3331, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3332, - "name": "King's Bounty: Warriors of the North", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3333, - "name": "Grim Fandango Remastered", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3334, - "name": "Harts", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3335, - "name": "Halo: Combat Evolved", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3336, - "name": "Dragon Age: Origins - Awakening", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3337, - "name": "Kingdom Rush", - "site": "igromania_ru", - "genres": [ - "Стратегия в реальном времени", - "Стратегия" - ] - }, - { - "id": 3338, - "name": "Five Nights At Freddy's 3", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы" - ] - }, - { - "id": 3339, - "name": "Devil May Cry 4", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3340, - "name": "Evoland", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 3341, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3342, - "name": "GUN", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3343, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3344, - "name": "Hatred", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up" - ] - }, - { - "id": 3345, - "name": "Hand of Fate", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 3346, - "name": "DreadOut", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 3347, - "name": "Devil May Cry 5", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Слэшер" - ] - }, - { - "id": 3348, - "name": "Grow Home", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 3349, - "name": "Kingdom: Classic", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Симулятор", - "Боевик" - ] - }, - { - "id": 3350, - "name": "Kingdom Rush", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3351, - "name": "Five Nights At Freddy's 4", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Стратегии" - ] - }, - { - "id": 3352, - "name": "Evoland 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3353, - "name": "Gemini Rue", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3354, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3355, - "name": "Firewatch", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 3356, - "name": "Diablo II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3357, - "name": "Hand of Fate 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Карточные игры", - "Ролевые игры" - ] - }, - { - "id": 3358, - "name": "Hellblade: Senua's Sacrifice", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 3359, - "name": "Kingdom: Classic", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3360, - "name": "Kingdoms of Amalur: Reckoning", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 3361, - "name": "Grow Up", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Открытый мир", - "Адвенчура" - ] - }, - { - "id": 3362, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3363, - "name": "Happy Room", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3364, - "name": "Dropsy", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 3365, - "name": "Geometry Dash", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3366, - "name": "Kingdoms of Amalur: Reckoning", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3367, - "name": "Five Nights at Freddy's 3D", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3368, - "name": "Knock-Knock", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3369, - "name": "First Winter", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3370, - "name": "Hellgate: London", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 3371, - "name": "Diablo III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3372, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3373, - "name": "Happy Wheels", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3374, - "name": "Guns, Gore & Cannoli", - "site": "playground_ru", - "genres": [ - "Экшен", - "Инди", - "Шутер", - "Платформер" - ] - }, - { - "id": 3375, - "name": "Eyes The Horror Game", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3376, - "name": "Knock-Knock", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3377, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 3378, - "name": "Five Nights in Anime v3", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3379, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3380, - "name": "Ghost of a Tale", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3381, - "name": "Five Minutes", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3382, - "name": "Her Story", - "site": "gamer_info_com", - "genres": [ - "инди" - ] - }, - { - "id": 3383, - "name": "Dude, Stop", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 3384, - "name": "Hard Reset", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 3385, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3386, - "name": "Guns, Gore & Cannoli 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "Инди", - "Кооператив" - ] - }, - { - "id": 3387, - "name": "Dig or Die", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Платформер", - "Инди" - ] - }, - { - "id": 3388, - "name": "Lara Croft and the Guardian of Light", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3389, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3390, - "name": "Goat Simulator", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3391, - "name": "Hero Academy", - "site": "gamer_info_com", - "genres": [ - "тактика", - "TBS" - ] - }, - { - "id": 3392, - "name": "Fran Bow", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 3393, - "name": "Five Nights At Freddy's", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3394, - "name": "F.E.A.R.", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3395, - "name": "Hard Reset: Exile (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3396, - "name": "Lara Croft and the Guardian of Light", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3397, - "name": "Disciples II: Dark Prophecy", - "site": "gameguru_ru", - "genres": [ - "В пошаговом режиме", - "Стратегия" - ] - }, - { - "id": 3398, - "name": "Lara Croft and the Temple of Osiris", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Приключение" - ] - }, - { - "id": 3399, - "name": "Half-Life", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 3400, - "name": "Duke Nukem Forever", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 3401, - "name": "Harts", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3402, - "name": "Heroes of Might and Magic III", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3403, - "name": "Disciples III: Renaissance", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3404, - "name": "Grand Theft Auto III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3405, - "name": "Lara Croft and the Temple of Osiris", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3406, - "name": "Five Nights At Freddy's 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3407, - "name": "Front Mission Evolved", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3408, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3409, - "name": "Late Shift", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3410, - "name": "Half-Life 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 3411, - "name": "Dishonored", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3412, - "name": "F.E.A.R. 2: Project Origin", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3413, - "name": "Late Shift", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3414, - "name": "Dungeon Nightmares", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3415, - "name": "Heroes of Might and Magic IV", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3416, - "name": "Hatred", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 3417, - "name": "Grand Theft Auto: San Andreas", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3418, - "name": "GUN", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 3419, - "name": "Layers of Fear", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3420, - "name": "Five Nights At Freddy's 3", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3421, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3422, - "name": "F.E.A.R. 2: Reborn", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3423, - "name": "Dragon's Dogma: Dark Arisen", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 3424, - "name": "Layers of Fear", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3425, - "name": "Heroes of Might and Magic V", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3426, - "name": "Half-Life 2: Episode One", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3427, - "name": "Hellblade: Senua's Sacrifice", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 3428, - "name": "Grand Theft Auto: Vice City", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3429, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3430, - "name": "Gemini Rue", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3431, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3432, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3433, - "name": "Five Nights At Freddy's 4", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3434, - "name": "Dying Light", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3435, - "name": "Distraint", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3436, - "name": "F.E.A.R. 3", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3437, - "name": "Draw a Stickman: Episode 1", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3438, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3439, - "name": "Hellgate: London", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "ММОРПГ/Многопользоватеские онлайновые", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 3440, - "name": "Half-Life 2: Episode Two", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3441, - "name": "Left 4 Dead", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3442, - "name": "Left 4 Dead", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 3443, - "name": "Geometry Dash", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Аркадные" - ] - }, - { - "id": 3444, - "name": "Grandpa", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3445, - "name": "Disturbed", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3446, - "name": "Dying Light: The Following (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3447, - "name": "Five Nights at Freddy's 3D", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3448, - "name": "F.E.A.R. Extraction Point", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3449, - "name": "Left 4 Dead 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3450, - "name": "Draw a Stickman: Episode 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3451, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3452, - "name": "Left 4 Dead 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 3453, - "name": "Her Story", - "site": "gamebomb_ru", - "genres": [ - "Симулятор", - "Головоломки/Пазл" - ] - }, - { - "id": 3454, - "name": "Ghost of a Tale", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 3455, - "name": "Half-Life: Blue Shift", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3456, - "name": "DmC: Devil May Cry", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3457, - "name": "Grey", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3458, - "name": "Legend Hand of God", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3459, - "name": "Homefront", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3460, - "name": "Five Nights in Anime v3", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3461, - "name": "Legend Hand of God", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3462, - "name": "F.E.A.R. Perseus Mandate", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3463, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 3464, - "name": "Hero Academy", - "site": "gamebomb_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 3465, - "name": "DreadOut", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3466, - "name": "Goat Simulator", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Инди", - "Симуляторы" - ] - }, - { - "id": 3467, - "name": "Grim Fandango Remastered", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Приключения" - ] - }, - { - "id": 3468, - "name": "Emily Wants To Play", - "site": "store_steampowered_com", - "genres": [ - "Strategy", - "Casual", - "Action", - "Indie", - "Adventure", - "Simulation" - ] - }, - { - "id": 3469, - "name": "Legend of Grimrock", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3470, - "name": "Half-Life: Opposing Force", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3471, - "name": "Doki Doki Literature Club", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3472, - "name": "Fable (2004)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3473, - "name": "Legend of Grimrock", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 3474, - "name": "Hotel Remorse", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3475, - "name": "Heroes of Might and Magic III", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3476, - "name": "Fran Bow", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 3477, - "name": "Legendary", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3478, - "name": "Don't Knock Twice", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3479, - "name": "Half-Life: Paranoia", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3480, - "name": "Grow Home", - "site": "iwantgames_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3481, - "name": "Dropsy", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3482, - "name": "Grand Theft Auto III", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 3483, - "name": "Life After Us: Shipwrecked", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3484, - "name": "Hotline Miami", - "site": "gamer_info_com", - "genres": [ - "инди", - "action" - ] - }, - { - "id": 3485, - "name": "Heroes of Might and Magic IV", - "site": "gamebomb_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 3486, - "name": "Legendary", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 3487, - "name": "Front Mission Evolved", - "site": "stopgame_ru", - "genres": [ - "simulator", - "action" - ] - }, - { - "id": 3488, - "name": "Escape Dead Island", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 3489, - "name": "Fable III", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 3490, - "name": "Doom 3", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3491, - "name": "Half-Life: The Xeno Project", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3492, - "name": "Grow Up", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3493, - "name": "Life Goes On: Done to Death", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3494, - "name": "Dude, Stop", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3495, - "name": "Grand Theft Auto: San Andreas", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 3496, - "name": "Double Dragon Neon", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 3497, - "name": "Hotline Miami 2: Wrong Number", - "site": "gamer_info_com", - "genres": [ - "инди", - "action" - ] - }, - { - "id": 3498, - "name": "Life After Us: Shipwrecked", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3499, - "name": "Heroes of Might and Magic V", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 3500, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3501, - "name": "GUN", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3502, - "name": "Life Is Strange", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3503, - "name": "Down Stairs", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3504, - "name": "Life Goes On: Done to Death", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Логика" - ] - }, - { - "id": 3505, - "name": "Guns, Gore & Cannoli", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3506, - "name": "Evil Defenders", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Strategy", - "Indie" - ] - }, - { - "id": 3507, - "name": "Fahrenheit", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3508, - "name": "House Flipper", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3509, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 3510, - "name": "Grand Theft Auto: Vice City", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3511, - "name": "Limbo", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3512, - "name": "Downfall", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 3513, - "name": "Duke Nukem Forever", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3514, - "name": "Halo: Combat Evolved", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 3515, - "name": "Life Is Strange", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3516, - "name": "Gemini Rue", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 3517, - "name": "I Am Alive", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3518, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 3519, - "name": "Guns, Gore & Cannoli 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3520, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 3521, - "name": "Little Nightmares", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3522, - "name": "Grandpa", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 3523, - "name": "Dungeon Nightmares", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3524, - "name": "Failman", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3525, - "name": "Evoland", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 3526, - "name": "Limbo", - "site": "igromania_ru", - "genres": [ - "Платформер" - ] - }, - { - "id": 3527, - "name": "Hand of Fate", - "site": "playground_ru", - "genres": [ - "Экшен", - "Карточная", - "Мультиплеер", - "Ролевая" - ] - }, - { - "id": 3528, - "name": "Geometry Dash", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 3529, - "name": "I am Setsuna", - "site": "gamer_info_com", - "genres": [ - "jRPG" - ] - }, - { - "id": 3530, - "name": "Homefront", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 3531, - "name": "Dragon Age II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3532, - "name": "Little Nightmares - The Depths (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3533, - "name": "Half-Life", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3534, - "name": "Grey", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3535, - "name": "Little Nightmares", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3536, - "name": "Dying Light", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3537, - "name": "Hotel Remorse", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3538, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3539, - "name": "Dragon Age: Inquisition", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 3540, - "name": "Fallout 3", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 3541, - "name": "Hand of Fate 2", - "site": "playground_ru", - "genres": [ - "Инди", - "Карточная", - "Адвенчура", - "Стратегия" - ] - }, - { - "id": 3542, - "name": "Ghost of a Tale", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 3543, - "name": "I hate this game", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3544, - "name": "Half-Life 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3545, - "name": "Evoland 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 3546, - "name": "Grim Fandango Remastered", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 3547, - "name": "Little Nightmares - The Depths (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3548, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "RPG" - ] - }, - { - "id": 3549, - "name": "Loki: Heroes of Mythology", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3550, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3551, - "name": "Hotline Miami", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 3552, - "name": "Happy Room", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3553, - "name": "INSIDE", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "платформеры" - ] - }, - { - "id": 3554, - "name": "Goat Simulator", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3555, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3556, - "name": "Half-Life 2: Episode One", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3557, - "name": "Dying Light: The Following (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3558, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3559, - "name": "Lost Planet: Colonies", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3560, - "name": "Eyes The Horror Game", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3561, - "name": "Grow Home", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Приключения" - ] - }, - { - "id": 3562, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3563, - "name": "Happy Wheels", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3564, - "name": "Hotline Miami 2: Wrong Number", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 3565, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG", - "Аддон" - ] - }, - { - "id": 3566, - "name": "Indivisible", - "site": "gamer_info_com", - "genres": [ - "RPG", - "платформеры" - ] - }, - { - "id": 3567, - "name": "Lost: Via Domus", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3568, - "name": "Grand Theft Auto III", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3569, - "name": "Loki: Heroes of Mythology", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 3570, - "name": "Half-Life 2: Episode Two", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3571, - "name": "Grow Up", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Инди", - "Приключения" - ] - }, - { - "id": 3572, - "name": "Emily Wants To Play", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3573, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3574, - "name": "Dragon Age: Origins", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 3575, - "name": "Hard Reset", - "site": "playground_ru", - "genres": [ - "Экшен", - "Шутер", - "Киберпанк" - ] - }, - { - "id": 3576, - "name": "Love Is Strange (DEMO)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3577, - "name": "House Flipper", - "site": "gamebomb_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 3578, - "name": "Injustice: Gods Among Us", - "site": "gamer_info_com", - "genres": [ - "файтинги" - ] - }, - { - "id": 3579, - "name": "F.E.A.R.", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 3580, - "name": "Lost Planet: Colonies", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3581, - "name": "Dragon Age: Origins - Awakening", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 3582, - "name": "Grand Theft Auto: San Andreas", - "site": "stopgame_ru", - "genres": [ - "arcade", - "action", - "racing" - ] - }, - { - "id": 3583, - "name": "Half-Life: Blue Shift", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3584, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3585, - "name": "Guns, Gore & Cannoli", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Инди", - "Экшены", - "Аркадные" - ] - }, - { - "id": 3586, - "name": "METAL SLUG", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3587, - "name": "Hard Reset: Exile (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3588, - "name": "Escape Dead Island", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3589, - "name": "Into the Breach", - "site": "gamer_info_com", - "genres": [ - "инди", - "TBS" - ] - }, - { - "id": 3590, - "name": "Lost: Via Domus", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3591, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3592, - "name": "F.E.A.R. 2: Project Origin", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3593, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3594, - "name": "METAL SLUG 3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3595, - "name": "Half-Life: Opposing Force", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3596, - "name": "Guns, Gore & Cannoli 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3597, - "name": "Grand Theft Auto: Vice City", - "site": "stopgame_ru", - "genres": [ - "arcade", - "action", - "racing" - ] - }, - { - "id": 3598, - "name": "Harts", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3599, - "name": "I Am Alive", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 3600, - "name": "Love Is Strange (DEMO)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3601, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3602, - "name": "Jade Empire", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3603, - "name": "Evil Defenders", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 3604, - "name": "MINERVA: Metastasis", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3605, - "name": "F.E.A.R. 2: Reborn", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3606, - "name": "Half-Life", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3607, - "name": "Half-Life: Paranoia", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3608, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3609, - "name": "Grandpa", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3610, - "name": "Fallout 4", - "site": "gamespot_com", - "genres": [ - "VR", - "Role-Playing" - ] - }, - { - "id": 3611, - "name": "I am Setsuna", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 3612, - "name": "Hatred", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Вид сверху" - ] - }, - { - "id": 3613, - "name": "METAL SLUG", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Боевик" - ] - }, - { - "id": 3614, - "name": "MOTHERGUNSHIP", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3615, - "name": "Jotun", - "site": "gamer_info_com", - "genres": [ - "инди", - "приключения" - ] - }, - { - "id": 3616, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3617, - "name": "I hate this game", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3618, - "name": "Half-Life 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3619, - "name": "Machinarium", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3620, - "name": "Half-Life: The Xeno Project", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3621, - "name": "METAL SLUG 3", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Боевик" - ] - }, - { - "id": 3622, - "name": "Evoland", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3623, - "name": "Grey", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 3624, - "name": "F.E.A.R. 3", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 3625, - "name": "Just Cause", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 3626, - "name": "Hellblade: Senua's Sacrifice", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Фэнтези" - ] - }, - { - "id": 3627, - "name": "Dragon's Dogma: Dark Arisen", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 3628, - "name": "Fallout: New Vegas", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 3629, - "name": "Magibot", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3630, - "name": "INSIDE", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 3631, - "name": "MINERVA: Metastasis", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3632, - "name": "Half-Life 2: Episode One", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3633, - "name": "Evoland 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3634, - "name": "Draw a Stickman: Episode 1", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3635, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3636, - "name": "Just Cause 2", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 3637, - "name": "F.E.A.R. Extraction Point", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3638, - "name": "Family Trouble", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3639, - "name": "Grim Fandango Remastered", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 3640, - "name": "Hellgate: London", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Ролевая", - "Экшен", - "От первого лица", - "Фэнтези" - ] - }, - { - "id": 3641, - "name": "Magicka", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3642, - "name": "MOTHERGUNSHIP", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 3643, - "name": "Draw a Stickman: Episode 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3644, - "name": "Indivisible", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 3645, - "name": "Half-Life 2: Episode Two", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3646, - "name": "Halo: Combat Evolved", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3647, - "name": "Kane & Lynch 2: Dog Days", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 3648, - "name": "Magicka 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3649, - "name": "F.E.A.R. Perseus Mandate", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3650, - "name": "DreadOut", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди" - ] - }, - { - "id": 3651, - "name": "Far Cry", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3652, - "name": "Grow Home", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 3653, - "name": "Eyes The Horror Game", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3654, - "name": "Machinarium", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3655, - "name": "Her Story", - "site": "playground_ru", - "genres": [ - "Другой симулятор", - "Адвенчура", - "Инди" - ] - }, - { - "id": 3656, - "name": "Dropsy", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3657, - "name": "Manual Samuel", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3658, - "name": "Half-Life: Blue Shift", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 3659, - "name": "Hand of Fate", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3660, - "name": "Killer is Dead: Nightmare Edition", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3661, - "name": "Fable (2004)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3662, - "name": "Injustice: Gods Among Us", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 3663, - "name": "Grow Up", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3664, - "name": "Dude, Stop", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3665, - "name": "Magibot", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3666, - "name": "F.E.A.R.", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3667, - "name": "Mario.EXE", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3668, - "name": "Hero Academy", - "site": "playground_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 3669, - "name": "Far Cry 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3670, - "name": "Half-Life: Opposing Force", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 3671, - "name": "King's Bounty: Armored Princess", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3672, - "name": "Hand of Fate 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3673, - "name": "Guns, Gore & Cannoli", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3674, - "name": "Duke Nukem Forever", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3675, - "name": "Into the Breach", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Симулятор", - "Ролевые игры" - ] - }, - { - "id": 3676, - "name": "Fable III", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3677, - "name": "Marvel vs. Capcom: Infinite", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3678, - "name": "Magicka", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Боевик" - ] - }, - { - "id": 3679, - "name": "Heroes of Might and Magic III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3680, - "name": "King's Bounty: Crossworlds", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3681, - "name": "Half-Life: Paranoia", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3682, - "name": "Happy Room", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3683, - "name": "Far Cry 3: Blood Dragon", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 3684, - "name": "Guns, Gore & Cannoli 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3685, - "name": "Dungeon Nightmares", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3686, - "name": "Masochisia", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3687, - "name": "Jade Empire", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 3688, - "name": "Fahrenheit", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3689, - "name": "Magicka 2", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 3690, - "name": "Heroes of Might and Magic IV", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3691, - "name": "Dying Light", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Шутер" - ] - }, - { - "id": 3692, - "name": "King's Bounty: Dark Side", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3693, - "name": "Half-Life: The Xeno Project", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3694, - "name": "F.E.A.R. 2: Project Origin", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3695, - "name": "Masters of Anima", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3696, - "name": "Happy Wheels", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3697, - "name": "Half-Life", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3698, - "name": "Manual Samuel", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3699, - "name": "Jotun", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 3700, - "name": "Heroes of Might and Magic V", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3701, - "name": "Dying Light: The Following (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Аддон" - ] - }, - { - "id": 3702, - "name": "Failman", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3703, - "name": "King's Bounty: The Legend", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3704, - "name": "Max Payne", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3705, - "name": "FarSky", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 3706, - "name": "Hard Reset", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3707, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3708, - "name": "Mario.EXE", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3709, - "name": "Emily Wants To Play", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 3710, - "name": "Half-Life 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3711, - "name": "Just Cause", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг", - "Битвы машин", - "Боевик/Экшн" - ] - }, - { - "id": 3712, - "name": "F.E.A.R. 2: Reborn", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3713, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3714, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3715, - "name": "King's Bounty: Warriors of the North", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3716, - "name": "Final Fantasy III (Remake)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3717, - "name": "Escape Dead Island", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3718, - "name": "Marvel vs. Capcom: Infinite", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 3719, - "name": "Hard Reset: Exile (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3720, - "name": "Fallout 3", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3721, - "name": "Halo: Combat Evolved", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 3722, - "name": "Just Cause 2", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг", - "Битвы машин", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 3723, - "name": "Half-Life 2: Episode One", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3724, - "name": "F.E.A.R. 3", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3725, - "name": "Max Payne 3", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3726, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3727, - "name": "Evil Defenders", - "site": "gameguru_ru", - "genres": [ - "В реальном времени", - "Стратегия" - ] - }, - { - "id": 3728, - "name": "Final Fantasy IV (Remake)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3729, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3730, - "name": "Masochisia", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 3731, - "name": "Kane & Lynch 2: Dog Days", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3732, - "name": "Harts", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3733, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3734, - "name": "Max: The Curse Of Brotherhood", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3735, - "name": "Evoland", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 3736, - "name": "Hand of Fate", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 3737, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3738, - "name": "Half-Life 2: Episode Two", - "site": "stopgame_ru", - "genres": [ - "logic", - "action" - ] - }, - { - "id": 3739, - "name": "Homefront", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 3740, - "name": "Kingdom Rush", - "site": "gamer_info_com", - "genres": [ - "инди", - "tower defense" - ] - }, - { - "id": 3741, - "name": "Masters of Anima", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Приключение" - ] - }, - { - "id": 3742, - "name": "Killer is Dead: Nightmare Edition", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3743, - "name": "Evoland 2", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 3744, - "name": "McPixel", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3745, - "name": "F.E.A.R. Extraction Point", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3746, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3747, - "name": "Hatred", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 3748, - "name": "Hand of Fate 2", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди" - ] - }, - { - "id": 3749, - "name": "Final Fantasy X HD Remaster", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3750, - "name": "Hotel Remorse", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3751, - "name": "Max Payne", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3752, - "name": "Eyes The Horror Game", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3753, - "name": "Kingdom: Classic", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3754, - "name": "Half-Life: Blue Shift", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 3755, - "name": "Metal Gear Rising: Revengeance", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3756, - "name": "King's Bounty: Armored Princess", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 3757, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3758, - "name": "Happy Room", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы" - ] - }, - { - "id": 3759, - "name": "Hellblade: Senua's Sacrifice", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3760, - "name": "F.E.A.R.", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3761, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3762, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3763, - "name": "Metal Wolf Chaos XD", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3764, - "name": "F.E.A.R. Perseus Mandate", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 3765, - "name": "Hotline Miami", - "site": "playground_ru", - "genres": [ - "Вид сверху", - "Аркада", - "Ретро", - "Шутер", - "Экшен" - ] - }, - { - "id": 3766, - "name": "Kingdoms of Amalur: Reckoning", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3767, - "name": "King's Bounty: Crossworlds", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 3768, - "name": "Half-Life: Opposing Force", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 3769, - "name": "F.E.A.R. 2: Project Origin", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3770, - "name": "Metro 2033", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3771, - "name": "Max Payne 3", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3772, - "name": "Happy Wheels", - "site": "ag_ru", - "genres": [ - "Экшены", - "Гонки" - ] - }, - { - "id": 3773, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3774, - "name": "Hellgate: London", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3775, - "name": "Fable (2004)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3776, - "name": "Final Fantasy XIII", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 3777, - "name": "Knock-Knock", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3778, - "name": "Hotline Miami 2: Wrong Number", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "Ретро", - "Вид сверху" - ] - }, - { - "id": 3779, - "name": "King's Bounty: Dark Side", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 3780, - "name": "F.E.A.R. 2: Reborn", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3781, - "name": "Metro 2033 Redux", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3782, - "name": "Half-Life: Paranoia", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3783, - "name": "Max: The Curse Of Brotherhood", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Логика" - ] - }, - { - "id": 3784, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3785, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3786, - "name": "Her Story", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3787, - "name": "F.E.A.R. 3", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3788, - "name": "Hard Reset", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3789, - "name": "House Flipper", - "site": "playground_ru", - "genres": [ - "Другой симулятор", - "Строительство", - "От первого лица", - "Симулятор" - ] - }, - { - "id": 3790, - "name": "Metro Exodus", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3791, - "name": "King's Bounty: The Legend", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 3792, - "name": "Final Fantasy XIII-2", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 3793, - "name": "McPixel", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 3794, - "name": "Fable III", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3795, - "name": "King's Bounty: Warriors of the North", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3796, - "name": "Half-Life: The Xeno Project", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3797, - "name": "Metro Last Light", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3798, - "name": "Lara Croft and the Guardian of Light", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up" - ] - }, - { - "id": 3799, - "name": "Hero Academy", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3800, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3801, - "name": "F.E.A.R. Extraction Point", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3802, - "name": "I Am Alive", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Постапокалипсис", - "Выживание", - "Шутер", - "Экшен", - "Открытый мир", - "Платформер" - ] - }, - { - "id": 3803, - "name": "Metal Gear Rising: Revengeance", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 3804, - "name": "Fahrenheit", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3805, - "name": "Hard Reset: Exile (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3806, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3807, - "name": "Metro Last Light Redux", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3808, - "name": "Fallout 4", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3809, - "name": "F.E.A.R. Perseus Mandate", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3810, - "name": "Lara Croft and the Temple of Osiris", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up" - ] - }, - { - "id": 3811, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3812, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3813, - "name": "I am Setsuna", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3814, - "name": "Heroes of Might and Magic III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3815, - "name": "Metal Wolf Chaos XD", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3816, - "name": "Kingdom Rush", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3817, - "name": "Failman", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3818, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3819, - "name": "Harts", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 3820, - "name": "Fable (2004)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3821, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3822, - "name": "Late Shift", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 3823, - "name": "I hate this game", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3824, - "name": "Halo: Combat Evolved", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3825, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3826, - "name": "Metro 2033", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 3827, - "name": "Fallout: New Vegas", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3828, - "name": "Kingdom: Classic", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Симулятор", - "Приключения" - ] - }, - { - "id": 3829, - "name": "Fable III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3830, - "name": "Heroes of Might and Magic IV", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3831, - "name": "Hatred", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Инди", - "Экшены" - ] - }, - { - "id": 3832, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3833, - "name": "Layers of Fear", - "site": "gamer_info_com", - "genres": [ - "ужасы" - ] - }, - { - "id": 3834, - "name": "Metro 2033 Redux", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3835, - "name": "Fahrenheit", - "site": "gameguru_ru", - "genres": [ - "Интерактивный фильм", - "Квест" - ] - }, - { - "id": 3836, - "name": "Fallout 3", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3837, - "name": "INSIDE", - "site": "playground_ru", - "genres": [ - "Аркада", - "Адвенчура", - "Инди", - "Платформер" - ] - }, - { - "id": 3838, - "name": "Final Fantasy XV", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 3839, - "name": "Hand of Fate", - "site": "stopgame_ru", - "genres": [ - "logic", - "rpg" - ] - }, - { - "id": 3840, - "name": "Family Trouble", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3841, - "name": "Heroes of Might and Magic V", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3842, - "name": "Hellblade: Senua's Sacrifice", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 3843, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3844, - "name": "Failman", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3845, - "name": "Kingdoms of Amalur: Reckoning", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 3846, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3847, - "name": "Metro Exodus", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 3848, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3849, - "name": "Indivisible", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая", - "Платформер" - ] - }, - { - "id": 3850, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3851, - "name": "Middle-earth: Shadow of Mordor", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3852, - "name": "Fallout 3", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 3853, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3854, - "name": "Hand of Fate 2", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 3855, - "name": "Hellgate: London", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Экшены" - ] - }, - { - "id": 3856, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3857, - "name": "Metro Last Light", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3858, - "name": "Left 4 Dead", - "site": "gamer_info_com", - "genres": [ - "FPS", - "action" - ] - }, - { - "id": 3859, - "name": "Far Cry", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 3860, - "name": "Knock-Knock", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3861, - "name": "Might & Magic Heroes VI", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3862, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "RPG", - "aRPG" - ] - }, - { - "id": 3863, - "name": "Injustice: Gods Among Us", - "site": "playground_ru", - "genres": [ - "Аркада", - "Условно-бесплатная", - "Файтинг" - ] - }, - { - "id": 3864, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3865, - "name": "Metro Last Light Redux", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3866, - "name": "Happy Room", - "site": "stopgame_ru", - "genres": [ - "simulator", - "action" - ] - }, - { - "id": 3867, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3868, - "name": "Her Story", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 3869, - "name": "Minecraft Story Mode", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3870, - "name": "Left 4 Dead 2", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "action" - ] - }, - { - "id": 3871, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 3872, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3873, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3874, - "name": "Far Cry 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3875, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3876, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3877, - "name": "Into the Breach", - "site": "playground_ru", - "genres": [ - "Инди", - "Тактика", - "Научная фантастика", - "Пошаговая", - "Стратегия" - ] - }, - { - "id": 3878, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "RPG", - "aRPG" - ] - }, - { - "id": 3879, - "name": "Hero Academy", - "site": "ag_ru", - "genres": [ - "Инди", - "Настольные", - "Стратегии" - ] - }, - { - "id": 3880, - "name": "Mini Ninjas", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3881, - "name": "Homefront", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3882, - "name": "Happy Wheels", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 3883, - "name": "Legend Hand of God", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3884, - "name": "Lara Croft and the Guardian of Light", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Пристрели их всех", - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 3885, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3886, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "RPG", - "aRPG" - ] - }, - { - "id": 3887, - "name": "Mirror's Edge", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3888, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3889, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3890, - "name": "Jade Empire", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 3891, - "name": "Heroes of Might and Magic III", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3892, - "name": "Legend of Grimrock", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3893, - "name": "Far Cry 3: Blood Dragon", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 3894, - "name": "Hotel Remorse", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3895, - "name": "Lara Croft and the Temple of Osiris", - "site": "gamebomb_ru", - "genres": [ - "Логические игры", - "Боевик-приключения" - ] - }, - { - "id": 3896, - "name": "Hard Reset", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3897, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "RPG", - "aRPG" - ] - }, - { - "id": 3898, - "name": "Monst", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3899, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3900, - "name": "Fallout 4", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG", - "Шутер" - ] - }, - { - "id": 3901, - "name": "Heroes of Might and Magic IV", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3902, - "name": "Legendary", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 3903, - "name": "Jotun", - "site": "playground_ru", - "genres": [ - "Аркада", - "Адвенчура", - "Инди" - ] - }, - { - "id": 3904, - "name": "Late Shift", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3905, - "name": "Mortal Kombat IX", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3906, - "name": "Hotline Miami", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3907, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3908, - "name": "Firewatch", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 3909, - "name": "Hard Reset: Exile (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3910, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3911, - "name": "Fallout: New Vegas", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 3912, - "name": "FarSky", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 3913, - "name": "Mortal Kombat X", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3914, - "name": "Life After Us: Shipwrecked", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3915, - "name": "Middle-earth: Shadow of Mordor", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 3916, - "name": "Heroes of Might and Magic V", - "site": "ag_ru", - "genres": [ - "Стратегии" - ] - }, - { - "id": 3917, - "name": "Layers of Fear", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 3918, - "name": "Just Cause", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Песочница" - ] - }, - { - "id": 3919, - "name": "Hotline Miami 2: Wrong Number", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3920, - "name": "Family Trouble", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3921, - "name": "Harts", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3922, - "name": "Mother Russia Bleeds", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3923, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 3924, - "name": "Might & Magic Heroes VI", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3925, - "name": "Final Fantasy III (Remake)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3926, - "name": "Life Goes On: Done to Death", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3927, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 3928, - "name": "Far Cry", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3929, - "name": "First Winter", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 3930, - "name": "House Flipper", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3931, - "name": "Hatred", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 3932, - "name": "Just Cause 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Шутер", - "Песочница" - ] - }, - { - "id": 3933, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3934, - "name": "Mount Your Friends", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3935, - "name": "Minecraft Story Mode", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3936, - "name": "Far Cry 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 3937, - "name": "Life Is Strange", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 3938, - "name": "Left 4 Dead", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3939, - "name": "Final Fantasy IV (Remake)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3940, - "name": "Five Minutes", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 3941, - "name": "Fallout 4", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3942, - "name": "Kane & Lynch 2: Dog Days", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3943, - "name": "Mr. Robot", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3944, - "name": "I Am Alive", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3945, - "name": "Hellblade: Senua's Sacrifice", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 3946, - "name": "Far Cry 3: Blood Dragon", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Аддон", - "Самостоятельный" - ] - }, - { - "id": 3947, - "name": "Mini Ninjas", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 3948, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "ag_ru", - "genres": [] - }, - { - "id": 3949, - "name": "Limbo", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "платформеры" - ] - }, - { - "id": 3950, - "name": "Mr.President!", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3951, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3952, - "name": "FarSky", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3953, - "name": "Killer is Dead: Nightmare Edition", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3954, - "name": "Five Nights At Freddy's", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 3955, - "name": "Mirror's Edge", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 3956, - "name": "Left 4 Dead 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 3957, - "name": "I am Setsuna", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3958, - "name": "Hellgate: London", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 3959, - "name": "Fallout: New Vegas", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 3960, - "name": "Homefront", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 3961, - "name": "Murdered: Soul Suspect", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3962, - "name": "Little Nightmares", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "платформеры" - ] - }, - { - "id": 3963, - "name": "Final Fantasy III (Remake)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3964, - "name": "Final Fantasy X HD Remaster", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3965, - "name": "King's Bounty: Armored Princess", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3966, - "name": "Five Nights At Freddy's 2", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 3967, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3968, - "name": "Legend Hand of God", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 3969, - "name": "I hate this game", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3970, - "name": "Final Fantasy IV (Remake)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3971, - "name": "Monst", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3972, - "name": "Her Story", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 3973, - "name": "Little Nightmares - The Depths (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3974, - "name": "Hotel Remorse", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 3975, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3976, - "name": "Family Trouble", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 3977, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3978, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 3979, - "name": "King's Bounty: Crossworlds", - "site": "playground_ru", - "genres": [] - }, - { - "id": 3980, - "name": "Legend of Grimrock", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 3981, - "name": "Five Nights At Freddy's 3", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 3982, - "name": "Mortal Kombat IX", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 3983, - "name": "INSIDE", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 3984, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 3985, - "name": "Hero Academy", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3986, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3987, - "name": "Hotline Miami", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Инди", - "Экшены", - "Аркадные" - ] - }, - { - "id": 3988, - "name": "Final Fantasy X HD Remaster", - "site": "gameguru_ru", - "genres": [ - "Ремастер", - "RPG", - "jRPG" - ] - }, - { - "id": 3989, - "name": "Mortal Kombat X", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 3990, - "name": "King's Bounty: Dark Side", - "site": "playground_ru", - "genres": [ - "Ролевая", - "Стратегия" - ] - }, - { - "id": 3991, - "name": "Legendary", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 3992, - "name": "Indivisible", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 3993, - "name": "Need for Speed: Underground", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 3994, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 3995, - "name": "Loki: Heroes of Mythology", - "site": "gamer_info_com", - "genres": [ - "RPG", - "hack & slash" - ] - }, - { - "id": 3996, - "name": "Final Fantasy XIII", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 3997, - "name": "Five Nights At Freddy's 4", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 3998, - "name": "Heroes of Might and Magic III", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 3999, - "name": "Hotline Miami 2: Wrong Number", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Инди", - "Экшены", - "Аркадные" - ] - }, - { - "id": 4000, - "name": "Far Cry", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4001, - "name": "Life After Us: Shipwrecked", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4002, - "name": "Mother Russia Bleeds", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Боевик" - ] - }, - { - "id": 4003, - "name": "Never Alone", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4004, - "name": "Final Fantasy XIII", - "site": "gameguru_ru", - "genres": [ - "RPG", - "jRPG" - ] - }, - { - "id": 4005, - "name": "King's Bounty: The Legend", - "site": "playground_ru", - "genres": [ - "Стратегия", - "Пошаговая" - ] - }, - { - "id": 4006, - "name": "Injustice: Gods Among Us", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4007, - "name": "Five Nights at Freddy's 3D", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4008, - "name": "Lost Planet: Colonies", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4009, - "name": "Life Goes On: Done to Death", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4010, - "name": "Final Fantasy XIII-2", - "site": "gameguru_ru", - "genres": [ - "RPG", - "jRPG" - ] - }, - { - "id": 4011, - "name": "Heroes of Might and Magic IV", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4012, - "name": "Neverending Nightmares", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4013, - "name": "Mount Your Friends", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4014, - "name": "House Flipper", - "site": "ag_ru", - "genres": [ - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 4015, - "name": "Final Fantasy XIII-2", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 4016, - "name": "Far Cry 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4017, - "name": "Five Nights in Anime v3", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4018, - "name": "King's Bounty: Warriors of the North", - "site": "playground_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 4019, - "name": "Into the Breach", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4020, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4021, - "name": "Lost: Via Domus", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4022, - "name": "Nevermind", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4023, - "name": "Mr. Robot", - "site": "igromania_ru", - "genres": [ - "Тактика", - "Платформер", - "Ролевая игра" - ] - }, - { - "id": 4024, - "name": "Life Is Strange", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 4025, - "name": "I Am Alive", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 4026, - "name": "Heroes of Might and Magic V", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4027, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4028, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4029, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4030, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4031, - "name": "Love Is Strange (DEMO)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4032, - "name": "Fran Bow", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4033, - "name": "Jade Empire", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4034, - "name": "Mr.President!", - "site": "igromania_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 4035, - "name": "Far Cry 3: Blood Dragon", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4036, - "name": "Limbo", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 4037, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4038, - "name": "I am Setsuna", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Приключения" - ] - }, - { - "id": 4039, - "name": "Neverwinter Nights", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4040, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4041, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4042, - "name": "Kingdom Rush", - "site": "playground_ru", - "genres": [ - "Стратегия", - "Tower defence" - ] - }, - { - "id": 4043, - "name": "METAL SLUG", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4044, - "name": "Murdered: Soul Suspect", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4045, - "name": "Final Fantasy XV", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4046, - "name": "Jotun", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4047, - "name": "Front Mission Evolved", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 4048, - "name": "Little Nightmares", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл", - "Приключения" - ] - }, - { - "id": 4049, - "name": "Neverwinter Nights 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4050, - "name": "FarSky", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4051, - "name": "I hate this game", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 4052, - "name": "Kingdom: Classic", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4053, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4054, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4055, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 4056, - "name": "METAL SLUG 3", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up" - ] - }, - { - "id": 4057, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4058, - "name": "Little Nightmares - The Depths (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4059, - "name": "NieR: Automata", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4060, - "name": "Just Cause", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4061, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4062, - "name": "GUN", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4063, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4064, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Файтинг" - ] - }, - { - "id": 4065, - "name": "INSIDE", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Приключения", - "Экшены", - "Головоломки", - "Платформеры" - ] - }, - { - "id": 4066, - "name": "Final Fantasy III (Remake)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4067, - "name": "Kingdoms of Amalur: Reckoning", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Ролевая", - "Экшен", - "Открытый мир", - "Фэнтези" - ] - }, - { - "id": 4068, - "name": "Nightmare House 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4069, - "name": "MINERVA: Metastasis", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4070, - "name": "Final Fantasy XV", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4071, - "name": "Homefront", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4072, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4073, - "name": "Just Cause 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4074, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 4075, - "name": "Loki: Heroes of Mythology", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 4076, - "name": "Gemini Rue", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4077, - "name": "Nosferatu: The Wrath of Malachi", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4078, - "name": "Final Fantasy IV (Remake)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4079, - "name": "Knock-Knock", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4080, - "name": "MOTHERGUNSHIP", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 4081, - "name": "Indivisible", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди" - ] - }, - { - "id": 4082, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4083, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4084, - "name": "Hotel Remorse", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4085, - "name": "Nox Timore", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4086, - "name": "Lost Planet: Colonies", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4087, - "name": "Kane & Lynch 2: Dog Days", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4088, - "name": "Need for Speed: Underground", - "site": "igromania_ru", - "genres": [ - "Автомобили", - "Симулятор" - ] - }, - { - "id": 4089, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4090, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4091, - "name": "Machinarium", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4092, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4093, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4094, - "name": "Hotline Miami", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4095, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4096, - "name": "Injustice: Gods Among Us", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4097, - "name": "Firewatch", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 4098, - "name": "Lost: Via Domus", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл", - "Боевик/Экшн" - ] - }, - { - "id": 4099, - "name": "Never Alone", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4100, - "name": "Magibot", - "site": "gamer_info_com", - "genres": [ - "инди", - "платформеры" - ] - }, - { - "id": 4101, - "name": "Killer is Dead: Nightmare Edition", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4102, - "name": "Geometry Dash", - "site": "gamespot_com", - "genres": [ - "Music/Rhythm" - ] - }, - { - "id": 4103, - "name": "First Winter", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4104, - "name": "Final Fantasy X HD Remaster", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4105, - "name": "Omensight", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4106, - "name": "Lara Croft and the Guardian of Light", - "site": "playground_ru", - "genres": [ - "Аркада", - "Вид сверху" - ] - }, - { - "id": 4107, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4108, - "name": "Love Is Strange (DEMO)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4109, - "name": "Into the Breach", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 4110, - "name": "Neverending Nightmares", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4111, - "name": "Hotline Miami 2: Wrong Number", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4112, - "name": "Five Minutes", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4113, - "name": "Magicka", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4114, - "name": "King's Bounty: Armored Princess", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4115, - "name": "One Hand Clapping (DEMO)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4116, - "name": "Ghost of a Tale", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4117, - "name": "METAL SLUG", - "site": "gamebomb_ru", - "genres": [ - "Шутер" - ] - }, - { - "id": 4118, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4119, - "name": "Nevermind", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4120, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4121, - "name": "Lara Croft and the Temple of Osiris", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4122, - "name": "Five Nights At Freddy's", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди", - "Квест" - ] - }, - { - "id": 4123, - "name": "Jade Empire", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Файтинги" - ] - }, - { - "id": 4124, - "name": "House Flipper", - "site": "stopgame_ru", - "genres": [ - "simulator" - ] - }, - { - "id": 4125, - "name": "One Night at Flumpty's", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4126, - "name": "Magicka 2", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 4127, - "name": "King's Bounty: Crossworlds", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4128, - "name": "Five Nights At Freddy's 2", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 4129, - "name": "METAL SLUG 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 4130, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4131, - "name": "Late Shift", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 4132, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4133, - "name": "One Night at Flumpty's 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4134, - "name": "Goat Simulator", - "site": "gamespot_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 4135, - "name": "Manual Samuel", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4136, - "name": "Final Fantasy XIII", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 4137, - "name": "Jotun", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4138, - "name": "Five Nights At Freddy's 3", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди", - "Квест" - ] - }, - { - "id": 4139, - "name": "MINERVA: Metastasis", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4140, - "name": "I Am Alive", - "site": "stopgame_ru", - "genres": [ - "adventure", - "action" - ] - }, - { - "id": 4141, - "name": "King's Bounty: Dark Side", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4142, - "name": "Neverwinter Nights", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 4143, - "name": "One Piece: Burning Blood", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4144, - "name": "Five Nights At Freddy's 4", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди", - "Квест" - ] - }, - { - "id": 4145, - "name": "Mario.EXE", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4146, - "name": "Layers of Fear", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "От первого лица" - ] - }, - { - "id": 4147, - "name": "Just Cause", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4148, - "name": "MOTHERGUNSHIP", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 4149, - "name": "Neverwinter Nights 2", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 4150, - "name": "Orcs Must Die!", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4151, - "name": "I am Setsuna", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 4152, - "name": "King's Bounty: The Legend", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4153, - "name": "Firewatch", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 4154, - "name": "Five Nights at Freddy's 3D", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4155, - "name": "Grand Theft Auto III", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4156, - "name": "Marvel vs. Capcom: Infinite", - "site": "gamer_info_com", - "genres": [ - "файтинги" - ] - }, - { - "id": 4157, - "name": "Final Fantasy XIII-2", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Role-Playing (RPG)" - ] - }, - { - "id": 4158, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4159, - "name": "Just Cause 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4160, - "name": "NieR: Automata", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 4161, - "name": "Machinarium", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл" - ] - }, - { - "id": 4162, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4163, - "name": "Five Nights in Anime v3", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4164, - "name": "King's Bounty: Warriors of the North", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4165, - "name": "I hate this game", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4166, - "name": "Ori and the Blind Forest", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4167, - "name": "Magibot", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4168, - "name": "Masochisia", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4169, - "name": "Nightmare House 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4170, - "name": "Fran Bow", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4171, - "name": "Grand Theft Auto: San Andreas", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4172, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4173, - "name": "First Winter", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 4174, - "name": "Left 4 Dead", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Ужасы", - "Кооператив", - "Шутер", - "Зомби", - "Экшен", - "От первого лица" - ] - }, - { - "id": 4175, - "name": "Kane & Lynch 2: Dog Days", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4176, - "name": "Outlast", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4177, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4178, - "name": "Front Mission Evolved", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Симулятор", - "Шутер" - ] - }, - { - "id": 4179, - "name": "Nosferatu: The Wrath of Malachi", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 4180, - "name": "INSIDE", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 4181, - "name": "Masters of Anima", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 4182, - "name": "Magicka", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 4183, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4184, - "name": "Outlast: Whistleblower", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4185, - "name": "Five Minutes", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4186, - "name": "Grand Theft Auto: Vice City", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4187, - "name": "Left 4 Dead 2", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Ужасы", - "Кооператив", - "Шутер", - "Зомби", - "Экшен", - "От первого лица" - ] - }, - { - "id": 4188, - "name": "GUN", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 4189, - "name": "Killer is Dead: Nightmare Edition", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 4190, - "name": "Nox Timore", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4191, - "name": "Kingdom Rush", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4192, - "name": "Max Payne", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица" - ] - }, - { - "id": 4193, - "name": "Magicka 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 4194, - "name": "Indivisible", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 4195, - "name": "Overlord", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4196, - "name": "Gemini Rue", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 4197, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4198, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4199, - "name": "Legend Hand of God", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая", - "Фэнтези", - "Вид сверху" - ] - }, - { - "id": 4200, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица" - ] - }, - { - "id": 4201, - "name": "Geometry Dash", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4202, - "name": "King's Bounty: Armored Princess", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4203, - "name": "Overlord: Raising Hell", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4204, - "name": "Manual Samuel", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 4205, - "name": "Kingdom: Classic", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4206, - "name": "Injustice: Gods Among Us", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4207, - "name": "Five Nights At Freddy's", - "site": "store_steampowered_com", - "genres": [ - "Simulation", - "Indie" - ] - }, - { - "id": 4208, - "name": "Omensight", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4209, - "name": "Final Fantasy XV", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4210, - "name": "Legend of Grimrock", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 4211, - "name": "Ghost of a Tale", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4212, - "name": "Mario.EXE", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4213, - "name": "Oxenfree", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4214, - "name": "Max Payne 3", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица" - ] - }, - { - "id": 4215, - "name": "King's Bounty: Crossworlds", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4216, - "name": "Grandpa", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 4217, - "name": "One Hand Clapping (DEMO)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4218, - "name": "Kingdoms of Amalur: Reckoning", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4219, - "name": "Into the Breach", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 4220, - "name": "Goat Simulator", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Открытый мир", - "Симулятор" - ] - }, - { - "id": 4221, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4222, - "name": "Painkiller", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4223, - "name": "Five Nights At Freddy's 2", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 4224, - "name": "Legendary", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4225, - "name": "Max: The Curse Of Brotherhood", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "платформеры" - ] - }, - { - "id": 4226, - "name": "Marvel vs. Capcom: Infinite", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 4227, - "name": "Grey", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4228, - "name": "One Night at Flumpty's", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4229, - "name": "Grand Theft Auto III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4230, - "name": "Knock-Knock", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4231, - "name": "Painkiller: Battle Out of Hell", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4232, - "name": "Jade Empire", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4233, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4234, - "name": "Life After Us: Shipwrecked", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4235, - "name": "Masochisia", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4236, - "name": "McPixel", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "инди", - "логические", - "приключения" - ] - }, - { - "id": 4237, - "name": "Grand Theft Auto: San Andreas", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4238, - "name": "Grim Fandango Remastered", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4239, - "name": "One Night at Flumpty's 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4240, - "name": "King's Bounty: Dark Side", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4241, - "name": "Painkiller: Overdose", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4242, - "name": "Five Nights At Freddy's 3", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Simulation", - "Indie" - ] - }, - { - "id": 4243, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4244, - "name": "Jotun", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 4245, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4246, - "name": "Grand Theft Auto: Vice City", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4247, - "name": "Masters of Anima", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 4248, - "name": "Life Goes On: Done to Death", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада", - "Платформер" - ] - }, - { - "id": 4249, - "name": "Metal Gear Rising: Revengeance", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 4250, - "name": "One Piece: Burning Blood", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 4251, - "name": "Paint the Town Red", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4252, - "name": "King's Bounty: The Legend", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4253, - "name": "Grow Home", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4254, - "name": "Grandpa", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4255, - "name": "Lara Croft and the Guardian of Light", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4256, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4257, - "name": "Just Cause", - "site": "stopgame_ru", - "genres": [ - "arcade", - "action", - "racing" - ] - }, - { - "id": 4258, - "name": "Max Payne", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 4259, - "name": "Panty Party", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4260, - "name": "Orcs Must Die!", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 4261, - "name": "Metal Wolf Chaos XD", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4262, - "name": "Life Is Strange", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Адвенчура", - "Квест" - ] - }, - { - "id": 4263, - "name": "Five Nights At Freddy's 4", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie" - ] - }, - { - "id": 4264, - "name": "Grey", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4265, - "name": "King's Bounty: Warriors of the North", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4266, - "name": "Grow Up", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4267, - "name": "Papers, Please", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4268, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4269, - "name": "Lara Croft and the Temple of Osiris", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Аркады", - "Приключения" - ] - }, - { - "id": 4270, - "name": "Just Cause 2", - "site": "stopgame_ru", - "genres": [ - "action", - "racing" - ] - }, - { - "id": 4271, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 4272, - "name": "Grim Fandango Remastered", - "site": "gameguru_ru", - "genres": [ - "Ремастер", - "Квест" - ] - }, - { - "id": 4273, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4274, - "name": "Metro 2033", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4275, - "name": "Five Nights at Freddy's 3D", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4276, - "name": "Limbo", - "site": "playground_ru", - "genres": [ - "Аркада", - "Адвенчура", - "Инди", - "Платформер" - ] - }, - { - "id": 4277, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4278, - "name": "Peace, Death!", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4279, - "name": "Kane & Lynch 2: Dog Days", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4280, - "name": "Ori and the Blind Forest", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4281, - "name": "Grow Home", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер", - "Квест" - ] - }, - { - "id": 4282, - "name": "Guns, Gore & Cannoli", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 4283, - "name": "Late Shift", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4284, - "name": "Metro 2033 Redux", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4285, - "name": "Pillars of Eternity", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4286, - "name": "Grow Up", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер", - "Квест" - ] - }, - { - "id": 4287, - "name": "Outlast", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4288, - "name": "Little Nightmares", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "Платформер" - ] - }, - { - "id": 4289, - "name": "Firewatch", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4290, - "name": "Five Nights in Anime v3", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4291, - "name": "Kingdom Rush", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Стратегии" - ] - }, - { - "id": 4292, - "name": "Killer is Dead: Nightmare Edition", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4293, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4294, - "name": "Metro Exodus", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4295, - "name": "Guns, Gore & Cannoli", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4296, - "name": "Layers of Fear", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Приключения" - ] - }, - { - "id": 4297, - "name": "Outlast: Whistleblower", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4298, - "name": "Little Nightmares - The Depths (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4299, - "name": "Max Payne 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 4300, - "name": "Guns, Gore & Cannoli 2", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 4301, - "name": "First Winter", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4302, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4303, - "name": "Kingdom: Classic", - "site": "ag_ru", - "genres": [ - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 4304, - "name": "Guns, Gore & Cannoli 2", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Аркада", - "Платформер" - ] - }, - { - "id": 4305, - "name": "Metro Last Light", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4306, - "name": "King's Bounty: Armored Princess", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4307, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4308, - "name": "Fran Bow", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 4309, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4310, - "name": "Overlord", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 4311, - "name": "Half-Life", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4312, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4313, - "name": "Max: The Curse Of Brotherhood", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 4314, - "name": "Five Minutes", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4315, - "name": "Kingdoms of Amalur: Reckoning", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 4316, - "name": "Metro Last Light Redux", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4317, - "name": "Half-Life", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4318, - "name": "King's Bounty: Crossworlds", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4319, - "name": "Half-Life 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4320, - "name": "Left 4 Dead", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4321, - "name": "Overlord: Raising Hell", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 4322, - "name": "Pinstripe", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4323, - "name": "McPixel", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4324, - "name": "Loki: Heroes of Mythology", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 4325, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4326, - "name": "Half-Life 2: Episode One", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4327, - "name": "Knock-Knock", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4328, - "name": "Front Mission Evolved", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4329, - "name": "Half-Life 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4330, - "name": "Pirates of the Caribbean: At World’s End", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4331, - "name": "Lost Planet: Colonies", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4332, - "name": "Five Nights At Freddy's", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4333, - "name": "King's Bounty: Dark Side", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4334, - "name": "Oxenfree", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4335, - "name": "Left 4 Dead 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4336, - "name": "Half-Life 2: Episode Two", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4337, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4338, - "name": "Pizza Delivery 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4339, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4340, - "name": "King's Bounty: The Legend", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4341, - "name": "Half-Life: Blue Shift", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4342, - "name": "Painkiller", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 4343, - "name": "Half-Life 2: Episode One", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4344, - "name": "Lost: Via Domus", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура" - ] - }, - { - "id": 4345, - "name": "Five Nights At Freddy's 2", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4346, - "name": "Legend Hand of God", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4347, - "name": "GUN", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4348, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4349, - "name": "Plague Inc: Evolved", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4350, - "name": "Metal Gear Rising: Revengeance", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 4351, - "name": "Lara Croft and the Guardian of Light", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 4352, - "name": "Half-Life: Opposing Force", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4353, - "name": "Painkiller: Battle Out of Hell", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 4354, - "name": "Love Is Strange (DEMO)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4355, - "name": "King's Bounty: Warriors of the North", - "site": "stopgame_ru", - "genres": [ - "add-on", - "rpg" - ] - }, - { - "id": 4356, - "name": "Legend of Grimrock", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4357, - "name": "Plants vs. Zombies", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4358, - "name": "Half-Life: Paranoia", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4359, - "name": "Metal Wolf Chaos XD", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 4360, - "name": "Five Nights At Freddy's 3", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4361, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4362, - "name": "Half-Life 2: Episode Two", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4363, - "name": "Painkiller: Overdose", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 4364, - "name": "Lara Croft and the Temple of Osiris", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Головоломки" - ] - }, - { - "id": 4365, - "name": "METAL SLUG", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4366, - "name": "Gemini Rue", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 4367, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4368, - "name": "Half-Life: The Xeno Project", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4369, - "name": "Legendary", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4370, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4371, - "name": "Middle-earth: Shadow of Mordor", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4372, - "name": "Metro 2033", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 4373, - "name": "Late Shift", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 4374, - "name": "Paint the Town Red", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 4375, - "name": "Five Nights At Freddy's 4", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4376, - "name": "METAL SLUG 3", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4377, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4378, - "name": "Pony Island", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4379, - "name": "Half-Life: Blue Shift", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4380, - "name": "Metro 2033 Redux", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4381, - "name": "Panty Party", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4382, - "name": "Life After Us: Shipwrecked", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4383, - "name": "Kingdom Rush", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 4384, - "name": "MINERVA: Metastasis", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4385, - "name": "Halo: Combat Evolved", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4386, - "name": "Might & Magic Heroes VI", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4387, - "name": "Portal", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4388, - "name": "Half-Life: Opposing Force", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4389, - "name": "Geometry Dash", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 4390, - "name": "Layers of Fear", - "site": "ag_ru", - "genres": [ - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 4391, - "name": "Five Nights at Freddy's 3D", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4392, - "name": "Hand of Fate", - "site": "gameguru_ru", - "genres": [ - "Карты", - "На логику" - ] - }, - { - "id": 4393, - "name": "Papers, Please", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4394, - "name": "Portal 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4395, - "name": "Metro Exodus", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 4396, - "name": "Life Goes On: Done to Death", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4397, - "name": "Half-Life: Paranoia", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4398, - "name": "Minecraft Story Mode", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4399, - "name": "MOTHERGUNSHIP", - "site": "playground_ru", - "genres": [ - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 4400, - "name": "Kingdom: Classic", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4401, - "name": "Five Nights in Anime v3", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4402, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 4403, - "name": "Hand of Fate 2", - "site": "gameguru_ru", - "genres": [ - "Карты", - "На логику" - ] - }, - { - "id": 4404, - "name": "Prey (2006)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4405, - "name": "Peace, Death!", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4406, - "name": "Ghost of a Tale", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 4407, - "name": "Half-Life: The Xeno Project", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4408, - "name": "Mini Ninjas", - "site": "gamer_info_com", - "genres": [ - "детские", - "action" - ] - }, - { - "id": 4409, - "name": "Life Is Strange", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 4410, - "name": "Happy Room", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4411, - "name": "Machinarium", - "site": "playground_ru", - "genres": [ - "Стимпанк", - "Логические", - "Адвенчура", - "Квест" - ] - }, - { - "id": 4412, - "name": "Prince of Persia (2008)", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4413, - "name": "Left 4 Dead", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4414, - "name": "Pillars of Eternity", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 4415, - "name": "Kingdoms of Amalur: Reckoning", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 4416, - "name": "Fran Bow", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4417, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4418, - "name": "Happy Wheels", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Аркада", - "Гонки/вождение" - ] - }, - { - "id": 4419, - "name": "Mirror's Edge", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 4420, - "name": "Prince of Persia: The Two Thrones", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4421, - "name": "Limbo", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 4422, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4423, - "name": "Goat Simulator", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Simulation", - "Indie" - ] - }, - { - "id": 4424, - "name": "Magibot", - "site": "playground_ru", - "genres": [ - "Инди", - "Адвенчура", - "Экшен", - "Логические", - "Платформер", - "Фэнтези" - ] - }, - { - "id": 4425, - "name": "Hard Reset", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4426, - "name": "Knock-Knock", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4427, - "name": "Left 4 Dead 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4428, - "name": "Front Mission Evolved", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4429, - "name": "Prospekt", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4430, - "name": "Monst", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4431, - "name": "Halo: Combat Evolved", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4432, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4433, - "name": "Hard Reset: Exile (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4434, - "name": "Little Nightmares", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 4435, - "name": "Magicka", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4436, - "name": "Legend Hand of God", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 4437, - "name": "Prototype", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4438, - "name": "Mortal Kombat IX", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4439, - "name": "Harts", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4440, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4441, - "name": "Grand Theft Auto III", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4442, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4443, - "name": "Hand of Fate", - "site": "gamespot_com", - "genres": [ - "Trivia/Board Game" - ] - }, - { - "id": 4444, - "name": "Little Nightmares - The Depths (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4445, - "name": "Prototype 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4446, - "name": "Hatred", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4447, - "name": "Legend of Grimrock", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 4448, - "name": "Magicka 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура" - ] - }, - { - "id": 4449, - "name": "GUN", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 4450, - "name": "Pinstripe", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4451, - "name": "Mortal Kombat X", - "site": "gamer_info_com", - "genres": [ - "файтинги" - ] - }, - { - "id": 4452, - "name": "Lara Croft and the Guardian of Light", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4453, - "name": "Psychonauts", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4454, - "name": "Hellblade: Senua's Sacrifice", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Слэшер" - ] - }, - { - "id": 4455, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4456, - "name": "Metro Last Light", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 4457, - "name": "Pirates of the Caribbean: At World’s End", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4458, - "name": "Legendary", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4459, - "name": "Hand of Fate 2", - "site": "gamespot_com", - "genres": [ - "Trivia/Board Game" - ] - }, - { - "id": 4460, - "name": "Grand Theft Auto: San Andreas", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4461, - "name": "Manual Samuel", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Юмор" - ] - }, - { - "id": 4462, - "name": "Mother Russia Bleeds", - "site": "gamer_info_com", - "genres": [ - "инди", - "beat 'em up" - ] - }, - { - "id": 4463, - "name": "Hellgate: London", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 4464, - "name": "PuniTy", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4465, - "name": "Metro Last Light Redux", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4466, - "name": "Gemini Rue", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4467, - "name": "Pizza Delivery 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4468, - "name": "Lara Croft and the Temple of Osiris", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4469, - "name": "Loki: Heroes of Mythology", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4470, - "name": "Mario.EXE", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4471, - "name": "Her Story", - "site": "gameguru_ru", - "genres": [ - "Инди", - "На логику", - "Квест" - ] - }, - { - "id": 4472, - "name": "Life After Us: Shipwrecked", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4473, - "name": "P·O·L·L·E·N", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4474, - "name": "Mount Your Friends", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4475, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4476, - "name": "Plague Inc: Evolved", - "site": "igromania_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 4477, - "name": "Hero Academy", - "site": "gameguru_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 4478, - "name": "Grand Theft Auto: Vice City", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4479, - "name": "Geometry Dash", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4480, - "name": "Late Shift", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 4481, - "name": "Lost Planet: Colonies", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4482, - "name": "Quake 4", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4483, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4484, - "name": "Happy Room", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 4485, - "name": "Marvel vs. Capcom: Infinite", - "site": "playground_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 4486, - "name": "Mr. Robot", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4487, - "name": "Life Goes On: Done to Death", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Экшены", - "Казуальные", - "Платформеры" - ] - }, - { - "id": 4488, - "name": "Heroes of Might and Magic III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4489, - "name": "Plants vs. Zombies", - "site": "igromania_ru", - "genres": [ - "Стратегия в реальном времени", - "Стратегия", - "Аркада" - ] - }, - { - "id": 4490, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4491, - "name": "Quantum Break", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4492, - "name": "Masochisia", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4493, - "name": "Layers of Fear", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4494, - "name": "Lost: Via Domus", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4495, - "name": "Mr.President!", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4496, - "name": "Heroes of Might and Magic IV", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4497, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4498, - "name": "Ghost of a Tale", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 4499, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4500, - "name": "Raft", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4501, - "name": "Grandpa", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 4502, - "name": "Life Is Strange", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 4503, - "name": "Heroes of Might and Magic V", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4504, - "name": "Murdered: Soul Suspect", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 4505, - "name": "Masters of Anima", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Фэнтези", - "Тактика" - ] - }, - { - "id": 4506, - "name": "Love Is Strange (DEMO)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4507, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4508, - "name": "Rage", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4509, - "name": "Pony Island", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 4510, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4511, - "name": "Happy Wheels", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 4512, - "name": "Limbo", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Экшены", - "Головоломки", - "Платформеры" - ] - }, - { - "id": 4513, - "name": "Grey", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4514, - "name": "Goat Simulator", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4515, - "name": "Middle-earth: Shadow of Mordor", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Боевик/Экшн" - ] - }, - { - "id": 4516, - "name": "Max Payne", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица" - ] - }, - { - "id": 4517, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "gamer_info_com", - "genres": [ - "файтинги" - ] - }, - { - "id": 4518, - "name": "METAL SLUG", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4519, - "name": "Left 4 Dead", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4520, - "name": "Rampage Knights", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4521, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4522, - "name": "Portal", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Логика" - ] - }, - { - "id": 4523, - "name": "Little Nightmares", - "site": "ag_ru", - "genres": [ - "Экшены", - "Платформеры" - ] - }, - { - "id": 4524, - "name": "Hard Reset", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4525, - "name": "Might & Magic Heroes VI", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4526, - "name": "Homefront", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4527, - "name": "Rayman Legends", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4528, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "gamer_info_com", - "genres": [ - "файтинги", - "action" - ] - }, - { - "id": 4529, - "name": "METAL SLUG 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4530, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица" - ] - }, - { - "id": 4531, - "name": "Left 4 Dead 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4532, - "name": "Grim Fandango Remastered", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4533, - "name": "Portal 2", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Логика" - ] - }, - { - "id": 4534, - "name": "Hotel Remorse", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4535, - "name": "Hard Reset: Exile (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4536, - "name": "Rayman Origins", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4537, - "name": "Minecraft Story Mode", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 4538, - "name": "Little Nightmares - The Depths (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Платформеры" - ] - }, - { - "id": 4539, - "name": "Grand Theft Auto III", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 4540, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "gamer_info_com", - "genres": [ - "файтинги" - ] - }, - { - "id": 4541, - "name": "MINERVA: Metastasis", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4542, - "name": "Prey (2006)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4543, - "name": "Hotline Miami", - "site": "gameguru_ru", - "genres": [ - "Shoot-em-up", - "Аркада" - ] - }, - { - "id": 4544, - "name": "Max Payne 3", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Шутер" - ] - }, - { - "id": 4545, - "name": "Legend Hand of God", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 4546, - "name": "Rayman Raving Rabbids", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4547, - "name": "Mini Ninjas", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 4548, - "name": "Grow Home", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 4549, - "name": "Need for Speed: Underground", - "site": "gamer_info_com", - "genres": [ - "гонки" - ] - }, - { - "id": 4550, - "name": "Hotline Miami 2: Wrong Number", - "site": "gameguru_ru", - "genres": [ - "Shoot-em-up", - "Аркада" - ] - }, - { - "id": 4551, - "name": "Prince of Persia (2008)", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4552, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Платформеры" - ] - }, - { - "id": 4553, - "name": "MOTHERGUNSHIP", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4554, - "name": "Rayman Raving Rabbids 2", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4555, - "name": "Max: The Curse Of Brotherhood", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада" - ] - }, - { - "id": 4556, - "name": "Legend of Grimrock", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 4557, - "name": "Grand Theft Auto: San Andreas", - "site": "mobygames_com", - "genres": [ - "Racing / driving" - ] - }, - { - "id": 4558, - "name": "House Flipper", - "site": "gameguru_ru", - "genres": [ - "Стратегия", - "Симулятор" - ] - }, - { - "id": 4559, - "name": "Mirror's Edge", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Платформер", - "Боевик/Экшн" - ] - }, - { - "id": 4560, - "name": "Prince of Persia: The Two Thrones", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4561, - "name": "Never Alone", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 4562, - "name": "Real Horror Stories", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4563, - "name": "Machinarium", - "site": "iwantgames_ru", - "genres": [ - "Головоломки" - ] - }, - { - "id": 4564, - "name": "Harts", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 4565, - "name": "Loki: Heroes of Mythology", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 4566, - "name": "I Am Alive", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 4567, - "name": "Legendary", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4568, - "name": "Grow Up", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 4569, - "name": "McPixel", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 4570, - "name": "Monst", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4571, - "name": "Prospekt", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4572, - "name": "Neverending Nightmares", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 4573, - "name": "Redeemer", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4574, - "name": "I am Setsuna", - "site": "gameguru_ru", - "genres": [ - "RPG", - "jRPG" - ] - }, - { - "id": 4575, - "name": "Mortal Kombat IX", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4576, - "name": "Magibot", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4577, - "name": "Life After Us: Shipwrecked", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4578, - "name": "Lost Planet: Colonies", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4579, - "name": "Grand Theft Auto: Vice City", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 4580, - "name": "Nevermind", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 4581, - "name": "Metal Gear Rising: Revengeance", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Слэшер", - "Научная фантастика" - ] - }, - { - "id": 4582, - "name": "Prototype", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Ролевая игра" - ] - }, - { - "id": 4583, - "name": "Reign Of Kings", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4584, - "name": "I hate this game", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4585, - "name": "Hatred", - "site": "gamespot_com", - "genres": [ - "Fixed-Screen", - "Action", - "2D", - "Shooter" - ] - }, - { - "id": 4586, - "name": "Guns, Gore & Cannoli", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 4587, - "name": "Magicka", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4588, - "name": "INSIDE", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4589, - "name": "Life Goes On: Done to Death", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 4590, - "name": "Reigns", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4591, - "name": "Prototype 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 4592, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4593, - "name": "Grandpa", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4594, - "name": "Metal Wolf Chaos XD", - "site": "playground_ru", - "genres": [ - "Экшен", - "Научная фантастика" - ] - }, - { - "id": 4595, - "name": "Mortal Kombat X", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 4596, - "name": "Indivisible", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4597, - "name": "Lost: Via Domus", - "site": "ag_ru", - "genres": [ - "Приключения", - "Аркадные" - ] - }, - { - "id": 4598, - "name": "Reigns: Her Majesty", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4599, - "name": "Magicka 2", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4600, - "name": "Guns, Gore & Cannoli 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4601, - "name": "Psychonauts", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4602, - "name": "Life Is Strange", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4603, - "name": "Hellblade: Senua's Sacrifice", - "site": "gamespot_com", - "genres": [ - "Action", - "Open-World", - "3D", - "Adventure" - ] - }, - { - "id": 4604, - "name": "Neverwinter Nights", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 4605, - "name": "Injustice: Gods Among Us", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 4606, - "name": "Grey", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4607, - "name": "Metro 2033", - "site": "playground_ru", - "genres": [ - "Стелс", - "Научная фантастика", - "Постапокалипсис", - "Шутер", - "Экшен", - "От первого лица" - ] - }, - { - "id": 4608, - "name": "Mother Russia Bleeds", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 4609, - "name": "Resident Evil", - "site": "gamefaqs_gamespot_com", - "genres": [] - }, - { - "id": 4610, - "name": "Love Is Strange (DEMO)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4611, - "name": "PuniTy", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4612, - "name": "Manual Samuel", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4613, - "name": "Into the Breach", - "site": "gameguru_ru", - "genres": [ - "Инди", - "В пошаговом режиме", - "Стратегия" - ] - }, - { - "id": 4614, - "name": "Limbo", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 4615, - "name": "Neverwinter Nights 2", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 4616, - "name": "Mount Your Friends", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4617, - "name": "Metro 2033 Redux", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4618, - "name": "Hellgate: London", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 4619, - "name": "Half-Life", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4620, - "name": "P·O·L·L·E·N", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Космос" - ] - }, - { - "id": 4621, - "name": "METAL SLUG", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Аркадные", - "Файтинги" - ] - }, - { - "id": 4622, - "name": "Jade Empire", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4623, - "name": "Grim Fandango Remastered", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4624, - "name": "Mr. Robot", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4625, - "name": "Mario.EXE", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4626, - "name": "Little Nightmares", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 4627, - "name": "NieR: Automata", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 4628, - "name": "Jotun", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди" - ] - }, - { - "id": 4629, - "name": "Quake 4", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 4630, - "name": "Metro Exodus", - "site": "playground_ru", - "genres": [ - "Стелс", - "Постапокалипсис", - "Выживание", - "Шутер", - "Экшен", - "От первого лица" - ] - }, - { - "id": 4631, - "name": "METAL SLUG 3", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Аркадные", - "Файтинги" - ] - }, - { - "id": 4632, - "name": "Her Story", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 4633, - "name": "Mr.President!", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4634, - "name": "Marvel vs. Capcom: Infinite", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4635, - "name": "Just Cause", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Гонки/вождение", - "Квест" - ] - }, - { - "id": 4636, - "name": "Half-Life 2", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4637, - "name": "Nightmare House 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4638, - "name": "Little Nightmares - The Depths (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4639, - "name": "Quantum Break", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Боевик" - ] - }, - { - "id": 4640, - "name": "Grow Home", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4641, - "name": "Metro Last Light", - "site": "playground_ru", - "genres": [ - "Стелс", - "Научная фантастика", - "Постапокалипсис", - "Шутер", - "Экшен", - "От первого лица" - ] - }, - { - "id": 4642, - "name": "Just Cause 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Гонки/вождение", - "Квест" - ] - }, - { - "id": 4643, - "name": "Hero Academy", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 4644, - "name": "MINERVA: Metastasis", - "site": "ag_ru", - "genres": [ - "Шутеры" - ] - }, - { - "id": 4645, - "name": "Nosferatu: The Wrath of Malachi", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4646, - "name": "Masochisia", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4647, - "name": "Murdered: Soul Suspect", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 4648, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4649, - "name": "Raft", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4650, - "name": "Kane & Lynch 2: Dog Days", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4651, - "name": "Metro Last Light Redux", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4652, - "name": "Half-Life 2: Episode One", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4653, - "name": "Heroes of Might and Magic III", - "site": "gamespot_com", - "genres": [ - "Strategy" - ] - }, - { - "id": 4654, - "name": "MOTHERGUNSHIP", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 4655, - "name": "Grow Up", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4656, - "name": "Nox Timore", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4657, - "name": "Killer is Dead: Nightmare Edition", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4658, - "name": "Masters of Anima", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4659, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 4660, - "name": "Loki: Heroes of Mythology", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 4661, - "name": "Rage", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4662, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4663, - "name": "King's Bounty: Armored Princess", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 4664, - "name": "Machinarium", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4665, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "gamer_info_com", - "genres": [ - "jRPG", - "action" - ] - }, - { - "id": 4666, - "name": "Max Payne", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4667, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 4668, - "name": "Guns, Gore & Cannoli", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4669, - "name": "Half-Life 2: Episode Two", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4670, - "name": "Heroes of Might and Magic IV", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 4671, - "name": "Lost Planet: Colonies", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4672, - "name": "King's Bounty: Crossworlds", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 4673, - "name": "Rampage Knights", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4674, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4675, - "name": "Magibot", - "site": "ag_ru", - "genres": [ - "Приключения", - "Экшены", - "Инди", - "Стратегии" - ] - }, - { - "id": 4676, - "name": "Omensight", - "site": "gamer_info_com", - "genres": [ - "приключения", - "action" - ] - }, - { - "id": 4677, - "name": "King's Bounty: Dark Side", - "site": "gameguru_ru", - "genres": [ - "RPG", - "Стратегия" - ] - }, - { - "id": 4678, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4679, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 4680, - "name": "Rayman Legends", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4681, - "name": "Guns, Gore & Cannoli 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4682, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4683, - "name": "Lost: Via Domus", - "site": "stopgame_ru", - "genres": [ - "adventure", - "arcade" - ] - }, - { - "id": 4684, - "name": "Heroes of Might and Magic V", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 4685, - "name": "Magicka", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4686, - "name": "King's Bounty: The Legend", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 4687, - "name": "One Hand Clapping (DEMO)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4688, - "name": "Half-Life: Blue Shift", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4689, - "name": "Rayman Origins", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4690, - "name": "Max Payne 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4691, - "name": "Need for Speed: Underground", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг" - ] - }, - { - "id": 4692, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4693, - "name": "King's Bounty: Warriors of the North", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4694, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4695, - "name": "Love Is Strange (DEMO)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4696, - "name": "Half-Life", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4697, - "name": "One Night at Flumpty's", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4698, - "name": "Magicka 2", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 4699, - "name": "Rayman Raving Rabbids", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4700, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4701, - "name": "Half-Life: Opposing Force", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 4702, - "name": "Max: The Curse Of Brotherhood", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4703, - "name": "METAL SLUG", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4704, - "name": "Middle-earth: Shadow of Mordor", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ролевая", - "Открытый мир" - ] - }, - { - "id": 4705, - "name": "One Night at Flumpty's 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4706, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 4707, - "name": "Rayman Raving Rabbids 2", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4708, - "name": "Manual Samuel", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 4709, - "name": "Kingdom Rush", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4710, - "name": "Half-Life 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4711, - "name": "Might & Magic Heroes VI", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4712, - "name": "Half-Life: Paranoia", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4713, - "name": "McPixel", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4714, - "name": "One Piece: Burning Blood", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4715, - "name": "Kingdom: Classic", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4716, - "name": "METAL SLUG 3", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4717, - "name": "Real Horror Stories", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 4718, - "name": "Mario.EXE", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4719, - "name": "Kingdoms of Amalur: Reckoning", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 4720, - "name": "Redeemer", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 4721, - "name": "Half-Life: The Xeno Project", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4722, - "name": "Orcs Must Die!", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4723, - "name": "Minecraft Story Mode", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Адвенчура", - "Фэнтези" - ] - }, - { - "id": 4724, - "name": "Metal Gear Rising: Revengeance", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4725, - "name": "MINERVA: Metastasis", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4726, - "name": "Homefront", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 4727, - "name": "Never Alone", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 4728, - "name": "Knock-Knock", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4729, - "name": "Half-Life 2: Episode One", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4730, - "name": "Reign Of Kings", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4731, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4732, - "name": "MOTHERGUNSHIP", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4733, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4734, - "name": "Mini Ninjas", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "Адвенчура", - "От третьего лица" - ] - }, - { - "id": 4735, - "name": "Metal Wolf Chaos XD", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4736, - "name": "Neverending Nightmares", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 4737, - "name": "Marvel vs. Capcom: Infinite", - "site": "ag_ru", - "genres": [ - "Экшены", - "Файтинги" - ] - }, - { - "id": 4738, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4739, - "name": "Reigns", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 4740, - "name": "Lara Croft and the Guardian of Light", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4741, - "name": "Half-Life 2: Episode Two", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4742, - "name": "Ori and the Blind Forest", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 4743, - "name": "Hotel Remorse", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 4744, - "name": "Machinarium", - "site": "stopgame_ru", - "genres": [ - "adventure", - "logic" - ] - }, - { - "id": 4745, - "name": "Nevermind", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 4746, - "name": "Masochisia", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 4747, - "name": "Metro 2033", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4748, - "name": "Mirror's Edge", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица" - ] - }, - { - "id": 4749, - "name": "Halo: Combat Evolved", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4750, - "name": "Reigns: Her Majesty", - "site": "igromania_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 4751, - "name": "Lara Croft and the Temple of Osiris", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4752, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4753, - "name": "Outlast", - "site": "gamer_info_com", - "genres": [ - "ужасы" - ] - }, - { - "id": 4754, - "name": "Masters of Anima", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения", - "Стратегии" - ] - }, - { - "id": 4755, - "name": "Monst", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4756, - "name": "Magibot", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4757, - "name": "Metro 2033 Redux", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4758, - "name": "Hotline Miami", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4759, - "name": "Late Shift", - "site": "gameguru_ru", - "genres": [ - "Интерактивный фильм", - "Квест" - ] - }, - { - "id": 4760, - "name": "Resident Evil", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4761, - "name": "Half-Life: Blue Shift", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4762, - "name": "Neverwinter Nights", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры" - ] - }, - { - "id": 4763, - "name": "Outlast: Whistleblower", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4764, - "name": "Mortal Kombat IX", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4765, - "name": "Layers of Fear", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 4766, - "name": "Hand of Fate", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 4767, - "name": "Max Payne", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4768, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4769, - "name": "Metro Exodus", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Экшен" - ] - }, - { - "id": 4770, - "name": "Magicka", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 4771, - "name": "Hotline Miami 2: Wrong Number", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4772, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "Квест" - ] - }, - { - "id": 4773, - "name": "Neverwinter Nights 2", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 4774, - "name": "Overlord", - "site": "gamer_info_com", - "genres": [ - "приключения", - "action" - ] - }, - { - "id": 4775, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4776, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4777, - "name": "Mortal Kombat X", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Аркада", - "Файтинг" - ] - }, - { - "id": 4778, - "name": "Half-Life: Opposing Force", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 4779, - "name": "Left 4 Dead", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4780, - "name": "Metro Last Light", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4781, - "name": "Magicka 2", - "site": "stopgame_ru", - "genres": [ - "online", - "arcade", - "rpg" - ] - }, - { - "id": 4782, - "name": "NieR: Automata", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 4783, - "name": "Hand of Fate 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 4784, - "name": "Overlord: Raising Hell", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 4785, - "name": "Resident Evil 4", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4786, - "name": "Left 4 Dead 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4787, - "name": "Max Payne 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4788, - "name": "Nightmare House 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4789, - "name": "House Flipper", - "site": "gamespot_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 4790, - "name": "Metro Last Light Redux", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4791, - "name": "Manual Samuel", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4792, - "name": "Mother Russia Bleeds", - "site": "playground_ru", - "genres": [ - "Аркада", - "Платформер", - "Ретро" - ] - }, - { - "id": 4793, - "name": "Oxenfree", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы", - "приключения", - "платформеры" - ] - }, - { - "id": 4794, - "name": "Resident Evil 5", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4795, - "name": "Legend Hand of God", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 4796, - "name": "Max: The Curse Of Brotherhood", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 4797, - "name": "Nosferatu: The Wrath of Malachi", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 4798, - "name": "Happy Room", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Simulation", - "Indie" - ] - }, - { - "id": 4799, - "name": "Mount Your Friends", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4800, - "name": "Mario.EXE", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4801, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4802, - "name": "Legend of Grimrock", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 4803, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4804, - "name": "Half-Life: Paranoia", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4805, - "name": "I Am Alive", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 4806, - "name": "Painkiller", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4807, - "name": "Nox Timore", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4808, - "name": "McPixel", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Аркадные", - "Экшены", - "Головоломки" - ] - }, - { - "id": 4809, - "name": "Mr. Robot", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4810, - "name": "Legendary", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4811, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4812, - "name": "Happy Wheels", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4813, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4814, - "name": "Painkiller: Battle Out of Hell", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4815, - "name": "Marvel vs. Capcom: Infinite", - "site": "stopgame_ru", - "genres": [ - "arcade", - "fighting" - ] - }, - { - "id": 4816, - "name": "Half-Life: The Xeno Project", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4817, - "name": "Life After Us: Shipwrecked", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4818, - "name": "I am Setsuna", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 4819, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 4820, - "name": "Mr.President!", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4821, - "name": "Metal Gear Rising: Revengeance", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 4822, - "name": "Resident Evil 6", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4823, - "name": "Hard Reset", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4824, - "name": "Painkiller: Overdose", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4825, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4826, - "name": "Life Goes On: Done to Death", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4827, - "name": "Masochisia", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4828, - "name": "Omensight", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 4829, - "name": "Metal Wolf Chaos XD", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 4830, - "name": "Resident Evil 7: Biohazard", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4831, - "name": "Murdered: Soul Suspect", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица" - ] - }, - { - "id": 4832, - "name": "Life Is Strange", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4833, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4834, - "name": "Paint the Town Red", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4835, - "name": "Hard Reset: Exile (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4836, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4837, - "name": "One Hand Clapping (DEMO)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4838, - "name": "Masters of Anima", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 4839, - "name": "Limbo", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Аркада", - "Платформер" - ] - }, - { - "id": 4840, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4841, - "name": "Metro 2033", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4842, - "name": "I hate this game", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 4843, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4844, - "name": "One Night at Flumpty's", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4845, - "name": "Panty Party", - "site": "gamer_info_com", - "genres": [ - "для взрослых", - "инди", - "action" - ] - }, - { - "id": 4846, - "name": "Middle-earth: Shadow of Mordor", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4847, - "name": "Little Nightmares", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4848, - "name": "Halo: Combat Evolved", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4849, - "name": "One Night at Flumpty's 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4850, - "name": "Max Payne", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4851, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4852, - "name": "Harts", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 4853, - "name": "Metro 2033 Redux", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4854, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "playground_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 4855, - "name": "INSIDE", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 4856, - "name": "Little Nightmares - The Depths (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "Аркада", - "Платформер" - ] - }, - { - "id": 4857, - "name": "Papers, Please", - "site": "gamer_info_com", - "genres": [ - "инди" - ] - }, - { - "id": 4858, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4859, - "name": "One Piece: Burning Blood", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 4860, - "name": "Might & Magic Heroes VI", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4861, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "Аркада", - "Платформер" - ] - }, - { - "id": 4862, - "name": "Metro Exodus", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4863, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4864, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4865, - "name": "Peace, Death!", - "site": "gamer_info_com", - "genres": [ - "инди", - "аркадные" - ] - }, - { - "id": 4866, - "name": "Hatred", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 4867, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4868, - "name": "Loki: Heroes of Mythology", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4869, - "name": "Orcs Must Die!", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Боевик/Экшн" - ] - }, - { - "id": 4870, - "name": "Minecraft Story Mode", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4871, - "name": "Metro Last Light", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4872, - "name": "Indivisible", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 4873, - "name": "Max Payne 3", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4874, - "name": "Pillars of Eternity", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 4875, - "name": "Need for Speed: Underground", - "site": "playground_ru", - "genres": [ - "Аркада", - "Гонки", - "Симулятор" - ] - }, - { - "id": 4876, - "name": "Resident Evil: Operation Raccoon City", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4877, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4878, - "name": "Lost Planet: Colonies", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4879, - "name": "Mini Ninjas", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4880, - "name": "Metro Last Light Redux", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 4881, - "name": "Hellblade: Senua's Sacrifice", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 4882, - "name": "Resident Evil: Revelations", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4883, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4884, - "name": "Lost: Via Domus", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Квест" - ] - }, - { - "id": 4885, - "name": "Max: The Curse Of Brotherhood", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 4886, - "name": "Never Alone", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4887, - "name": "Injustice: Gods Among Us", - "site": "gamespot_com", - "genres": [ - "Action", - "Fighting", - "2D" - ] - }, - { - "id": 4888, - "name": "Ori and the Blind Forest", - "site": "gamebomb_ru", - "genres": [ - "Платформер" - ] - }, - { - "id": 4889, - "name": "Love Is Strange (DEMO)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4890, - "name": "Hand of Fate", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 4891, - "name": "Mirror's Edge", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4892, - "name": "Resident Evil: Revelations 2", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик", - "Приключение", - "Боевик от третьего лица" - ] - }, - { - "id": 4893, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4894, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4895, - "name": "McPixel", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4896, - "name": "Hellgate: London", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 4897, - "name": "METAL SLUG", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Шутер" - ] - }, - { - "id": 4898, - "name": "Neverending Nightmares", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 4899, - "name": "Into the Breach", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy", - "Tactical" - ] - }, - { - "id": 4900, - "name": "Reus", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4901, - "name": "Monst", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4902, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4903, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4904, - "name": "METAL SLUG 3", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4905, - "name": "Hand of Fate 2", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 4906, - "name": "Nevermind", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4907, - "name": "Metal Gear Rising: Revengeance", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4908, - "name": "Outlast", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 4909, - "name": "Rezrog", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Логика" - ] - }, - { - "id": 4910, - "name": "MINERVA: Metastasis", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4911, - "name": "Pinstripe", - "site": "gamer_info_com", - "genres": [ - "инди", - "платформеры" - ] - }, - { - "id": 4912, - "name": "Mortal Kombat IX", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4913, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4914, - "name": "Jade Empire", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 4915, - "name": "Her Story", - "site": "store_steampowered_com", - "genres": [ - "Simulation", - "Indie", - "Adventure" - ] - }, - { - "id": 4916, - "name": "Outlast: Whistleblower", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4917, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4918, - "name": "MOTHERGUNSHIP", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4919, - "name": "Happy Room", - "site": "mobygames_com", - "genres": [ - "Action", - "Simulation" - ] - }, - { - "id": 4920, - "name": "Rise of the Tomb Raider", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 4921, - "name": "Metal Wolf Chaos XD", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4922, - "name": "Pirates of the Caribbean: At World’s End", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4923, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4924, - "name": "Mortal Kombat X", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4925, - "name": "Overlord", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Боевик/Экшн" - ] - }, - { - "id": 4926, - "name": "Machinarium", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка", - "Квест" - ] - }, - { - "id": 4927, - "name": "Hero Academy", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4928, - "name": "Happy Wheels", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4929, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4930, - "name": "Neverwinter Nights", - "site": "playground_ru", - "genres": [ - "Фэнтези", - "Ролевая", - "Вид сверху" - ] - }, - { - "id": 4931, - "name": "Metro 2033", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4932, - "name": "Pizza Delivery 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4933, - "name": "Magibot", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 4934, - "name": "Overlord: Raising Hell", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Стратегия" - ] - }, - { - "id": 4935, - "name": "Middle-earth: Shadow of Mordor", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4936, - "name": "Jotun", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 4937, - "name": "Mother Russia Bleeds", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4938, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4939, - "name": "Heroes of Might and Magic III", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4940, - "name": "Magicka", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 4941, - "name": "Hard Reset", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4942, - "name": "Metro 2033 Redux", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4943, - "name": "Neverwinter Nights 2", - "site": "playground_ru", - "genres": [ - "Вид сверху", - "От третьего лица", - "Ролевая", - "Экшен", - "Фэнтези" - ] - }, - { - "id": 4944, - "name": "Plague Inc: Evolved", - "site": "gamer_info_com", - "genres": [ - "инди", - "RTS" - ] - }, - { - "id": 4945, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4946, - "name": "Mount Your Friends", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4947, - "name": "Might & Magic Heroes VI", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4948, - "name": "Magicka 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 4949, - "name": "Heroes of Might and Magic IV", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4950, - "name": "Just Cause", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4951, - "name": "Plants vs. Zombies", - "site": "gamer_info_com", - "genres": [ - "tower defense" - ] - }, - { - "id": 4952, - "name": "Metro Exodus", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4953, - "name": "Hard Reset: Exile (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 4954, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 4955, - "name": "NieR: Automata", - "site": "playground_ru", - "genres": [ - "Экшен", - "Постапокалипсис", - "Слэшер", - "Научная фантастика" - ] - }, - { - "id": 4956, - "name": "Manual Samuel", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Квест" - ] - }, - { - "id": 4957, - "name": "Oxenfree", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 4958, - "name": "Mr. Robot", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4959, - "name": "Minecraft Story Mode", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4960, - "name": "Heroes of Might and Magic V", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4961, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "action" - ] - }, - { - "id": 4962, - "name": "Nightmare House 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4963, - "name": "Mario.EXE", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 4964, - "name": "Risen", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 4965, - "name": "Harts", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 4966, - "name": "Metro Last Light", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 4967, - "name": "Just Cause 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 4968, - "name": "Painkiller", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 4969, - "name": "Mr.President!", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4970, - "name": "Marvel vs. Capcom: Infinite", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 4971, - "name": "Rock of Ages", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Стратегия" - ] - }, - { - "id": 4972, - "name": "Mini Ninjas", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Семейные" - ] - }, - { - "id": 4973, - "name": "Pony Island", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 4974, - "name": "Nosferatu: The Wrath of Malachi", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 4975, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4976, - "name": "Metro Last Light Redux", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4977, - "name": "Masochisia", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 4978, - "name": "Hatred", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4979, - "name": "Painkiller: Battle Out of Hell", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 4980, - "name": "Kane & Lynch 2: Dog Days", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 4981, - "name": "Murdered: Soul Suspect", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4982, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Стратегия" - ] - }, - { - "id": 4983, - "name": "Mirror's Edge", - "site": "ag_ru", - "genres": [ - "Экшены", - "Платформеры" - ] - }, - { - "id": 4984, - "name": "Nox Timore", - "site": "playground_ru", - "genres": [] - }, - { - "id": 4985, - "name": "Portal", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "action" - ] - }, - { - "id": 4986, - "name": "Masters of Anima", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 4987, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 4988, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 4989, - "name": "Killer is Dead: Nightmare Edition", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 4990, - "name": "Painkiller: Overdose", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 4991, - "name": "Root Of Evil: The Tailor", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 4992, - "name": "Max Payne", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 4993, - "name": "Hellblade: Senua's Sacrifice", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 4994, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 4995, - "name": "Monst", - "site": "ag_ru", - "genres": [] - }, - { - "id": 4996, - "name": "Portal 2", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "action" - ] - }, - { - "id": 4997, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Ролевая", - "Вид сверху" - ] - }, - { - "id": 4998, - "name": "Paint the Town Red", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 4999, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5000, - "name": "Runic Rampage", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 5001, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5002, - "name": "King's Bounty: Armored Princess", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 5003, - "name": "Homefront", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5004, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5005, - "name": "Mortal Kombat IX", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5006, - "name": "Panty Party", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5007, - "name": "Prey (2006)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5008, - "name": "Omensight", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Ролевая", - "Инди" - ] - }, - { - "id": 5009, - "name": "Max Payne 3", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5010, - "name": "Hellgate: London", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 5011, - "name": "Rust", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5012, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5013, - "name": "Max: The Curse Of Brotherhood", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 5014, - "name": "Prince of Persia (2008)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5015, - "name": "Papers, Please", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 5016, - "name": "One Hand Clapping (DEMO)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5017, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5018, - "name": "King's Bounty: Crossworlds", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 5019, - "name": "Mortal Kombat X", - "site": "ag_ru", - "genres": [ - "Экшены", - "Файтинги" - ] - }, - { - "id": 5020, - "name": "Hotel Remorse", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 5021, - "name": "Ryse: Son of Rome", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 5022, - "name": "McPixel", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5023, - "name": "Peace, Death!", - "site": "gamebomb_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 5024, - "name": "Prince of Persia: The Two Thrones", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5025, - "name": "One Night at Flumpty's", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5026, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5027, - "name": "Her Story", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5028, - "name": "Need for Speed: Underground", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5029, - "name": "King's Bounty: Dark Side", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 5030, - "name": "Metal Gear Rising: Revengeance", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Слэшер" - ] - }, - { - "id": 5031, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 5032, - "name": "Mother Russia Bleeds", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Аркадные", - "Файтинги" - ] - }, - { - "id": 5033, - "name": "One Night at Flumpty's 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5034, - "name": "Prospekt", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 5035, - "name": "Hotline Miami", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 5036, - "name": "Metal Wolf Chaos XD", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Роботы" - ] - }, - { - "id": 5037, - "name": "Pillars of Eternity", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 5038, - "name": "Never Alone", - "site": "iwantgames_ru", - "genres": [ - "Аркады" - ] - }, - { - "id": 5039, - "name": "Middle-earth: Shadow of Mordor", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 5040, - "name": "Hero Academy", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5041, - "name": "SCP-087", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5042, - "name": "Mount Your Friends", - "site": "ag_ru", - "genres": [ - "Спортивные", - "Инди", - "Симуляторы" - ] - }, - { - "id": 5043, - "name": "King's Bounty: The Legend", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 5044, - "name": "Metro 2033", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5045, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5046, - "name": "Might & Magic Heroes VI", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5047, - "name": "One Piece: Burning Blood", - "site": "playground_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 5048, - "name": "Prototype", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 5049, - "name": "SCP-087-B", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5050, - "name": "Neverending Nightmares", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5051, - "name": "Heroes of Might and Magic III", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5052, - "name": "Metro 2033 Redux", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5053, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5054, - "name": "Mr. Robot", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5055, - "name": "King's Bounty: Warriors of the North", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 5056, - "name": "Hotline Miami 2: Wrong Number", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 5057, - "name": "SINNER: Sacrifice for Redemption", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5058, - "name": "Prototype 2", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 5059, - "name": "Minecraft Story Mode", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5060, - "name": "Orcs Must Die!", - "site": "playground_ru", - "genres": [ - "Экшен", - "Стратегия", - "Tower defence" - ] - }, - { - "id": 5061, - "name": "Metro Exodus", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Шутер" - ] - }, - { - "id": 5062, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5063, - "name": "Nevermind", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5064, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5065, - "name": "Mr.President!", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 5066, - "name": "SWAT 4", - "site": "igromania_ru", - "genres": [ - "Тактика", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 5067, - "name": "Metro Last Light", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5068, - "name": "Psychonauts", - "site": "gamer_info_com", - "genres": [ - "приключения", - "платформеры" - ] - }, - { - "id": 5069, - "name": "Heroes of Might and Magic IV", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5070, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5071, - "name": "Mini Ninjas", - "site": "stopgame_ru", - "genres": [ - "for kids", - "action" - ] - }, - { - "id": 5072, - "name": "Pinstripe", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл", - "Платформер" - ] - }, - { - "id": 5073, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5074, - "name": "House Flipper", - "site": "store_steampowered_com", - "genres": [ - "Simulation", - "Indie" - ] - }, - { - "id": 5075, - "name": "Metro Last Light Redux", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5076, - "name": "Murdered: Soul Suspect", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 5077, - "name": "Kingdom Rush", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 5078, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5079, - "name": "PuniTy", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5080, - "name": "Ori and the Blind Forest", - "site": "playground_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 5081, - "name": "Pirates of the Caribbean: At World’s End", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5082, - "name": "Mirror's Edge", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5083, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5084, - "name": "Neverwinter Nights", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5085, - "name": "Kingdom: Classic", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5086, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5087, - "name": "Heroes of Might and Magic V", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5088, - "name": "P·O·L·L·E·N", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5089, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Файтинги" - ] - }, - { - "id": 5090, - "name": "Pizza Delivery 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5091, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5092, - "name": "I Am Alive", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5093, - "name": "Outlast", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Ужасы" - ] - }, - { - "id": 5094, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5095, - "name": "Monst", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5096, - "name": "Neverwinter Nights 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5097, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5098, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "ag_ru", - "genres": [ - "Файтинги" - ] - }, - { - "id": 5099, - "name": "Quake 4", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5100, - "name": "Kingdoms of Amalur: Reckoning", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 5101, - "name": "Plague Inc: Evolved", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Симулятор" - ] - }, - { - "id": 5102, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 5103, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5104, - "name": "Outlast: Whistleblower", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5105, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5106, - "name": "NieR: Automata", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "РПГ" - ] - }, - { - "id": 5107, - "name": "Mortal Kombat IX", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5108, - "name": "Quantum Break", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 5109, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 5110, - "name": "I am Setsuna", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 5111, - "name": "Plants vs. Zombies", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Головоломки/Пазл" - ] - }, - { - "id": 5112, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5113, - "name": "Middle-earth: Shadow of Mordor", - "site": "gameguru_ru", - "genres": [ - "Открытый мир", - "RPG", - "aRPG" - ] - }, - { - "id": 5114, - "name": "Overlord", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5115, - "name": "Nightmare House 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5116, - "name": "Knock-Knock", - "site": "gamespot_com", - "genres": [ - "Miscellaneous" - ] - }, - { - "id": 5117, - "name": "Need for Speed: Underground", - "site": "ag_ru", - "genres": [ - "Гонки", - "Аркадные" - ] - }, - { - "id": 5118, - "name": "Mortal Kombat X", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5119, - "name": "Raft", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5120, - "name": "Might & Magic Heroes VI", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5121, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "gamebomb_ru", - "genres": [ - "Многопользоватеский шутер" - ] - }, - { - "id": 5122, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5123, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5124, - "name": "Overlord: Raising Hell", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5125, - "name": "I hate this game", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 5126, - "name": "Nosferatu: The Wrath of Malachi", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5127, - "name": "Minecraft Story Mode", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5128, - "name": "Rage", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 5129, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5130, - "name": "Never Alone", - "site": "ag_ru", - "genres": [ - "Аркадные" - ] - }, - { - "id": 5131, - "name": "Mother Russia Bleeds", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5132, - "name": "Pony Island", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5133, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 5134, - "name": "Homefront", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5135, - "name": "Mini Ninjas", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Квест" - ] - }, - { - "id": 5136, - "name": "Oxenfree", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Инди" - ] - }, - { - "id": 5137, - "name": "Nox Timore", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5138, - "name": "Rampage Knights", - "site": "gamer_info_com", - "genres": [ - "инди", - "beat 'em up" - ] - }, - { - "id": 5139, - "name": "Sacred", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5140, - "name": "Neverending Nightmares", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 5141, - "name": "Mount Your Friends", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5142, - "name": "Portal", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл", - "Боевик/Экшн" - ] - }, - { - "id": 5143, - "name": "INSIDE", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 5144, - "name": "Mirror's Edge", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Симулятор", - "Шутер" - ] - }, - { - "id": 5145, - "name": "Hotel Remorse", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5146, - "name": "Lara Croft and the Guardian of Light", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5147, - "name": "Rayman Legends", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 5148, - "name": "Sacred 3", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5149, - "name": "Painkiller", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 5150, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5151, - "name": "Monst", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5152, - "name": "Nevermind", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 5153, - "name": "Mr. Robot", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 5154, - "name": "Portal 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Головоломки/Пазл", - "Боевик/Экшн" - ] - }, - { - "id": 5155, - "name": "Rayman Origins", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 5156, - "name": "Mortal Kombat IX", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5157, - "name": "Hotline Miami", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5158, - "name": "Sacred Citadel", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер", - "Боевик" - ] - }, - { - "id": 5159, - "name": "Painkiller: Battle Out of Hell", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5160, - "name": "Indivisible", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 5161, - "name": "Omensight", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5162, - "name": "Lara Croft and the Temple of Osiris", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5163, - "name": "Mortal Kombat X", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 5164, - "name": "Mr.President!", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5165, - "name": "Prey (2006)", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 5166, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5167, - "name": "Rayman Raving Rabbids", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5168, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5169, - "name": "Painkiller: Overdose", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5170, - "name": "Injustice: Gods Among Us", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5171, - "name": "Hotline Miami 2: Wrong Number", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5172, - "name": "One Hand Clapping (DEMO)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5173, - "name": "Mother Russia Bleeds", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Beat-em-up" - ] - }, - { - "id": 5174, - "name": "Prince of Persia (2008)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5175, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5176, - "name": "Late Shift", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5177, - "name": "Rayman Raving Rabbids 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5178, - "name": "Murdered: Soul Suspect", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5179, - "name": "Mount Your Friends", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5180, - "name": "Neverwinter Nights", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5181, - "name": "Paint the Town Red", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Выживание" - ] - }, - { - "id": 5182, - "name": "Prince of Persia: The Two Thrones", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 5183, - "name": "One Night at Flumpty's", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5184, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5185, - "name": "House Flipper", - "site": "mobygames_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 5186, - "name": "Mr. Robot", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка" - ] - }, - { - "id": 5187, - "name": "Real Horror Stories", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5188, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5189, - "name": "Layers of Fear", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 5190, - "name": "Into the Breach", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Simulation", - "Strategy", - "Indie" - ] - }, - { - "id": 5191, - "name": "Neverwinter Nights 2", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 5192, - "name": "Prospekt", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5193, - "name": "Saints Row IV", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5194, - "name": "Mr.President!", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5195, - "name": "One Night at Flumpty's 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5196, - "name": "Panty Party", - "site": "playground_ru", - "genres": [ - "Экшен", - "Юмор", - "Инди" - ] - }, - { - "id": 5197, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5198, - "name": "Redeemer", - "site": "gamer_info_com", - "genres": [ - "инди", - "action" - ] - }, - { - "id": 5199, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5200, - "name": "I Am Alive", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5201, - "name": "Jade Empire", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5202, - "name": "Murdered: Soul Suspect", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 5203, - "name": "Prototype", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5204, - "name": "Saints Row: Gat out of Hell", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 5205, - "name": "One Piece: Burning Blood", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5206, - "name": "Papers, Please", - "site": "playground_ru", - "genres": [ - "Другой симулятор", - "Ретро", - "Адвенчура", - "Инди" - ] - }, - { - "id": 5207, - "name": "NieR: Automata", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 5208, - "name": "Reign Of Kings", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5209, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5210, - "name": "Left 4 Dead", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 5211, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5212, - "name": "Salt and Sanctuary", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 5213, - "name": "Jotun", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5214, - "name": "I am Setsuna", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 5215, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 5216, - "name": "Nightmare House 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5217, - "name": "Orcs Must Die!", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5218, - "name": "Reigns", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди" - ] - }, - { - "id": 5219, - "name": "Peace, Death!", - "site": "playground_ru", - "genres": [ - "Ретро", - "Юмор", - "Инди" - ] - }, - { - "id": 5220, - "name": "Samsara Room", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 5221, - "name": "Need for Speed: Underground", - "site": "stopgame_ru", - "genres": [ - "arcade", - "racing" - ] - }, - { - "id": 5222, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5223, - "name": "I hate this game", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5224, - "name": "Left 4 Dead 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 5225, - "name": "Nosferatu: The Wrath of Malachi", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5226, - "name": "Reigns: Her Majesty", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди" - ] - }, - { - "id": 5227, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5228, - "name": "Pillars of Eternity", - "site": "playground_ru", - "genres": [ - "Фэнтези", - "Адвенчура", - "Ролевая", - "Вид сверху" - ] - }, - { - "id": 5229, - "name": "Just Cause", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5230, - "name": "Need for Speed: Underground", - "site": "gameguru_ru", - "genres": [ - "Гонки/вождение" - ] - }, - { - "id": 5231, - "name": "Saw: The Video Game", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5232, - "name": "Never Alone", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5233, - "name": "Prototype 2", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 5234, - "name": "Never Alone", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 5235, - "name": "INSIDE", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5236, - "name": "Legend Hand of God", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 5237, - "name": "Nox Timore", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5238, - "name": "Resident Evil", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 5239, - "name": "Scratches", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 5240, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5241, - "name": "Ori and the Blind Forest", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5242, - "name": "Neverending Nightmares", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 5243, - "name": "Psychonauts", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Платформер" - ] - }, - { - "id": 5244, - "name": "Neverending Nightmares", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5245, - "name": "Just Cause 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5246, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 5247, - "name": "Seasons after Fall", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 5248, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5249, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5250, - "name": "Legend of Grimrock", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 5251, - "name": "Indivisible", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 5252, - "name": "Nevermind", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5253, - "name": "Outlast", - "site": "iwantgames_ru", - "genres": [ - "Ужасы" - ] - }, - { - "id": 5254, - "name": "PuniTy", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5255, - "name": "Serena", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 5256, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5257, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5258, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5259, - "name": "Nevermind", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5260, - "name": "Omensight", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 5261, - "name": "Injustice: Gods Among Us", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5262, - "name": "Outlast: Whistleblower", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5263, - "name": "P·O·L·L·E·N", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 5264, - "name": "Neverwinter Nights", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 5265, - "name": "Serious Sam 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 5266, - "name": "Pinstripe", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5267, - "name": "Legendary", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 5268, - "name": "Kane & Lynch 2: Dog Days", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5269, - "name": "Resident Evil 4", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "ужасы", - "action" - ] - }, - { - "id": 5270, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5271, - "name": "One Hand Clapping (DEMO)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5272, - "name": "Quake 4", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 5273, - "name": "Neverwinter Nights 2", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 5274, - "name": "Overlord", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5275, - "name": "Serious Sam 3: BFE", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 5276, - "name": "Life After Us: Shipwrecked", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5277, - "name": "Pirates of the Caribbean: At World’s End", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5278, - "name": "Into the Breach", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5279, - "name": "Resident Evil 5", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "ужасы", - "приключения", - "action" - ] - }, - { - "id": 5280, - "name": "NieR: Automata", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Слэшер", - "Роботы" - ] - }, - { - "id": 5281, - "name": "Neverwinter Nights", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 5282, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5283, - "name": "One Night at Flumpty's", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5284, - "name": "Overlord: Raising Hell", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5285, - "name": "Life Goes On: Done to Death", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5286, - "name": "Killer is Dead: Nightmare Edition", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5287, - "name": "Quantum Break", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 5288, - "name": "Pizza Delivery 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5289, - "name": "Nightmare House 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5290, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5291, - "name": "Jade Empire", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5292, - "name": "Serious Sam Classic: The First Encounter", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5293, - "name": "Neverwinter Nights 2", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 5294, - "name": "Plague Inc: Evolved", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5295, - "name": "Nosferatu: The Wrath of Malachi", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5296, - "name": "Oxenfree", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5297, - "name": "Raft", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5298, - "name": "Life Is Strange", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5299, - "name": "One Night at Flumpty's 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5300, - "name": "Serious Sam Classic: The Second Encounter", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5301, - "name": "Jotun", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5302, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5303, - "name": "Nox Timore", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5304, - "name": "NieR: Automata", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 5305, - "name": "Rage", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5306, - "name": "King's Bounty: Armored Princess", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy" - ] - }, - { - "id": 5307, - "name": "Painkiller", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5308, - "name": "Plants vs. Zombies", - "site": "playground_ru", - "genres": [ - "Зомби", - "Аркада", - "Tower defence" - ] - }, - { - "id": 5309, - "name": "Shadow Warrior", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 5310, - "name": "One Piece: Burning Blood", - "site": "ag_ru", - "genres": [ - "Файтинги", - "Аркадные" - ] - }, - { - "id": 5311, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Квест" - ] - }, - { - "id": 5312, - "name": "Resident Evil 6", - "site": "gamer_info_com", - "genres": [ - "шутеры от 3-го лица", - "ужасы", - "action" - ] - }, - { - "id": 5313, - "name": "Rampage Knights", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5314, - "name": "Nightmare House 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5315, - "name": "Just Cause", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 5316, - "name": "Omensight", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG", - "Инди" - ] - }, - { - "id": 5317, - "name": "Painkiller: Battle Out of Hell", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5318, - "name": "Shadow of the Tomb Raider", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Боевик" - ] - }, - { - "id": 5319, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "От третьего лица", - "Шутер" - ] - }, - { - "id": 5320, - "name": "Resident Evil 7: Biohazard", - "site": "gamer_info_com", - "genres": [ - "ужасы" - ] - }, - { - "id": 5321, - "name": "One Hand Clapping (DEMO)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5322, - "name": "Limbo", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 5323, - "name": "Orcs Must Die!", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5324, - "name": "King's Bounty: Crossworlds", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy" - ] - }, - { - "id": 5325, - "name": "Rayman Legends", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Боевик/Экшн" - ] - }, - { - "id": 5326, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5327, - "name": "Painkiller: Overdose", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5328, - "name": "Nosferatu: The Wrath of Malachi", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5329, - "name": "One Night at Flumpty's", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 5330, - "name": "Just Cause 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5331, - "name": "Pony Island", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 5332, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5333, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5334, - "name": "Shantae: Half-Genie Hero", - "site": "igromania_ru", - "genres": [ - "Платформер" - ] - }, - { - "id": 5335, - "name": "Rayman Origins", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 5336, - "name": "Little Nightmares", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5337, - "name": "Paint the Town Red", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5338, - "name": "One Night at Flumpty's 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5339, - "name": "Nox Timore", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5340, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5341, - "name": "Portal", - "site": "playground_ru", - "genres": [ - "Экшен", - "Логические", - "Научная фантастика" - ] - }, - { - "id": 5342, - "name": "Ori and the Blind Forest", - "site": "ag_ru", - "genres": [ - "Платформеры" - ] - }, - { - "id": 5343, - "name": "King's Bounty: Dark Side", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy" - ] - }, - { - "id": 5344, - "name": "Shoot Your Nightmare Halloween Special", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5345, - "name": "One Piece: Burning Blood", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5346, - "name": "Little Nightmares - The Depths (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5347, - "name": "Rayman Raving Rabbids", - "site": "gamebomb_ru", - "genres": [ - "Платформер" - ] - }, - { - "id": 5348, - "name": "Panty Party", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5349, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5350, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5351, - "name": "Orcs Must Die!", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Стратегия" - ] - }, - { - "id": 5352, - "name": "Outlast", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 5353, - "name": "Shovel Knight: Shovel of Hope", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5354, - "name": "Kane & Lynch 2: Dog Days", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5355, - "name": "Portal 2", - "site": "playground_ru", - "genres": [ - "Логические", - "От первого лица", - "Научная фантастика" - ] - }, - { - "id": 5356, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5357, - "name": "Rayman Raving Rabbids 2", - "site": "gamebomb_ru", - "genres": [ - "Музыка/Ритм", - "Миниигры" - ] - }, - { - "id": 5358, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5359, - "name": "Papers, Please", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5360, - "name": "Omensight", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5361, - "name": "Shrek 2: The Game", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5362, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5363, - "name": "Loki: Heroes of Mythology", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5364, - "name": "Prey (2006)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5365, - "name": "King's Bounty: The Legend", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy" - ] - }, - { - "id": 5366, - "name": "Real Horror Stories", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5367, - "name": "Killer is Dead: Nightmare Edition", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5368, - "name": "Outlast: Whistleblower", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 5369, - "name": "Ori and the Blind Forest", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 5370, - "name": "Silence on The Line", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5371, - "name": "Resident Evil: Operation Raccoon City", - "site": "gamer_info_com", - "genres": [ - "командные шутеры" - ] - }, - { - "id": 5372, - "name": "Peace, Death!", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5373, - "name": "Lost Planet: Colonies", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5374, - "name": "One Hand Clapping (DEMO)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5375, - "name": "Outlast", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5376, - "name": "Redeemer", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5377, - "name": "Prince of Persia (2008)", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Адвенчура", - "Аркада", - "Экшен", - "Открытый мир", - "Платформер", - "Фэнтези" - ] - }, - { - "id": 5378, - "name": "Overlord", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 5379, - "name": "Silent Hill: Alchemilla", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5380, - "name": "Resident Evil: Revelations", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 5381, - "name": "Outlast: Whistleblower", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 5382, - "name": "King's Bounty: Armored Princess", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5383, - "name": "Pillars of Eternity", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5384, - "name": "King's Bounty: Warriors of the North", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy" - ] - }, - { - "id": 5385, - "name": "Reign Of Kings", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 5386, - "name": "One Night at Flumpty's", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5387, - "name": "Overlord: Raising Hell", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 5388, - "name": "Prince of Persia: The Two Thrones", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Платформер" - ] - }, - { - "id": 5389, - "name": "Silverfall", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5390, - "name": "Overlord", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 5391, - "name": "Lost: Via Domus", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5392, - "name": "Resident Evil: Revelations 2", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 5393, - "name": "Reigns", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5394, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5395, - "name": "Prospekt", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5396, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5397, - "name": "King's Bounty: Crossworlds", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 5398, - "name": "Overlord: Raising Hell", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5399, - "name": "Silverfall: Earth Awakening", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5400, - "name": "One Night at Flumpty's 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5401, - "name": "Love Is Strange (DEMO)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5402, - "name": "Reigns: Her Majesty", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5403, - "name": "Oxenfree", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 5404, - "name": "Reus", - "site": "gamer_info_com", - "genres": [ - "симуляторы бога" - ] - }, - { - "id": 5405, - "name": "Oxenfree", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 5406, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5407, - "name": "Singularity", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5408, - "name": "Painkiller", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5409, - "name": "Prototype", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5410, - "name": "Resident Evil", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 5411, - "name": "One Piece: Burning Blood", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5412, - "name": "Painkiller", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5413, - "name": "Rezrog", - "site": "gamer_info_com", - "genres": [ - "инди", - "тактика", - "TBS" - ] - }, - { - "id": 5414, - "name": "King's Bounty: Dark Side", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics", - "Role-Playing (RPG)" - ] - }, - { - "id": 5415, - "name": "Kingdom Rush", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie" - ] - }, - { - "id": 5416, - "name": "Slap-Man", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5417, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5418, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5419, - "name": "METAL SLUG", - "site": "gamespot_com", - "genres": [ - "Action", - "Shoot-'Em-Up", - "2D" - ] - }, - { - "id": 5420, - "name": "Painkiller: Battle Out of Hell", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5421, - "name": "Prototype 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир" - ] - }, - { - "id": 5422, - "name": "Orcs Must Die!", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5423, - "name": "Painkiller: Battle Out of Hell", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5424, - "name": "Rise of the Tomb Raider", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 5425, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5426, - "name": "Slay the Spire", - "site": "igromania_ru", - "genres": [ - "Карточная игра", - "Пошаговая стратегия" - ] - }, - { - "id": 5427, - "name": "Pinstripe", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5428, - "name": "Painkiller: Overdose", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5429, - "name": "King's Bounty: The Legend", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5430, - "name": "Painkiller: Overdose", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5431, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5432, - "name": "Psychonauts", - "site": "playground_ru", - "genres": [ - "Юмор", - "От третьего лица", - "Аркада", - "Экшен", - "Платформер" - ] - }, - { - "id": 5433, - "name": "Kingdom: Classic", - "site": "store_steampowered_com", - "genres": [ - "Simulation", - "Strategy", - "Indie" - ] - }, - { - "id": 5434, - "name": "Sleeping Dogs", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 5435, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5436, - "name": "Paint the Town Red", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5437, - "name": "Resident Evil 4", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 5438, - "name": "Pirates of the Caribbean: At World’s End", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5439, - "name": "METAL SLUG 3", - "site": "gamespot_com", - "genres": [ - "Action", - "2D", - "Shoot-'Em-Up" - ] - }, - { - "id": 5440, - "name": "Paint the Town Red", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди" - ] - }, - { - "id": 5441, - "name": "Panty Party", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди" - ] - }, - { - "id": 5442, - "name": "PuniTy", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5443, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5444, - "name": "Slender: The Arrival", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 5445, - "name": "Ori and the Blind Forest", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5446, - "name": "Resident Evil 5", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер" - ] - }, - { - "id": 5447, - "name": "King's Bounty: Warriors of the North", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics", - "Role-Playing (RPG)" - ] - }, - { - "id": 5448, - "name": "Papers, Please", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка" - ] - }, - { - "id": 5449, - "name": "Pizza Delivery 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5450, - "name": "Kingdoms of Amalur: Reckoning", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 5451, - "name": "Slendytubbies", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5452, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5453, - "name": "Panty Party", - "site": "ag_ru", - "genres": [ - "Приключения", - "Инди", - "Экшены", - "Казуальные", - "Головоломки", - "Симуляторы" - ] - }, - { - "id": 5454, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5455, - "name": "P·O·L·L·E·N", - "site": "playground_ru", - "genres": [ - "Виртуальная реальность", - "От первого лица", - "Космос" - ] - }, - { - "id": 5456, - "name": "MINERVA: Metastasis", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5457, - "name": "Outlast", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5458, - "name": "Peace, Death!", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Инди" - ] - }, - { - "id": 5459, - "name": "Slime Rancher", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Логика", - "Боевик" - ] - }, - { - "id": 5460, - "name": "Plague Inc: Evolved", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5461, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5462, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 5463, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5464, - "name": "Papers, Please", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Ролевые", - "Инди", - "Симуляторы" - ] - }, - { - "id": 5465, - "name": "Pillars of Eternity", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 5466, - "name": "Knock-Knock", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 5467, - "name": "Outlast: Whistleblower", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 5468, - "name": "Quake 4", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 5469, - "name": "Sniper Elite", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 5470, - "name": "Plants vs. Zombies", - "site": "iwantgames_ru", - "genres": [ - "Стратегии" - ] - }, - { - "id": 5471, - "name": "Risen", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 5472, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5473, - "name": "Peace, Death!", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Симуляторы", - "Аркадные" - ] - }, - { - "id": 5474, - "name": "MOTHERGUNSHIP", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 5475, - "name": "Kingdom Rush", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 5476, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5477, - "name": "Quantum Break", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица" - ] - }, - { - "id": 5478, - "name": "Sniper Elite 3", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 5479, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5480, - "name": "Overlord", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5481, - "name": "Resident Evil 6", - "site": "gamebomb_ru", - "genres": [ - "Шутер" - ] - }, - { - "id": 5482, - "name": "Rock of Ages", - "site": "gamer_info_com", - "genres": [ - "аркадные", - "action", - "TBS" - ] - }, - { - "id": 5483, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5484, - "name": "Pillars of Eternity", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 5485, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5486, - "name": "Sniper Elite V2", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 5487, - "name": "Kingdom: Classic", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5488, - "name": "Overlord: Raising Hell", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5489, - "name": "Raft", - "site": "playground_ru", - "genres": [ - "Инди", - "Адвенчура", - "Выживание", - "Открытый мир", - "От первого лица" - ] - }, - { - "id": 5490, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5491, - "name": "Pony Island", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5492, - "name": "Resident Evil 7: Biohazard", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5493, - "name": "Pinstripe", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 5494, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5495, - "name": "Machinarium", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5496, - "name": "Sophie's Curse", - "site": "igromania_ru", - "genres": [ - "Ужасы" - ] - }, - { - "id": 5497, - "name": "Lara Croft and the Guardian of Light", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5498, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5499, - "name": "Pirates of the Caribbean: At World’s End", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 5500, - "name": "Oxenfree", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 5501, - "name": "Root Of Evil: The Tailor", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 5502, - "name": "Portal", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5503, - "name": "Rage", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5504, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Приключения" - ] - }, - { - "id": 5505, - "name": "South Park: The Fractured But Whole", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Ролевая игра" - ] - }, - { - "id": 5506, - "name": "Kingdoms of Amalur: Reckoning", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 5507, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5508, - "name": "Pizza Delivery 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5509, - "name": "Runic Rampage", - "site": "gamer_info_com", - "genres": [ - "инди", - "hack & slash" - ] - }, - { - "id": 5510, - "name": "Painkiller", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5511, - "name": "Portal 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5512, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5513, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Приключения" - ] - }, - { - "id": 5514, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5515, - "name": "Rampage Knights", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "Адвенчура", - "Инди" - ] - }, - { - "id": 5516, - "name": "Lara Croft and the Temple of Osiris", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5517, - "name": "Plague Inc: Evolved", - "site": "gameguru_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 5518, - "name": "Knock-Knock", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5519, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5520, - "name": "Rust", - "site": "gamer_info_com", - "genres": [ - "инди", - "survival" - ] - }, - { - "id": 5521, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5522, - "name": "Painkiller: Battle Out of Hell", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5523, - "name": "Plants vs. Zombies", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Стратегия" - ] - }, - { - "id": 5524, - "name": "Prey (2006)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5525, - "name": "Rayman Legends", - "site": "playground_ru", - "genres": [ - "Аркада", - "Юмор", - "Платформер" - ] - }, - { - "id": 5526, - "name": "Pinstripe", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Головоломки", - "Казуальные", - "Платформеры" - ] - }, - { - "id": 5527, - "name": "Magibot", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 5528, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5529, - "name": "South Park: The Stick of Truth", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5530, - "name": "Ryse: Son of Rome", - "site": "gamer_info_com", - "genres": [ - "приключения", - "action" - ] - }, - { - "id": 5531, - "name": "Late Shift", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 5532, - "name": "Painkiller: Overdose", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5533, - "name": "Resident Evil: Operation Raccoon City", - "site": "gamebomb_ru", - "genres": [ - "Шутер" - ] - }, - { - "id": 5534, - "name": "Prince of Persia (2008)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5535, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5536, - "name": "Rayman Origins", - "site": "playground_ru", - "genres": [ - "Аркада", - "Юмор", - "Платформер" - ] - }, - { - "id": 5537, - "name": "Pony Island", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка" - ] - }, - { - "id": 5538, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5539, - "name": "Pirates of the Caribbean: At World’s End", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5540, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5541, - "name": "Paint the Town Red", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5542, - "name": "Prince of Persia: The Two Thrones", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5543, - "name": "Portal", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "На логику", - "Головоломка" - ] - }, - { - "id": 5544, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5545, - "name": "Magicka", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5546, - "name": "Layers of Fear", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 5547, - "name": "Rayman Raving Rabbids", - "site": "playground_ru", - "genres": [ - "Аркада", - "Юмор" - ] - }, - { - "id": 5548, - "name": "Pizza Delivery 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5549, - "name": "Resident Evil: Revelations", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 5550, - "name": "Lara Croft and the Guardian of Light", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5551, - "name": "SCP-087", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5552, - "name": "Portal 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "На логику", - "Головоломка" - ] - }, - { - "id": 5553, - "name": "Panty Party", - "site": "stopgame_ru", - "genres": [ - "arcade", - "for adults" - ] - }, - { - "id": 5554, - "name": "Prospekt", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5555, - "name": "Prey (2006)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5556, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5557, - "name": "Rayman Raving Rabbids 2", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 5558, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5559, - "name": "Magicka 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5560, - "name": "SCP-087-B", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5561, - "name": "Resident Evil: Revelations 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 5562, - "name": "Plague Inc: Evolved", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 5563, - "name": "Papers, Please", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 5564, - "name": "Lara Croft and the Temple of Osiris", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5565, - "name": "Prototype", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5566, - "name": "Prince of Persia (2008)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5567, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5568, - "name": "Reus", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5569, - "name": "Real Horror Stories", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5570, - "name": "SINNER: Sacrifice for Redemption", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди", - "action" - ] - }, - { - "id": 5571, - "name": "Plants vs. Zombies", - "site": "ag_ru", - "genres": [ - "Экшены", - "Головоломки", - "Аркадные", - "Стратегии" - ] - }, - { - "id": 5572, - "name": "Manual Samuel", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 5573, - "name": "Prince of Persia: The Two Thrones", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5574, - "name": "Rezrog", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5575, - "name": "Prototype 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5576, - "name": "Left 4 Dead", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5577, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5578, - "name": "Peace, Death!", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5579, - "name": "Late Shift", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5580, - "name": "Prospekt", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5581, - "name": "Mario.EXE", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5582, - "name": "Redeemer", - "site": "playground_ru", - "genres": [ - "Экшен", - "Инди", - "Файтинг", - "Вид сверху" - ] - }, - { - "id": 5583, - "name": "SWAT 4", - "site": "gamer_info_com", - "genres": [ - "FPS", - "тактика" - ] - }, - { - "id": 5584, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5585, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 5586, - "name": "Prototype", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5587, - "name": "Psychonauts", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5588, - "name": "Rise of the Tomb Raider", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Боевик/Экшн" - ] - }, - { - "id": 5589, - "name": "Pillars of Eternity", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 5590, - "name": "Reign Of Kings", - "site": "playground_ru", - "genres": [ - "Инди", - "Мультиплеер", - "Средневековье", - "Ролевая", - "Выживание", - "Экшен", - "От первого лица" - ] - }, - { - "id": 5591, - "name": "Layers of Fear", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5592, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5593, - "name": "Prototype 2", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5594, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5595, - "name": "Left 4 Dead 2", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5596, - "name": "Marvel vs. Capcom: Infinite", - "site": "gamespot_com", - "genres": [ - "Action", - "Fighting", - "2D" - ] - }, - { - "id": 5597, - "name": "Pony Island", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Инди", - "Аркадные", - "Приключения" - ] - }, - { - "id": 5598, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5599, - "name": "PuniTy", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5600, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5601, - "name": "Psychonauts", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 5602, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5603, - "name": "Reigns", - "site": "playground_ru", - "genres": [ - "Юмор", - "Инди", - "Адвенчура", - "Аркада", - "Карточная", - "Ролевая" - ] - }, - { - "id": 5604, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5605, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5606, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 5607, - "name": "Legend Hand of God", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5608, - "name": "PuniTy", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5609, - "name": "Portal", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Головоломки" - ] - }, - { - "id": 5610, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5611, - "name": "P·O·L·L·E·N", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5612, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5613, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5614, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5615, - "name": "Reigns: Her Majesty", - "site": "playground_ru", - "genres": [ - "Юмор", - "Инди", - "Аркада", - "Карточная", - "Ролевая", - "Симулятор" - ] - }, - { - "id": 5616, - "name": "P·O·L·L·E·N", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 5617, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5618, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5619, - "name": "Portal 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Головоломки" - ] - }, - { - "id": 5620, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5621, - "name": "Quake 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5622, - "name": "Quake 4", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5623, - "name": "Masochisia", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5624, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5625, - "name": "Legend of Grimrock", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 5626, - "name": "Resident Evil", - "site": "playground_ru", - "genres": [ - "Экшен", - "Зомби", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 5627, - "name": "Left 4 Dead", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5628, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5629, - "name": "Risen", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 5630, - "name": "Quantum Break", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5631, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5632, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5633, - "name": "Quantum Break", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5634, - "name": "Prey (2006)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 5635, - "name": "Pinstripe", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5636, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5637, - "name": "Masters of Anima", - "site": "gamespot_com", - "genres": [ - "MOBA", - "Strategy" - ] - }, - { - "id": 5638, - "name": "Raft", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди", - "Симулятор" - ] - }, - { - "id": 5639, - "name": "Rock of Ages", - "site": "gamebomb_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 5640, - "name": "Legendary", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5641, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5642, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5643, - "name": "Spooky's House of Jump Scares", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5644, - "name": "Rage", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Гонки/вождение", - "Шутер" - ] - }, - { - "id": 5645, - "name": "Raft", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5646, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5647, - "name": "Prince of Persia (2008)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5648, - "name": "Pirates of the Caribbean: At World’s End", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5649, - "name": "Max Payne", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 5650, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5651, - "name": "Life After Us: Shipwrecked", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5652, - "name": "Rampage Knights", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Инди" - ] - }, - { - "id": 5653, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик", - "Боевик от третьего лица" - ] - }, - { - "id": 5654, - "name": "Root Of Evil: The Tailor", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5655, - "name": "Left 4 Dead 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5656, - "name": "Resident Evil 4", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 5657, - "name": "Rage", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5658, - "name": "Rayman Legends", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 5659, - "name": "Prince of Persia: The Two Thrones", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5660, - "name": "Pizza Delivery 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5661, - "name": "Runic Rampage", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5662, - "name": "Sacred", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5663, - "name": "Star Wars: The Force Unleashed", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 5664, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 5665, - "name": "Rayman Origins", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада" - ] - }, - { - "id": 5666, - "name": "Resident Evil 5", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Кооператив", - "Зомби", - "Шутер", - "Экшен" - ] - }, - { - "id": 5667, - "name": "Rampage Knights", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5668, - "name": "Legend Hand of God", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 5669, - "name": "Life Goes On: Done to Death", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Casual", - "Adventure" - ] - }, - { - "id": 5670, - "name": "Plague Inc: Evolved", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 5671, - "name": "Prospekt", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 5672, - "name": "Sacred 3", - "site": "gamer_info_com", - "genres": [ - "RPG", - "hack & slash" - ] - }, - { - "id": 5673, - "name": "Star Wars: The Force Unleashed II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5674, - "name": "Rayman Raving Rabbids", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Для детей" - ] - }, - { - "id": 5675, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5676, - "name": "Rust", - "site": "gamebomb_ru", - "genres": [ - "Многопользоватеский шутер", - "Боевик-приключения" - ] - }, - { - "id": 5677, - "name": "Max Payne 3", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 5678, - "name": "Rayman Legends", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5679, - "name": "SteamWorld Dig", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 5680, - "name": "Life Is Strange", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5681, - "name": "Sacred Citadel", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 5682, - "name": "Rayman Raving Rabbids 2", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Для детей" - ] - }, - { - "id": 5683, - "name": "Prototype", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 5684, - "name": "Legend of Grimrock", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 5685, - "name": "Plants vs. Zombies", - "site": "stopgame_ru", - "genres": [ - "strategy", - "arcade" - ] - }, - { - "id": 5686, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5687, - "name": "Real Horror Stories", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5688, - "name": "SteamWorld Dig 2", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Приключение" - ] - }, - { - "id": 5689, - "name": "Ryse: Son of Rome", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5690, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5691, - "name": "Rayman Origins", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5692, - "name": "Prototype 2", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 5693, - "name": "Max: The Curse Of Brotherhood", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5694, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "stopgame_ru", - "genres": [ - "online", - "action" - ] - }, - { - "id": 5695, - "name": "Legendary", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5696, - "name": "Redeemer", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5697, - "name": "Limbo", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 5698, - "name": "Resident Evil 6", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Ужасы", - "Кооператив", - "Шутер", - "Экшен" - ] - }, - { - "id": 5699, - "name": "SteamWorld Heist", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Стратегия", - "Пошаговая стратегия", - "Боевик" - ] - }, - { - "id": 5700, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5701, - "name": "Psychonauts", - "site": "ag_ru", - "genres": [ - "Экшены", - "Платформеры" - ] - }, - { - "id": 5702, - "name": "Reign Of Kings", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Симулятор" - ] - }, - { - "id": 5703, - "name": "Rayman Raving Rabbids", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5704, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 5705, - "name": "Life After Us: Shipwrecked", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5706, - "name": "McPixel", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5707, - "name": "Pony Island", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5708, - "name": "Resident Evil 7: Biohazard", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5709, - "name": "Steampunk Tower 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5710, - "name": "Reigns", - "site": "gameguru_ru", - "genres": [ - "В реальном времени", - "Инди", - "Стратегия", - "Квест" - ] - }, - { - "id": 5711, - "name": "SCP-087", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5712, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5713, - "name": "PuniTy", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5714, - "name": "Rayman Raving Rabbids 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5715, - "name": "Little Nightmares", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5716, - "name": "Steel Rats", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Боевик" - ] - }, - { - "id": 5717, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5718, - "name": "Portal", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 5719, - "name": "Reigns: Her Majesty", - "site": "gameguru_ru", - "genres": [ - "В реальном времени", - "Инди", - "Стратегия", - "Квест" - ] - }, - { - "id": 5720, - "name": "Life Goes On: Done to Death", - "site": "mobygames_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 5721, - "name": "SCP-087-B", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5722, - "name": "Metal Gear Rising: Revengeance", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5723, - "name": "Saints Row IV", - "site": "gamer_info_com", - "genres": [ - "для взрослых", - "action" - ] - }, - { - "id": 5724, - "name": "Resident Evil", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5725, - "name": "Stories: The Path of Destinies", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5726, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5727, - "name": "Real Horror Stories", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5728, - "name": "Little Nightmares - The Depths (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5729, - "name": "Life Is Strange", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5730, - "name": "Portal 2", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 5731, - "name": "P·O·L·L·E·N", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 5732, - "name": "Metal Wolf Chaos XD", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5733, - "name": "SINNER: Sacrifice for Redemption", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 5734, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5735, - "name": "Saints Row: Gat out of Hell", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 5736, - "name": "Street Fighter X Tekken", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 5737, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5738, - "name": "Redeemer", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5739, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5740, - "name": "Quake 4", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5741, - "name": "SWAT 4", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн", - "Стратегия", - "Симулятор", - "Шутер" - ] - }, - { - "id": 5742, - "name": "Prey (2006)", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5743, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5744, - "name": "Limbo", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5745, - "name": "Metro 2033", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 5746, - "name": "Salt and Sanctuary", - "site": "gamer_info_com", - "genres": [ - "RPG", - "платформеры" - ] - }, - { - "id": 5748, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5749, - "name": "Resident Evil 4", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5750, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5751, - "name": "Reign Of Kings", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5752, - "name": "Loki: Heroes of Mythology", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5753, - "name": "Metro 2033 Redux", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5754, - "name": "Quantum Break", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 5755, - "name": "Stupidella", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5756, - "name": "Prince of Persia (2008)", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5757, - "name": "Samsara Room", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5758, - "name": "Resident Evil 5", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5759, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5760, - "name": "Resident Evil: Operation Raccoon City", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Кооператив", - "Зомби", - "Шутер", - "Экшен" - ] - }, - { - "id": 5761, - "name": "Little Nightmares", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5762, - "name": "Reigns", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5763, - "name": "Sudeki", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5764, - "name": "Lost Planet: Colonies", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5765, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5766, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5767, - "name": "Raft", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 5768, - "name": "Saw: The Video Game", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5769, - "name": "Prince of Persia: The Two Thrones", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5770, - "name": "Metro Exodus", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 5771, - "name": "Resident Evil: Revelations", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 5772, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5773, - "name": "Reigns: Her Majesty", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5774, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5775, - "name": "Superhot", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 5776, - "name": "Little Nightmares - The Depths (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 5777, - "name": "Lost: Via Domus", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5778, - "name": "Scratches", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5779, - "name": "Rage", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Гонки", - "Экшены" - ] - }, - { - "id": 5780, - "name": "Prospekt", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5781, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5782, - "name": "Resident Evil 6", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5783, - "name": "Metro Last Light", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 5784, - "name": "Resident Evil: Revelations 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Ужасы" - ] - }, - { - "id": 5785, - "name": "Surgeon Simulator", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5786, - "name": "Resident Evil", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5787, - "name": "Love Is Strange (DEMO)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5788, - "name": "Seasons after Fall", - "site": "gamer_info_com", - "genres": [ - "инди", - "приключения", - "головоломки", - "логические", - "платформеры" - ] - }, - { - "id": 5789, - "name": "Rampage Knights", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 5790, - "name": "Resident Evil 7: Biohazard", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5791, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5792, - "name": "Prototype", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5793, - "name": "Metro Last Light Redux", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5794, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 5795, - "name": "Tales from the Borderlands", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 5796, - "name": "Reus", - "site": "playground_ru", - "genres": [ - "Другой симулятор", - "Песочница", - "Стратегия", - "Симулятор" - ] - }, - { - "id": 5797, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 5798, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5799, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5800, - "name": "Serena", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы", - "приключения" - ] - }, - { - "id": 5801, - "name": "Rayman Legends", - "site": "ag_ru", - "genres": [ - "Платформеры" - ] - }, - { - "id": 5802, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5803, - "name": "Tales of Berseria", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 5804, - "name": "Rezrog", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5805, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 5806, - "name": "METAL SLUG", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5807, - "name": "Prototype 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5808, - "name": "Sacred", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 5809, - "name": "Serious Sam 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5810, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5811, - "name": "Rayman Origins", - "site": "ag_ru", - "genres": [ - "Экшены", - "Семейные", - "Аркадные", - "Платформеры" - ] - }, - { - "id": 5812, - "name": "Loki: Heroes of Mythology", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 5813, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5814, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 5815, - "name": "Tales of Zestiria", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 5816, - "name": "Rise of the Tomb Raider", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица" - ] - }, - { - "id": 5817, - "name": "Psychonauts", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5818, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5819, - "name": "Serious Sam 3: BFE", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5820, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 5821, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5822, - "name": "Sacred 3", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 5823, - "name": "Rayman Raving Rabbids", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5824, - "name": "Resident Evil 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5825, - "name": "Lost Planet: Colonies", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5826, - "name": "METAL SLUG 3", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5827, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5828, - "name": "Resident Evil: Operation Raccoon City", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5829, - "name": "PuniTy", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5830, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5831, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5832, - "name": "Tasty Planet: Back for Seconds", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 5833, - "name": "Rayman Raving Rabbids 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5834, - "name": "Sacred Citadel", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Пристрели их всех" - ] - }, - { - "id": 5835, - "name": "Lost: Via Domus", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5836, - "name": "Resident Evil 5", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5837, - "name": "MINERVA: Metastasis", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5838, - "name": "Resident Evil: Revelations", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5839, - "name": "Tekken 7", - "site": "igromania_ru", - "genres": [ - "Файтинг" - ] - }, - { - "id": 5840, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5841, - "name": "P·O·L·L·E·N", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 5842, - "name": "Serious Sam Classic: The First Encounter", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5843, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5844, - "name": "Resident Evil: Revelations 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5845, - "name": "Middle-earth: Shadow of Mordor", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5846, - "name": "Real Horror Stories", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 5847, - "name": "Love Is Strange (DEMO)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5848, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5849, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5850, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 5851, - "name": "Reus", - "site": "gameguru_ru", - "genres": [ - "В реальном времени", - "Стратегия" - ] - }, - { - "id": 5852, - "name": "Serious Sam Classic: The Second Encounter", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5853, - "name": "Quake 4", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5854, - "name": "MOTHERGUNSHIP", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 5855, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5856, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5857, - "name": "Redeemer", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди" - ] - }, - { - "id": 5858, - "name": "Rezrog", - "site": "gameguru_ru", - "genres": [ - "В пошаговом режиме", - "RPG", - "Стратегия" - ] - }, - { - "id": 5859, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5860, - "name": "METAL SLUG", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5861, - "name": "Might & Magic Heroes VI", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 5862, - "name": "Tesla vs Lovecraft", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5863, - "name": "Shadow Warrior", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 5864, - "name": "Quantum Break", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5865, - "name": "Rise of the Tomb Raider", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир" - ] - }, - { - "id": 5866, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5867, - "name": "Reign Of Kings", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Приключения", - "Экшены", - "Симуляторы" - ] - }, - { - "id": 5868, - "name": "Minecraft Story Mode", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5869, - "name": "Teslagrad", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Платформер", - "Логика" - ] - }, - { - "id": 5870, - "name": "Resident Evil 6", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5871, - "name": "Machinarium", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 5872, - "name": "Shadow of the Tomb Raider", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 5873, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5874, - "name": "METAL SLUG 3", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5875, - "name": "Reigns", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Приключения", - "Симуляторы", - "Карточные" - ] - }, - { - "id": 5876, - "name": "The Banner Saga", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Пошаговая стратегия", - "Ролевая игра" - ] - }, - { - "id": 5877, - "name": "Raft", - "site": "stopgame_ru", - "genres": [ - "adventure", - "action" - ] - }, - { - "id": 5878, - "name": "Risen", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ролевая", - "Открытый мир" - ] - }, - { - "id": 5879, - "name": "Mini Ninjas", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5880, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5881, - "name": "Resident Evil 7: Biohazard", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Экшен" - ] - }, - { - "id": 5882, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5883, - "name": "Saints Row IV", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 5884, - "name": "MINERVA: Metastasis", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5885, - "name": "Reigns: Her Majesty", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Приключения", - "Инди", - "Казуальные", - "Симуляторы", - "Карточные" - ] - }, - { - "id": 5886, - "name": "The Banner Saga 2", - "site": "igromania_ru", - "genres": [ - "Стратегия", - "Пошаговая стратегия", - "Ролевая игра" - ] - }, - { - "id": 5887, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5888, - "name": "Rock of Ages", - "site": "playground_ru", - "genres": [ - "Аркада", - "Стратегия" - ] - }, - { - "id": 5889, - "name": "Rage", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5890, - "name": "Magibot", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie", - "Adventure" - ] - }, - { - "id": 5891, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5892, - "name": "Mirror's Edge", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5893, - "name": "Shantae: Half-Genie Hero", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 5894, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5895, - "name": "The Bard’s Tale (2004)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5896, - "name": "Resident Evil", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 5897, - "name": "Saints Row: Gat out of Hell", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 5898, - "name": "MOTHERGUNSHIP", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5899, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "playground_ru", - "genres": [ - "Юмор", - "Инди", - "Аркада", - "Стратегия", - "Tower defence" - ] - }, - { - "id": 5900, - "name": "Rampage Knights", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5901, - "name": "Risen", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 5902, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5903, - "name": "Shoot Your Nightmare Halloween Special", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5904, - "name": "The Beginner's Guide", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 5905, - "name": "Salt and Sanctuary", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 5906, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5907, - "name": "Root Of Evil: The Tailor", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5908, - "name": "Monst", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5909, - "name": "Rock of Ages", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Стратегия" - ] - }, - { - "id": 5910, - "name": "Rayman Legends", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5911, - "name": "Magicka", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 5912, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5913, - "name": "Machinarium", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 5914, - "name": "Shovel Knight: Shovel of Hope", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5915, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5916, - "name": "Samsara Room", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5917, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Гонки/вождение" - ] - }, - { - "id": 5918, - "name": "Runic Rampage", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5919, - "name": "Mortal Kombat IX", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 5920, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 5921, - "name": "The Binding of Isaac: Rebirth", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 5922, - "name": "Rayman Origins", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5923, - "name": "Shrek 2: The Game", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5924, - "name": "Saw: The Video Game", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5925, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5926, - "name": "Root Of Evil: The Tailor", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5927, - "name": "Magibot", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5928, - "name": "Rust", - "site": "playground_ru", - "genres": [ - "Инди", - "Мультиплеер", - "Адвенчура", - "Выживание", - "Экшен" - ] - }, - { - "id": 5929, - "name": "Magicka 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5930, - "name": "Resident Evil 4", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 5931, - "name": "The Bunker", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 5932, - "name": "Mortal Kombat X", - "site": "gamespot_com", - "genres": [ - "Action", - "2D", - "Fighting" - ] - }, - { - "id": 5933, - "name": "Runic Rampage", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5934, - "name": "Scratches", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 5935, - "name": "Silence on The Line", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5936, - "name": "Rayman Raving Rabbids", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5937, - "name": "Resident Evil: Operation Raccoon City", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5938, - "name": "Rust", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Инди" - ] - }, - { - "id": 5939, - "name": "Ryse: Son of Rome", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Слэшер" - ] - }, - { - "id": 5940, - "name": "Resident Evil 5", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 5941, - "name": "Seasons after Fall", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 5942, - "name": "The Cat Lady", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Приключение" - ] - }, - { - "id": 5943, - "name": "Silent Hill: Alchemilla", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5944, - "name": "Mother Russia Bleeds", - "site": "gamespot_com", - "genres": [ - "Beat-'Em-Up", - "Action", - "2D" - ] - }, - { - "id": 5945, - "name": "Resident Evil: Revelations", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5946, - "name": "Ryse: Son of Rome", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Слэшер" - ] - }, - { - "id": 5947, - "name": "Manual Samuel", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 5948, - "name": "Magicka", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5949, - "name": "Rayman Raving Rabbids 2", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 5950, - "name": "The Council", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 5951, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 5952, - "name": "Serena", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 5953, - "name": "Silverfall", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 5954, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Постапокалипсис", - "Ролевая", - "Выживание", - "Экшен", - "Открытый мир", - "От первого лица" - ] - }, - { - "id": 5955, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5956, - "name": "Mario.EXE", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 5957, - "name": "Resident Evil: Revelations 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5958, - "name": "Mount Your Friends", - "site": "gamespot_com", - "genres": [ - "Sports" - ] - }, - { - "id": 5959, - "name": "Magicka 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5960, - "name": "Real Horror Stories", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 5961, - "name": "Serious Sam 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5962, - "name": "The Curse of Blackwater", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5963, - "name": "SCP-087", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5964, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 5965, - "name": "SCP-087", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5966, - "name": "Silverfall: Earth Awakening", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5967, - "name": "SCP-087-B", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5968, - "name": "Serious Sam 3: BFE", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5969, - "name": "Reus", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5970, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5971, - "name": "Mr. Robot", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 5972, - "name": "Redeemer", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5973, - "name": "SCP-087-B", - "site": "playground_ru", - "genres": [] - }, - { - "id": 5974, - "name": "Manual Samuel", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 5975, - "name": "Singularity", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 5976, - "name": "Resident Evil 6", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 5977, - "name": "Marvel vs. Capcom: Infinite", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 5978, - "name": "SINNER: Sacrifice for Redemption", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 5979, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5980, - "name": "The Darkness II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5981, - "name": "Reign Of Kings", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 5982, - "name": "Rezrog", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 5983, - "name": "Mario.EXE", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 5984, - "name": "Slap-Man", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 5985, - "name": "SINNER: Sacrifice for Redemption", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 5986, - "name": "Serious Sam Classic: The First Encounter", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5987, - "name": "SWAT 4", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 5988, - "name": "The Elder Scrolls IV: Oblivion", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5989, - "name": "Resident Evil 7: Biohazard", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 5990, - "name": "Mr.President!", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 5991, - "name": "Serious Sam Classic: The Second Encounter", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 5992, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 5993, - "name": "Slay the Spire", - "site": "gamer_info_com", - "genres": [ - "инди", - "карточные", - "TBS" - ] - }, - { - "id": 5994, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 5995, - "name": "Rise of the Tomb Raider", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Приключения" - ] - }, - { - "id": 5996, - "name": "SWAT 4", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 5997, - "name": "Reigns", - "site": "stopgame_ru", - "genres": [ - "logic" - ] - }, - { - "id": 5998, - "name": "Masochisia", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 5999, - "name": "Marvel vs. Capcom: Infinite", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6000, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6001, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6002, - "name": "Shadow Warrior", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 6003, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6004, - "name": "Sleeping Dogs", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 6005, - "name": "Murdered: Soul Suspect", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 6006, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6007, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6008, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6009, - "name": "Reigns: Her Majesty", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 6010, - "name": "Shadow of the Tomb Raider", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 6011, - "name": "The Elder Scrolls V: Skyrim", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6012, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6013, - "name": "Masters of Anima", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Strategy", - "Adventure" - ] - }, - { - "id": 6014, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6015, - "name": "Slender: The Arrival", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 6016, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6017, - "name": "Masochisia", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6018, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6019, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Fighting" - ] - }, - { - "id": 6020, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6021, - "name": "Resident Evil", - "site": "stopgame_ru", - "genres": [ - "adventure", - "action" - ] - }, - { - "id": 6022, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6023, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6024, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6025, - "name": "Slendytubbies", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6026, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6027, - "name": "Shantae: Half-Genie Hero", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6028, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6029, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6030, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6031, - "name": "Max Payne", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6032, - "name": "Masters of Anima", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)", - "Adventure" - ] - }, - { - "id": 6033, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Fighting" - ] - }, - { - "id": 6034, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6035, - "name": "Shoot Your Nightmare Halloween Special", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6036, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6037, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6038, - "name": "Slime Rancher", - "site": "gamer_info_com", - "genres": [ - "симуляторы", - "инди" - ] - }, - { - "id": 6039, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6040, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6041, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6042, - "name": "Shovel Knight: Shovel of Hope", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6043, - "name": "Sacred", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6044, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6045, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6046, - "name": "Max Payne", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6047, - "name": "Sniper Elite", - "site": "gamer_info_com", - "genres": [ - "FPS", - "тактика", - "шутеры от 3-го лица" - ] - }, - { - "id": 6048, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6049, - "name": "Resident Evil: Operation Raccoon City", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 6050, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Fighting" - ] - }, - { - "id": 6051, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6052, - "name": "Shrek 2: The Game", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6053, - "name": "Risen", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6054, - "name": "Sacred 3", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 6055, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6056, - "name": "The Evil Within", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик", - "Приключение", - "Боевик от третьего лица" - ] - }, - { - "id": 6057, - "name": "Sniper Elite 3", - "site": "gamer_info_com", - "genres": [ - "FPS", - "тактика", - "шутеры от 3-го лица" - ] - }, - { - "id": 6058, - "name": "Silence on The Line", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6059, - "name": "Resident Evil 4", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6060, - "name": "Sacred Citadel", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6061, - "name": "Resident Evil: Revelations", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 6062, - "name": "Need for Speed: Underground", - "site": "gamespot_com", - "genres": [ - "Simulation", - "Driving/Racing" - ] - }, - { - "id": 6063, - "name": "Rock of Ages", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6064, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6065, - "name": "The Evil Within 2", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 6066, - "name": "Max Payne 3", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6067, - "name": "Sniper Elite V2", - "site": "gamer_info_com", - "genres": [ - "FPS", - "тактика", - "шутеры от 3-го лица" - ] - }, - { - "id": 6068, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6069, - "name": "Silent Hill: Alchemilla", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6070, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6071, - "name": "Resident Evil 5", - "site": "stopgame_ru", - "genres": [ - "adventure", - "action" - ] - }, - { - "id": 6072, - "name": "Resident Evil: Revelations 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6073, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6074, - "name": "The Evil Within: The Assignment (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6075, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6076, - "name": "Never Alone", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 6077, - "name": "Sacred", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Ролевая", - "Фэнтези" - ] - }, - { - "id": 6078, - "name": "Sophie's Curse", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 6079, - "name": "Silverfall", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6080, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6081, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6082, - "name": "The Evil Within: The Consequence (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6083, - "name": "Reus", - "site": "ag_ru", - "genres": [ - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 6084, - "name": "Max: The Curse Of Brotherhood", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6085, - "name": "Root Of Evil: The Tailor", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6086, - "name": "Max Payne 3", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6087, - "name": "South Park: The Fractured But Whole", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 6088, - "name": "Sacred 3", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 6089, - "name": "Neverending Nightmares", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 6090, - "name": "Silverfall: Earth Awakening", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6091, - "name": "Saints Row IV", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6092, - "name": "The Evil Within: The Executioner (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6093, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6094, - "name": "Runic Rampage", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6095, - "name": "Saints Row: Gat out of Hell", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Шутер", - "Аддон" - ] - }, - { - "id": 6096, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6097, - "name": "Sacred Citadel", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 6098, - "name": "McPixel", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 6099, - "name": "Singularity", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 6100, - "name": "The Expendabros", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 6101, - "name": "Max: The Curse Of Brotherhood", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6102, - "name": "Salt and Sanctuary", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "RPG", - "Платформер" - ] - }, - { - "id": 6103, - "name": "Rezrog", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 6104, - "name": "Resident Evil 6", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6105, - "name": "Rust", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6106, - "name": "Slap-Man", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6107, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6108, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6109, - "name": "The Final Station", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Боевик" - ] - }, - { - "id": 6110, - "name": "Samsara Room", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6111, - "name": "McPixel", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6112, - "name": "Resident Evil 7: Biohazard", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6113, - "name": "Nevermind", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 6114, - "name": "Ryse: Son of Rome", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6115, - "name": "South Park: The Stick of Truth", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6116, - "name": "Slay the Spire", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Карточные игры" - ] - }, - { - "id": 6117, - "name": "The First Tree", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6118, - "name": "Metal Gear Rising: Revengeance", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6119, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6120, - "name": "Saw: The Video Game", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6121, - "name": "Rise of the Tomb Raider", - "site": "ag_ru", - "genres": [ - "Экшены", - "Платформеры" - ] - }, - { - "id": 6122, - "name": "Scratches", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 6123, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6124, - "name": "Metal Gear Rising: Revengeance", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6125, - "name": "The Forest", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Приключение", - "Боевик" - ] - }, - { - "id": 6126, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6127, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6128, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6129, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6130, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 6131, - "name": "Seasons after Fall", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 6132, - "name": "Metal Wolf Chaos XD", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6133, - "name": "The Hat Man Shadow Ward", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6134, - "name": "Saints Row IV", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6135, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6136, - "name": "Sleeping Dogs", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 6137, - "name": "Neverwinter Nights", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 6138, - "name": "Metal Wolf Chaos XD", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6139, - "name": "Serena", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 6140, - "name": "SCP-087", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6141, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6142, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 6143, - "name": "Metro 2033", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6144, - "name": "The Incredible Adventures of Van Helsing", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 6145, - "name": "Serious Sam 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6146, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6147, - "name": "Saints Row: Gat out of Hell", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "От третьего лица" - ] - }, - { - "id": 6148, - "name": "Slender: The Arrival", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 6149, - "name": "SCP-087-B", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6150, - "name": "Metro 2033", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6151, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6152, - "name": "Neverwinter Nights 2", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 6153, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 6154, - "name": "Serious Sam 3: BFE", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 6155, - "name": "The Incredible Adventures of Van Helsing II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6156, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6157, - "name": "Slendytubbies", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6158, - "name": "Salt and Sanctuary", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая", - "Пошаговая" - ] - }, - { - "id": 6159, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 6160, - "name": "SINNER: Sacrifice for Redemption", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6161, - "name": "Metro 2033 Redux", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6162, - "name": "The Incredible Adventures of Van Helsing III", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 6163, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6164, - "name": "NieR: Automata", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 6165, - "name": "Slime Rancher", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6166, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6167, - "name": "Samsara Room", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6168, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6169, - "name": "Serious Sam Classic: The First Encounter", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6170, - "name": "Metro 2033 Redux", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6171, - "name": "The Last Remnant", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 6172, - "name": "SWAT 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6173, - "name": "Nightmare House 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6174, - "name": "Serious Sam Classic: The Second Encounter", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6175, - "name": "Risen", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 6176, - "name": "Sniper Elite", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 6177, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6178, - "name": "Resident Evil: Operation Raccoon City", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6179, - "name": "Metro Exodus", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6180, - "name": "Saw: The Video Game", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 6181, - "name": "The Mask Man", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6182, - "name": "Metro Exodus", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6183, - "name": "Shadow Warrior", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 6184, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6185, - "name": "Sniper Elite 3", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6186, - "name": "Nosferatu: The Wrath of Malachi", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6187, - "name": "Rock of Ages", - "site": "ag_ru", - "genres": [ - "Экшены", - "Гонки", - "Платформеры", - "Стратегии" - ] - }, - { - "id": 6188, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6189, - "name": "The Punisher (2005)", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 6190, - "name": "Metro Last Light", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6191, - "name": "Resident Evil: Revelations", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6192, - "name": "Scratches", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 6193, - "name": "Shadow of the Tomb Raider", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир" - ] - }, - { - "id": 6194, - "name": "Metro Last Light", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6195, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6196, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6197, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "ag_ru", - "genres": [ - "Экшены", - "Гонки", - "Аркадные", - "Стратегии" - ] - }, - { - "id": 6198, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Аддон" - ] - }, - { - "id": 6199, - "name": "The Secret of Monkey Island: Special Edition", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6200, - "name": "Seasons after Fall", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6201, - "name": "Sniper Elite V2", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 6202, - "name": "Resident Evil: Revelations 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6203, - "name": "Nox Timore", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6204, - "name": "Shantae: Half-Genie Hero", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 6205, - "name": "The Slippery Slope", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6206, - "name": "Root Of Evil: The Tailor", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Экшены", - "Казуальные", - "Симуляторы" - ] - }, - { - "id": 6207, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6208, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6209, - "name": "Metro Last Light Redux", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6210, - "name": "Metro Last Light Redux", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Action" - ] - }, - { - "id": 6211, - "name": "Serena", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6212, - "name": "Sophie's Curse", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6213, - "name": "Shoot Your Nightmare Halloween Special", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6214, - "name": "The Stanley Parable", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6215, - "name": "Reus", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 6216, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6217, - "name": "Runic Rampage", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 6218, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6219, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6220, - "name": "Shovel Knight: Shovel of Hope", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6221, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6222, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6223, - "name": "Serious Sam 2", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6224, - "name": "South Park: The Fractured But Whole", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 6225, - "name": "The Walking Dead: A New Frontier", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6226, - "name": "Shrek 2: The Game", - "site": "gameguru_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 6227, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6228, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6229, - "name": "Rezrog", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 6230, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6231, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6232, - "name": "Rust", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Шутеры", - "Экшены", - "ММО" - ] - }, - { - "id": 6233, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6234, - "name": "Silence on The Line", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6235, - "name": "The Walking Dead: Michonne", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6236, - "name": "Serious Sam 3: BFE", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6237, - "name": "Omensight", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 6238, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6239, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6240, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6241, - "name": "Rise of the Tomb Raider", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6242, - "name": "Silent Hill: Alchemilla", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6243, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6244, - "name": "The Walking Dead: Season One", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6245, - "name": "Ryse: Son of Rome", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 6246, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6247, - "name": "One Hand Clapping (DEMO)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6248, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6249, - "name": "Spooky's House of Jump Scares", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 6250, - "name": "Silverfall", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6251, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6252, - "name": "The Walking Dead: Season Two", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6253, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6254, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6255, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6256, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6257, - "name": "One Night at Flumpty's", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6258, - "name": "Silverfall: Earth Awakening", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6259, - "name": "South Park: The Stick of Truth", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 6260, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 6261, - "name": "Serious Sam Classic: The First Encounter", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6262, - "name": "The Witcher", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Приключение", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 6263, - "name": "Sacred", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6264, - "name": "SCP-087", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 6265, - "name": "One Night at Flumpty's 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6266, - "name": "Singularity", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 6267, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6268, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6269, - "name": "Middle-earth: Shadow of Mordor", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 6270, - "name": "Star Wars: The Force Unleashed", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 6271, - "name": "Serious Sam Classic: The Second Encounter", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6272, - "name": "The Witcher 2: Assassins of Kings", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6273, - "name": "Slap-Man", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6274, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6275, - "name": "Middle-earth: Shadow of Mordor", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6276, - "name": "Sacred 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6277, - "name": "SCP-087-B", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6278, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6279, - "name": "Slay the Spire", - "site": "gameguru_ru", - "genres": [ - "Инди", - "На логику", - "CCG" - ] - }, - { - "id": 6280, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6281, - "name": "The Witcher 3: Wild Hunt", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6282, - "name": "Star Wars: The Force Unleashed II", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 6283, - "name": "Shadow Warrior", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 6284, - "name": "One Piece: Burning Blood", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Fighting" - ] - }, - { - "id": 6285, - "name": "Might & Magic Heroes VI", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 6286, - "name": "Sacred Citadel", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6287, - "name": "Sleeping Dogs", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6288, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6289, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6290, - "name": "Might & Magic Heroes VI", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy" - ] - }, - { - "id": 6291, - "name": "SINNER: Sacrifice for Redemption", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 6292, - "name": "SteamWorld Dig", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 6293, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6294, - "name": "Shadow of the Tomb Raider", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "От третьего лица" - ] - }, - { - "id": 6295, - "name": "Slender: The Arrival", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 6296, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6297, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6298, - "name": "Orcs Must Die!", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 6299, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6300, - "name": "Minecraft Story Mode", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Adventure" - ] - }, - { - "id": 6301, - "name": "SWAT 4", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 6302, - "name": "SteamWorld Dig 2", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 6303, - "name": "Minecraft Story Mode", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6304, - "name": "Slendytubbies", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6305, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6306, - "name": "Risen", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 6307, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6308, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6309, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6310, - "name": "The Wolf Among Us", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6311, - "name": "Slime Rancher", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 6312, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6313, - "name": "SteamWorld Heist", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 6314, - "name": "Rock of Ages", - "site": "stopgame_ru", - "genres": [ - "strategy", - "arcade" - ] - }, - { - "id": 6315, - "name": "Mini Ninjas", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6316, - "name": "Sniper Elite", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 6317, - "name": "Shantae: Half-Genie Hero", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 6318, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6319, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6320, - "name": "Ori and the Blind Forest", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 6321, - "name": "Mini Ninjas", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6322, - "name": "Thief", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6323, - "name": "Steampunk Tower 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6324, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6325, - "name": "Sniper Elite 3", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Тактический", - "Стелс", - "Шутер" - ] - }, - { - "id": 6326, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 6327, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6328, - "name": "Shoot Your Nightmare Halloween Special", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6329, - "name": "Saints Row IV", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6330, - "name": "This Is The Police", - "site": "igromania_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 6331, - "name": "Steel Rats", - "site": "gamer_info_com", - "genres": [ - "аркадные", - "гонки" - ] - }, - { - "id": 6332, - "name": "Sniper Elite V2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 6333, - "name": "Mirror's Edge", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6334, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6335, - "name": "Root Of Evil: The Tailor", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6336, - "name": "Shovel Knight: Shovel of Hope", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6337, - "name": "Mirror's Edge", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6338, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6339, - "name": "TimeShift", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 6340, - "name": "Sophie's Curse", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6341, - "name": "Saints Row: Gat out of Hell", - "site": "iwantgames_ru", - "genres": [ - "Дополнение" - ] - }, - { - "id": 6342, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6343, - "name": "Outlast", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 6344, - "name": "Stories: The Path of Destinies", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 6345, - "name": "Monst", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6346, - "name": "Runic Rampage", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6347, - "name": "South Park: The Fractured But Whole", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6348, - "name": "Shrek 2: The Game", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Аркада" - ] - }, - { - "id": 6349, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 6350, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6351, - "name": "Titan Quest", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 6352, - "name": "Monst", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6353, - "name": "Salt and Sanctuary", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6354, - "name": "Street Fighter X Tekken", - "site": "gamer_info_com", - "genres": [ - "файтинги" - ] - }, - { - "id": 6355, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "RPG" - ] - }, - { - "id": 6356, - "name": "Mortal Kombat IX", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6357, - "name": "Silence on The Line", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6358, - "name": "Outlast: Whistleblower", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6359, - "name": "Titan Quest Anniversary Edition", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6360, - "name": "Rust", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 6361, - "name": "Spooky's House of Jump Scares", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6362, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6363, - "name": "Mortal Kombat IX", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6364, - "name": "Samsara Room", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6366, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "RPG" - ] - }, - { - "id": 6367, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6368, - "name": "Silent Hill: Alchemilla", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6369, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Приключения", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6370, - "name": "South Park: The Stick of Truth", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6371, - "name": "Ryse: Son of Rome", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6372, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6373, - "name": "Overlord", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6374, - "name": "Stupidella", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6375, - "name": "Saw: The Video Game", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6376, - "name": "Mortal Kombat X", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6377, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6378, - "name": "Silverfall", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 6379, - "name": "Mortal Kombat X", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6380, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6381, - "name": "Star Wars: The Force Unleashed", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 6382, - "name": "Overlord: Raising Hell", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6383, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6384, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 6385, - "name": "Titan Souls", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 6386, - "name": "Sudeki", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 6387, - "name": "Scratches", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6388, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6389, - "name": "Silverfall: Earth Awakening", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6390, - "name": "Mother Russia Bleeds", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6391, - "name": "Star Wars: The Force Unleashed II", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 6392, - "name": "SCP-087", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6393, - "name": "Titanfall 2", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "MMOG", - "Боевик" - ] - }, - { - "id": 6394, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6395, - "name": "Superhot", - "site": "gamer_info_com", - "genres": [ - "FPS", - "инди" - ] - }, - { - "id": 6396, - "name": "Mother Russia Bleeds", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 6397, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 6398, - "name": "Seasons after Fall", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6399, - "name": "Singularity", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6400, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6401, - "name": "Mount Your Friends", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6402, - "name": "To The Moon", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Ролевая игра" - ] - }, - { - "id": 6403, - "name": "Oxenfree", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6404, - "name": "SCP-087-B", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6405, - "name": "Surgeon Simulator", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6406, - "name": "Sacred", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6407, - "name": "SteamWorld Dig", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Приключения", - "Платформер" - ] - }, - { - "id": 6408, - "name": "Serena", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6409, - "name": "Slap-Man", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6410, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6411, - "name": "Mount Your Friends", - "site": "store_steampowered_com", - "genres": [ - "Sports", - "Simulation", - "Indie" - ] - }, - { - "id": 6412, - "name": "Tomb Raider", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6413, - "name": "Tales from the Borderlands", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6414, - "name": "SteamWorld Dig 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Платформер" - ] - }, - { - "id": 6415, - "name": "SINNER: Sacrifice for Redemption", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6416, - "name": "Mr. Robot", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 6417, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6418, - "name": "Sacred 3", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 6419, - "name": "Serious Sam 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6420, - "name": "Slay the Spire", - "site": "playground_ru", - "genres": [ - "Инди", - "Карточная", - "Стратегия", - "Пошаговая" - ] - }, - { - "id": 6421, - "name": "Torchlight", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 6422, - "name": "Tales of Berseria", - "site": "gamer_info_com", - "genres": [ - "jRPG" - ] - }, - { - "id": 6423, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6424, - "name": "Painkiller", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6425, - "name": "SteamWorld Heist", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Боевик/Экшн" - ] - }, - { - "id": 6426, - "name": "SWAT 4", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6427, - "name": "Mr. Robot", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie" - ] - }, - { - "id": 6428, - "name": "Sacred Citadel", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 6429, - "name": "Serious Sam 3: BFE", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6430, - "name": "Mr.President!", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6431, - "name": "Torchlight II", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6432, - "name": "Sleeping Dogs", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "От третьего лица" - ] - }, - { - "id": 6433, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6434, - "name": "Steampunk Tower 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6435, - "name": "Painkiller: Battle Out of Hell", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6436, - "name": "Tales of Zestiria", - "site": "gamer_info_com", - "genres": [ - "jRPG" - ] - }, - { - "id": 6437, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6438, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6439, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6440, - "name": "Toren", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6441, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6442, - "name": "Slender: The Arrival", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Ужасы" - ] - }, - { - "id": 6443, - "name": "Steel Rats", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг" - ] - }, - { - "id": 6444, - "name": "Painkiller: Overdose", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6445, - "name": "Mr.President!", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Simulation", - "Indie", - "Adventure" - ] - }, - { - "id": 6446, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6447, - "name": "Murdered: Soul Suspect", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6448, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6449, - "name": "Tormentum: Dark Sorrow", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6450, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6451, - "name": "Slendytubbies", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6452, - "name": "Serious Sam Classic: The First Encounter", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6453, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6454, - "name": "Stories: The Path of Destinies", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6455, - "name": "Tasty Planet: Back for Seconds", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6456, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6457, - "name": "Paint the Town Red", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 6458, - "name": "Total Overdose", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 6459, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Action" - ] - }, - { - "id": 6460, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6461, - "name": "Slime Rancher", - "site": "playground_ru", - "genres": [ - "Открытый мир", - "От первого лица", - "Инди" - ] - }, - { - "id": 6462, - "name": "Murdered: Soul Suspect", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6463, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6464, - "name": "Serious Sam Classic: The Second Encounter", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6465, - "name": "Street Fighter X Tekken", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 6466, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6467, - "name": "Tekken 7", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6468, - "name": "Transistor", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 6469, - "name": "Spooky's House of Jump Scares", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6470, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6472, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6473, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6474, - "name": "Sniper Elite", - "site": "playground_ru", - "genres": [ - "Экшен", - "Стелс", - "Шутер" - ] - }, - { - "id": 6475, - "name": "Shadow Warrior", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6476, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 6477, - "name": "Saints Row IV", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 6478, - "name": "Trine", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Логика" - ] - }, - { - "id": 6479, - "name": "Panty Party", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 6480, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6481, - "name": "Stupidella", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6482, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6483, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6484, - "name": "Sniper Elite 3", - "site": "playground_ru", - "genres": [ - "Стелс", - "Мультиплеер", - "Кооператив", - "Шутер", - "Экшен", - "От первого лица" - ] - }, - { - "id": 6485, - "name": "Shadow of the Tomb Raider", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Приключения" - ] - }, - { - "id": 6486, - "name": "Tesla vs Lovecraft", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up", - "инди" - ] - }, - { - "id": 6487, - "name": "Star Wars: The Force Unleashed", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 6488, - "name": "Trine 2", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Логика" - ] - }, - { - "id": 6489, - "name": "Saints Row: Gat out of Hell", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 6490, - "name": "Sudeki", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6491, - "name": "Papers, Please", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6492, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6493, - "name": "Star Wars: The Force Unleashed II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6494, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6495, - "name": "Trine 3: The Artifacts of Power", - "site": "igromania_ru", - "genres": [ - "Платформер", - "Логика" - ] - }, - { - "id": 6496, - "name": "Teslagrad", - "site": "gamer_info_com", - "genres": [ - "аркадные", - "инди", - "головоломки", - "логические", - "платформеры" - ] - }, - { - "id": 6497, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6498, - "name": "Salt and Sanctuary", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Платформеры" - ] - }, - { - "id": 6499, - "name": "Sniper Elite V2", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Стелс", - "Шутер" - ] - }, - { - "id": 6500, - "name": "Need for Speed: Underground", - "site": "mobygames_com", - "genres": [ - "Racing / driving" - ] - }, - { - "id": 6501, - "name": "Superhot", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Головоломки/Пазл", - "Боевик/Экшн" - ] - }, - { - "id": 6502, - "name": "SteamWorld Dig", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 6503, - "name": "Troll Tale", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6504, - "name": "The Banner Saga", - "site": "gamer_info_com", - "genres": [ - "RPG", - "TBS" - ] - }, - { - "id": 6505, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6506, - "name": "Sophie's Curse", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6507, - "name": "Surgeon Simulator", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6508, - "name": "Shantae: Half-Genie Hero", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6509, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6510, - "name": "SteamWorld Dig 2", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 6511, - "name": "Samsara Room", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Инди", - "Приключения" - ] - }, - { - "id": 6512, - "name": "Peace, Death!", - "site": "gamespot_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 6513, - "name": "Never Alone", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6514, - "name": "Trollface Quest", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6515, - "name": "The Banner Saga 2", - "site": "gamer_info_com", - "genres": [ - "RPG", - "TBS" - ] - }, - { - "id": 6516, - "name": "SteamWorld Heist", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Стратегия" - ] - }, - { - "id": 6517, - "name": "South Park: The Fractured But Whole", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая", - "Юмор" - ] - }, - { - "id": 6518, - "name": "Sacred", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 6519, - "name": "Tales from the Borderlands", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6520, - "name": "Shoot Your Nightmare Halloween Special", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6521, - "name": "Need for Speed: Underground", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6522, - "name": "Trollface Quest 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6523, - "name": "Saw: The Video Game", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6524, - "name": "Steampunk Tower 2", - "site": "gameguru_ru", - "genres": [ - "В реальном времени", - "Стратегия", - "Инди" - ] - }, - { - "id": 6525, - "name": "Neverending Nightmares", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6526, - "name": "Pillars of Eternity", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 6527, - "name": "The Bard’s Tale (2004)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6528, - "name": "Tales of Berseria", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6529, - "name": "Trollface Quest 3", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6530, - "name": "Sacred 3", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 6531, - "name": "Steel Rats", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада" - ] - }, - { - "id": 6532, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6533, - "name": "Shovel Knight: Shovel of Hope", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6534, - "name": "Scratches", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6535, - "name": "Never Alone", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6536, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6537, - "name": "The Beginner's Guide", - "site": "gamer_info_com", - "genres": [ - "инди" - ] - }, - { - "id": 6538, - "name": "Stories: The Path of Destinies", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 6539, - "name": "Trollface Quest 4", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6540, - "name": "Tales of Zestiria", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6541, - "name": "Sacred Citadel", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 6542, - "name": "Shrek 2: The Game", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6543, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6544, - "name": "Seasons after Fall", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 6545, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6546, - "name": "Street Fighter X Tekken", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 6547, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6548, - "name": "Trollface Quest 5", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6549, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6550, - "name": "Neverending Nightmares", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 6551, - "name": "Nevermind", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6552, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6554, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6555, - "name": "Serena", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 6556, - "name": "Silence on The Line", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6557, - "name": "Tasty Planet: Back for Seconds", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6558, - "name": "Trollface Quest 6", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6559, - "name": "South Park: The Stick of Truth", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая", - "Юмор" - ] - }, - { - "id": 6560, - "name": "Stupidella", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Квест" - ] - }, - { - "id": 6561, - "name": "The Binding of Isaac: Rebirth", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди" - ] - }, - { - "id": 6562, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6563, - "name": "Trollface Quest 7", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6564, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6565, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6566, - "name": "Tekken 7", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 6567, - "name": "Nevermind", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 6568, - "name": "Silent Hill: Alchemilla", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6569, - "name": "Sudeki", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6570, - "name": "The Bunker", - "site": "gamer_info_com", - "genres": [ - "инди", - "квесты" - ] - }, - { - "id": 6571, - "name": "Serious Sam 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6572, - "name": "Pinstripe", - "site": "gamespot_com", - "genres": [ - "Miscellaneous" - ] - }, - { - "id": 6573, - "name": "Trollface Quest: Video Memes", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6574, - "name": "Superhot", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "На логику", - "Шутер" - ] - }, - { - "id": 6575, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6576, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6577, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6578, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6579, - "name": "Silverfall", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6580, - "name": "Twin Sector", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 6581, - "name": "The Cat Lady", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6582, - "name": "Neverwinter Nights", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 6583, - "name": "Surgeon Simulator", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6584, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6585, - "name": "Saints Row IV", - "site": "stopgame_ru", - "genres": [ - "action", - "racing" - ] - }, - { - "id": 6586, - "name": "Serious Sam 3: BFE", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6587, - "name": "Tesla vs Lovecraft", - "site": "gamebomb_ru", - "genres": [ - "Шутер" - ] - }, - { - "id": 6588, - "name": "Neverwinter Nights", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6589, - "name": "Silverfall: Earth Awakening", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6590, - "name": "Tales from the Borderlands", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6591, - "name": "Pirates of the Caribbean: At World’s End", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6592, - "name": "The Council", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 6593, - "name": "Two Worlds", - "site": "igromania_ru", - "genres": [ - "Ролевая игра" - ] - }, - { - "id": 6594, - "name": "Neverwinter Nights 2", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 6595, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6596, - "name": "Tales of Berseria", - "site": "gameguru_ru", - "genres": [ - "RPG", - "jRPG" - ] - }, - { - "id": 6597, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 6598, - "name": "Teslagrad", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл", - "Боевик/Экшн" - ] - }, - { - "id": 6599, - "name": "Pizza Delivery 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6600, - "name": "Singularity", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6601, - "name": "Neverwinter Nights 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6602, - "name": "Saints Row: Gat out of Hell", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6603, - "name": "Typical Nightmare", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6604, - "name": "The Curse of Blackwater", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6605, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6606, - "name": "Tales of Zestiria", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6607, - "name": "NieR: Automata", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 6608, - "name": "The Banner Saga", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 6609, - "name": "Ultimate Epic Battle Simulator", - "site": "igromania_ru", - "genres": [ - "Симулятор" - ] - }, - { - "id": 6610, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6611, - "name": "Slap-Man", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6612, - "name": "Serious Sam Classic: The First Encounter", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6613, - "name": "Salt and Sanctuary", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 6614, - "name": "Plague Inc: Evolved", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 6615, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6616, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6617, - "name": "Nightmare House 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6618, - "name": "NieR: Automata", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6619, - "name": "The Banner Saga 2", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры" - ] - }, - { - "id": 6620, - "name": "Ultra Street Fighter IV", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6621, - "name": "Tasty Planet: Back for Seconds", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6622, - "name": "Slay the Spire", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6623, - "name": "The Darkness II", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 6624, - "name": "Serious Sam Classic: The Second Encounter", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6625, - "name": "Samsara Room", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6626, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6627, - "name": "Plants vs. Zombies", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 6628, - "name": "The Bard’s Tale (2004)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6629, - "name": "Undertale", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 6630, - "name": "Tekken 7", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Файтинг" - ] - }, - { - "id": 6631, - "name": "Nosferatu: The Wrath of Malachi", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6632, - "name": "Nightmare House 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6633, - "name": "The Elder Scrolls IV: Oblivion", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 6634, - "name": "Shadow Warrior", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6635, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6636, - "name": "Sleeping Dogs", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6637, - "name": "Saw: The Video Game", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6638, - "name": "Unfair Mario", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6639, - "name": "The Beginner's Guide", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6640, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 6641, - "name": "Nox Timore", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6642, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "Tactical", - "Shooter" - ] - }, - { - "id": 6643, - "name": "Tesla vs Lovecraft", - "site": "gameguru_ru", - "genres": [ - "Shoot-em-up", - "Аркада", - "Инди" - ] - }, - { - "id": 6644, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6645, - "name": "Shadow of the Tomb Raider", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 6646, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6647, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6648, - "name": "Slender: The Arrival", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6649, - "name": "Nosferatu: The Wrath of Malachi", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6650, - "name": "Scratches", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 6651, - "name": "Unmechanical", - "site": "igromania_ru", - "genres": [ - "Логика" - ] - }, - { - "id": 6652, - "name": "Teslagrad", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 6653, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6654, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6655, - "name": "The Binding of Isaac: Rebirth", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Ролевые игры" - ] - }, - { - "id": 6656, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6657, - "name": "Slendytubbies", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6658, - "name": "Unmechanical: Extended (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6659, - "name": "The Banner Saga", - "site": "gameguru_ru", - "genres": [ - "Инди", - "В пошаговом режиме", - "RPG", - "Стратегия" - ] - }, - { - "id": 6660, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6661, - "name": "Pony Island", - "site": "gamespot_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 6662, - "name": "Nox Timore", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6663, - "name": "Seasons after Fall", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 6664, - "name": "Omensight", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6665, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6666, - "name": "The Bunker", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6667, - "name": "The Elder Scrolls V: Skyrim", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 6668, - "name": "Until the last", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6669, - "name": "The Banner Saga 2", - "site": "gameguru_ru", - "genres": [ - "Инди", - "В пошаговом режиме", - "RPG", - "Стратегия" - ] - }, - { - "id": 6670, - "name": "Slime Rancher", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6671, - "name": "Shantae: Half-Genie Hero", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Платформеры", - "Приключения" - ] - }, - { - "id": 6672, - "name": "The Cat Lady", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6673, - "name": "Serena", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6674, - "name": "The Bard’s Tale (2004)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6675, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6676, - "name": "One Hand Clapping (DEMO)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6677, - "name": "Portal", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6678, - "name": "Valiant Hearts: The Great War", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6679, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6680, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 6681, - "name": "Sniper Elite", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6682, - "name": "Shoot Your Nightmare Halloween Special", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6683, - "name": "The Beginner's Guide", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6684, - "name": "The Council", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6685, - "name": "Spooky's House of Jump Scares", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6686, - "name": "Vanish", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6687, - "name": "Serious Sam 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6688, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6689, - "name": "One Night at Flumpty's", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6690, - "name": "Omensight", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6691, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "Аркада", - "RPG" - ] - }, - { - "id": 6692, - "name": "Portal 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6693, - "name": "The Curse of Blackwater", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6694, - "name": "Shovel Knight: Shovel of Hope", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 6695, - "name": "Sniper Elite 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6696, - "name": "Victor Vran", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 6697, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "playground_ru", - "genres": [ - "Слэшер", - "От третьего лица", - "Научная фантастика", - "Ролевая", - "Шутер", - "Экшен", - "От первого лица" - ] - }, - { - "id": 6698, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6699, - "name": "The Binding of Isaac: Rebirth", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "RPG" - ] - }, - { - "id": 6700, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6701, - "name": "Serious Sam 3: BFE", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6702, - "name": "One Night at Flumpty's 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 6703, - "name": "One Hand Clapping (DEMO)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6704, - "name": "Sniper Elite V2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6705, - "name": "Shrek 2: The Game", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6706, - "name": "Prey (2006)", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6707, - "name": "Vikings: Wolves of Midgard", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6708, - "name": "The Bunker", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6709, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6710, - "name": "Star Wars: The Force Unleashed", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6711, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6712, - "name": "The Cat Lady", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 6713, - "name": "Silence on The Line", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6714, - "name": "One Piece: Burning Blood", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6715, - "name": "Prince of Persia (2008)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6716, - "name": "Visage", - "site": "igromania_ru", - "genres": [ - "Ужасы", - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 6717, - "name": "Sophie's Curse", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6718, - "name": "One Night at Flumpty's", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6719, - "name": "The Darkness II", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 6720, - "name": "Star Wars: The Force Unleashed II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6721, - "name": "The Evil Within", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6722, - "name": "The Council", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6723, - "name": "Wanted: Weapons of Fate", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 6724, - "name": "Serious Sam Classic: The First Encounter", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6725, - "name": "Silent Hill: Alchemilla", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6726, - "name": "South Park: The Fractured But Whole", - "site": "iwantgames_ru", - "genres": [ - "РПГ", - "Приключения" - ] - }, - { - "id": 6727, - "name": "One Night at Flumpty's 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6728, - "name": "Orcs Must Die!", - "site": "mobygames_com", - "genres": [ - "Action", - "Strategy / tactics" - ] - }, - { - "id": 6729, - "name": "Prince of Persia: The Two Thrones", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6730, - "name": "The Curse of Blackwater", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6731, - "name": "The Elder Scrolls IV: Oblivion", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 6732, - "name": "The Evil Within 2", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "приключения" - ] - }, - { - "id": 6733, - "name": "SteamWorld Dig", - "site": "playground_ru", - "genres": [ - "Стимпанк", - "Аркада", - "Постапокалипсис", - "Вестерн" - ] - }, - { - "id": 6734, - "name": "Warcraft III: Reign of Chaos", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6735, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6736, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6737, - "name": "Silverfall", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 6738, - "name": "Serious Sam Classic: The Second Encounter", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6739, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6740, - "name": "The Evil Within: The Assignment (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6741, - "name": "Warcraft III: The Frozen Throne", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6742, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 6743, - "name": "The Darkness II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6744, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6745, - "name": "SteamWorld Dig 2", - "site": "playground_ru", - "genres": [ - "Стимпанк", - "Вестерн", - "Инди", - "Аркада", - "Платформер" - ] - }, - { - "id": 6746, - "name": "One Piece: Burning Blood", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6747, - "name": "Silverfall: Earth Awakening", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6748, - "name": "Prospekt", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6749, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6750, - "name": "Warhammer 40.000: Space Marine", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6751, - "name": "The Elder Scrolls IV: Oblivion", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6752, - "name": "Shadow Warrior", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6753, - "name": "The Evil Within: The Consequence (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6754, - "name": "SteamWorld Heist", - "site": "playground_ru", - "genres": [ - "Стратегия", - "Пошаговая" - ] - }, - { - "id": 6755, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6756, - "name": "Ori and the Blind Forest", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6757, - "name": "Warhammer: Chaosbane", - "site": "igromania_ru", - "genres": [ - "Боевик" - ] - }, - { - "id": 6758, - "name": "The Elder Scrolls V: Skyrim", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 6759, - "name": "South Park: The Stick of Truth", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6760, - "name": "The Evil Within: The Executioner (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6761, - "name": "Shadow of the Tomb Raider", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6762, - "name": "Singularity", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 6763, - "name": "Orcs Must Die!", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie" - ] - }, - { - "id": 6764, - "name": "Prototype", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6765, - "name": "Steampunk Tower 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6766, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6767, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6768, - "name": "We Happy Few", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6769, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6770, - "name": "The Expendabros", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6771, - "name": "Slap-Man", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6772, - "name": "The Elder Scrolls V: Skyrim", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6773, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6774, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6775, - "name": "Outlast", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6776, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6777, - "name": "Steel Rats", - "site": "playground_ru", - "genres": [ - "Стимпанк", - "Мотоциклы", - "Аркада", - "Экшен", - "Платформер" - ] - }, - { - "id": 6778, - "name": "Whack The Thief", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6779, - "name": "Prototype 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 6780, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6781, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6782, - "name": "The Final Station", - "site": "gamer_info_com", - "genres": [ - "survival" - ] - }, - { - "id": 6783, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6784, - "name": "Slay the Spire", - "site": "ag_ru", - "genres": [ - "Стратегии", - "Инди", - "Карточные" - ] - }, - { - "id": 6785, - "name": "Shantae: Half-Genie Hero", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 6786, - "name": "Whack Your Boss", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6787, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6788, - "name": "Stories: The Path of Destinies", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Ролевая", - "Вид сверху" - ] - }, - { - "id": 6789, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6790, - "name": "Ori and the Blind Forest", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6791, - "name": "The First Tree", - "site": "gamer_info_com", - "genres": [ - "инди" - ] - }, - { - "id": 6792, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6793, - "name": "Outlast: Whistleblower", - "site": "mobygames_com", - "genres": [ - "Action", - "DLC / add-on" - ] - }, - { - "id": 6794, - "name": "Whack Your Ex", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6795, - "name": "Sleeping Dogs", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 6796, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6797, - "name": "Shoot Your Nightmare Halloween Special", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6798, - "name": "Psychonauts", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "3D" - ] - }, - { - "id": 6799, - "name": "Street Fighter X Tekken", - "site": "playground_ru", - "genres": [ - "Аркада", - "Файтинг" - ] - }, - { - "id": 6800, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6801, - "name": "The Forest", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "survival" - ] - }, - { - "id": 6802, - "name": "Whack Your Neighbour", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6803, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6804, - "name": "The Evil Within", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 6805, - "name": "Slender: The Arrival", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 6806, - "name": "Overlord", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6808, - "name": "Outlast", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 6809, - "name": "The Evil Within", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 6810, - "name": "Shovel Knight: Shovel of Hope", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6811, - "name": "Whack Your Teacher", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6812, - "name": "The Hat Man Shadow Ward", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы", - "приключения", - "action" - ] - }, - { - "id": 6813, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6814, - "name": "PuniTy", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 6815, - "name": "The Evil Within 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 6816, - "name": "The Evil Within 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир" - ] - }, - { - "id": 6817, - "name": "Stupidella", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6818, - "name": "Slendytubbies", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6819, - "name": "Outlast: Whistleblower", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6820, - "name": "What Remains of Edith Finch", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6821, - "name": "Shrek 2: The Game", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 6822, - "name": "Overlord: Raising Hell", - "site": "mobygames_com", - "genres": [ - "Compilation", - "DLC / add-on" - ] - }, - { - "id": 6823, - "name": "The Evil Within: The Assignment (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6824, - "name": "The Evil Within: The Assignment (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 6825, - "name": "The Incredible Adventures of Van Helsing", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6826, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6827, - "name": "P·O·L·L·E·N", - "site": "gamespot_com", - "genres": [ - "VR", - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 6828, - "name": "Sudeki", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Адвенчура", - "Ролевая" - ] - }, - { - "id": 6829, - "name": "Will Rock", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 6830, - "name": "The Evil Within: The Consequence (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6831, - "name": "Slime Rancher", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 6832, - "name": "The Evil Within: The Consequence (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 6833, - "name": "Oxenfree", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6834, - "name": "Silence on The Line", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6835, - "name": "The Incredible Adventures of Van Helsing II", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6836, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6837, - "name": "Overlord", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 6838, - "name": "The Evil Within: The Executioner (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6839, - "name": "Willy-Nilly Knight", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Ролевая игра" - ] - }, - { - "id": 6840, - "name": "The Evil Within: The Executioner (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аддон" - ] - }, - { - "id": 6841, - "name": "Superhot", - "site": "playground_ru", - "genres": [ - "Виртуальная реальность", - "От первого лица", - "Инди", - "Шутер" - ] - }, - { - "id": 6842, - "name": "Sniper Elite", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 6843, - "name": "Quake 4", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6844, - "name": "The Incredible Adventures of Van Helsing III", - "site": "gamer_info_com", - "genres": [ - "hack & slash" - ] - }, - { - "id": 6845, - "name": "Silent Hill: Alchemilla", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6846, - "name": "The Expendabros", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6847, - "name": "Wizard of Legend", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6848, - "name": "Surgeon Simulator", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6849, - "name": "Painkiller", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6850, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6851, - "name": "The Expendabros", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 6852, - "name": "Sniper Elite 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 6853, - "name": "The Final Station", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 6854, - "name": "The Last Remnant", - "site": "gamer_info_com", - "genres": [ - "jRPG" - ] - }, - { - "id": 6855, - "name": "Quantum Break", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 6856, - "name": "Overlord: Raising Hell", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 6857, - "name": "Silverfall", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 6858, - "name": "Wolfenstein", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6859, - "name": "Tales from the Borderlands", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 6860, - "name": "The Final Station", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 6861, - "name": "The First Tree", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 6862, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6863, - "name": "Sniper Elite V2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 6864, - "name": "Painkiller: Battle Out of Hell", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 6865, - "name": "The Mask Man", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6866, - "name": "Wolfenstein: The New Order", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 6867, - "name": "Silverfall: Earth Awakening", - "site": "stopgame_ru", - "genres": [ - "add-on", - "rpg" - ] - }, - { - "id": 6868, - "name": "The Forest", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Открытый мир", - "Инди" - ] - }, - { - "id": 6869, - "name": "The First Tree", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6870, - "name": "Tales of Berseria", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Ролевая", - "Фэнтези" - ] - }, - { - "id": 6871, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6872, - "name": "The Punisher (2005)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6873, - "name": "Oxenfree", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 6874, - "name": "Wolfenstein: The Old Blood", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 6875, - "name": "Sophie's Curse", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Симуляторы" - ] - }, - { - "id": 6876, - "name": "The Hat Man Shadow Ward", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6877, - "name": "Painkiller: Overdose", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6878, - "name": "Raft", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 6879, - "name": "Singularity", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6880, - "name": "The Incredible Adventures of Van Helsing", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 6881, - "name": "Tales of Zestiria", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая" - ] - }, - { - "id": 6882, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6883, - "name": "Woolfe - The Red Hood Diaries", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6884, - "name": "The Secret of Monkey Island: Special Edition", - "site": "gamer_info_com", - "genres": [ - "квесты" - ] - }, - { - "id": 6885, - "name": "Painkiller", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6886, - "name": "The Incredible Adventures of Van Helsing II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6887, - "name": "Rage", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 6888, - "name": "Slap-Man", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6889, - "name": "Worms 2", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Стратегия" - ] - }, - { - "id": 6890, - "name": "The Slippery Slope", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6891, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6892, - "name": "The Forest", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Стратегия" - ] - }, - { - "id": 6893, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6894, - "name": "The Incredible Adventures of Van Helsing III", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6895, - "name": "Paint the Town Red", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6896, - "name": "Painkiller: Battle Out of Hell", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 6897, - "name": "South Park: The Fractured But Whole", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 6898, - "name": "The Hat Man Shadow Ward", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6899, - "name": "X-Blades / Ониблэйд", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6900, - "name": "Tasty Planet: Back for Seconds", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6901, - "name": "The Last Remnant", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 6902, - "name": "The Stanley Parable", - "site": "gamer_info_com", - "genres": [ - "визуальные новеллы", - "инди", - "приключения" - ] - }, - { - "id": 6903, - "name": "Slay the Spire", - "site": "stopgame_ru", - "genres": [ - "cards" - ] - }, - { - "id": 6904, - "name": "Spooky's House of Jump Scares", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6905, - "name": "X-Morph: Defense", - "site": "igromania_ru", - "genres": [ - "Стратегия в реальном времени", - "Стратегия" - ] - }, - { - "id": 6906, - "name": "The Incredible Adventures of Van Helsing", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 6907, - "name": "Panty Party", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 6908, - "name": "The Mask Man", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6909, - "name": "Rampage Knights", - "site": "gamespot_com", - "genres": [ - "Beat-'Em-Up", - "Action", - "2D" - ] - }, - { - "id": 6910, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6911, - "name": "The Walking Dead: A New Frontier", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 6912, - "name": "Tekken 7", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Файтинг", - "Аркада", - "Экшен", - "Виртуальная реальность" - ] - }, - { - "id": 6913, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6914, - "name": "Sleeping Dogs", - "site": "stopgame_ru", - "genres": [ - "action", - "racing" - ] - }, - { - "id": 6915, - "name": "Painkiller: Overdose", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 6916, - "name": "The Punisher (2005)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6917, - "name": "The Incredible Adventures of Van Helsing II", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6918, - "name": "Yandere School", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6919, - "name": "The Walking Dead: Michonne", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6920, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6921, - "name": "Papers, Please", - "site": "mobygames_com", - "genres": [ - "Simulation", - "Puzzle" - ] - }, - { - "id": 6922, - "name": "Rayman Legends", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 6923, - "name": "The Secret of Monkey Island: Special Edition", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6924, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "playground_ru", - "genres": [] - }, - { - "id": 6925, - "name": "The Incredible Adventures of Van Helsing III", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6926, - "name": "Star Wars: The Force Unleashed", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6927, - "name": "Year Walk", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Логика" - ] - }, - { - "id": 6928, - "name": "Slender: The Arrival", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6929, - "name": "The Slippery Slope", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6930, - "name": "The Walking Dead: Season One", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6931, - "name": "Paint the Town Red", - "site": "store_steampowered_com", - "genres": [ - "Early Access", - "Action", - "Indie" - ] - }, - { - "id": 6932, - "name": "The Last Remnant", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры" - ] - }, - { - "id": 6933, - "name": "South Park: The Stick of Truth", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6934, - "name": "Tesla vs Lovecraft", - "site": "playground_ru", - "genres": [ - "Экшен", - "Инди", - "Шутер", - "Вид сверху" - ] - }, - { - "id": 6935, - "name": "Rayman Origins", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 6936, - "name": "Peace, Death!", - "site": "mobygames_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 6937, - "name": "Star Wars: The Force Unleashed II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6938, - "name": "The Stanley Parable", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Квест" - ] - }, - { - "id": 6939, - "name": "Slendytubbies", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6940, - "name": "ZOMBI", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6941, - "name": "The Walking Dead: Season Two", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 6942, - "name": "The Mask Man", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6943, - "name": "Teslagrad", - "site": "playground_ru", - "genres": [ - "Логические", - "Инди", - "Платформер" - ] - }, - { - "id": 6944, - "name": "The Walking Dead: A New Frontier", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6945, - "name": "SteamWorld Dig", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6946, - "name": "Zero Escape: Zero Time Dilemma", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 6947, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 6948, - "name": "Slime Rancher", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6949, - "name": "The Punisher (2005)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6950, - "name": "The Witcher", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6951, - "name": "Panty Party", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Action", - "Indie", - "Adventure", - "Simulation" - ] - }, - { - "id": 6952, - "name": "The Walking Dead: Michonne", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6953, - "name": "Pillars of Eternity", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 6954, - "name": "Rayman Raving Rabbids", - "site": "gamespot_com", - "genres": [ - "Party/Minigame" - ] - }, - { - "id": 6955, - "name": "The Secret of Monkey Island: Special Edition", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6956, - "name": "The Banner Saga", - "site": "playground_ru", - "genres": [ - "Тактика", - "Ролевая", - "Стратегия", - "Пошаговая" - ] - }, - { - "id": 6957, - "name": "Ziggurat", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 6958, - "name": "SteamWorld Dig 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6959, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 6960, - "name": "The Walking Dead: Season One", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6961, - "name": "The Witcher 2: Assassins of Kings", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6962, - "name": "Sniper Elite", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6963, - "name": "The Slippery Slope", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6964, - "name": "Zombie Driver HD", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6965, - "name": "The Walking Dead: Season Two", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 6966, - "name": "Papers, Please", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 6967, - "name": "The Witcher 3: Wild Hunt", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6968, - "name": "Rayman Raving Rabbids 2", - "site": "gamespot_com", - "genres": [ - "Party/Minigame" - ] - }, - { - "id": 6969, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6970, - "name": "SteamWorld Heist", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Стратегии" - ] - }, - { - "id": 6971, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 6972, - "name": "The Banner Saga 2", - "site": "playground_ru", - "genres": [ - "Инди", - "Ролевая", - "Стратегия" - ] - }, - { - "id": 6973, - "name": "The Stanley Parable", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6974, - "name": "The Witcher", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 6975, - "name": "Zombie Party", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 6976, - "name": "Sniper Elite 3", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6977, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6978, - "name": "Steampunk Tower 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6979, - "name": "The Bard’s Tale (2004)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 6980, - "name": "The Witcher 2: Assassins of Kings", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 6981, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 6982, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 6983, - "name": "The Walking Dead: A New Frontier", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6984, - "name": "Zombie Vikings", - "site": "igromania_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 6985, - "name": "Peace, Death!", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Simulation", - "Indie" - ] - }, - { - "id": 6986, - "name": "Sniper Elite V2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 6987, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 6988, - "name": "Real Horror Stories", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 6989, - "name": "The Witcher 3: Wild Hunt", - "site": "gameguru_ru", - "genres": [ - "Открытый мир", - "RPG", - "aRPG" - ] - }, - { - "id": 6990, - "name": "Steel Rats", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 6991, - "name": "Zuma Deluxe", - "site": "igromania_ru", - "genres": [ - "Аркада", - "Логика" - ] - }, - { - "id": 6992, - "name": "The Walking Dead: Michonne", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 6993, - "name": "The Beginner's Guide", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Инди", - "Юмор" - ] - }, - { - "id": 6994, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 6995, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "Открытый мир", - "RPG", - "aRPG" - ] - }, - { - "id": 6996, - "name": "Sophie's Curse", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 6997, - "name": "The Wolf Among Us", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 6998, - "name": "The Walking Dead: Season One", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 6999, - "name": "Stories: The Path of Destinies", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7000, - "name": "Pillars of Eternity", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 7001, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "gameguru_ru", - "genres": [ - "Аддон", - "Открытый мир", - "RPG", - "aRPG" - ] - }, - { - "id": 7002, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7003, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7004, - "name": "Zuma's Revenge!", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7005, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 7006, - "name": "Redeemer", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7007, - "name": "Thief", - "site": "gamer_info_com", - "genres": [ - "stealth" - ] - }, - { - "id": 7008, - "name": "South Park: The Fractured But Whole", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7009, - "name": "The Wolf Among Us", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7010, - "name": "Street Fighter X Tekken", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7011, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7012, - "name": "The Binding of Isaac: Rebirth", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7013, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7014, - "name": "The Walking Dead: Season Two", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7015, - "name": "This Is The Police", - "site": "gamer_info_com", - "genres": [ - "инди", - "RTS" - ] - }, - { - "id": 7016, - "name": "Thief", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Стелс" - ] - }, - { - "id": 7017, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7018, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7019, - "name": "Reign Of Kings", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7021, - "name": "Pinstripe", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7022, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7023, - "name": "This Is The Police", - "site": "gameguru_ru", - "genres": [ - "Стратегия", - "Менеджемент", - "Квест" - ] - }, - { - "id": 7024, - "name": "TimeShift", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 7025, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7026, - "name": "The Bunker", - "site": "playground_ru", - "genres": [ - "Инди", - "Мультиплеер", - "Кооператив", - "Ретро", - "Экшен" - ] - }, - { - "id": 7027, - "name": "The Witcher", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 7028, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7029, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7030, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7031, - "name": "TimeShift", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 7032, - "name": "Stupidella", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7033, - "name": "Pirates of the Caribbean: At World’s End", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7034, - "name": "The Witcher 2: Assassins of Kings", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7035, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7036, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7037, - "name": "Titan Quest", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 7038, - "name": "The Cat Lady", - "site": "playground_ru", - "genres": [ - "Ужасы", - "Адвенчура", - "Квест" - ] - }, - { - "id": 7039, - "name": "Titan Quest", - "site": "gamer_info_com", - "genres": [ - "RPG", - "hack & slash" - ] - }, - { - "id": 7040, - "name": "Reigns", - "site": "gamespot_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 7041, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7042, - "name": "South Park: The Stick of Truth", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7043, - "name": "Sudeki", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7044, - "name": "Pizza Delivery 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7045, - "name": "Titan Quest Anniversary Edition", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 7046, - "name": "Невероятные Приключения Трояна", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7047, - "name": "Titan Quest Anniversary Edition", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7048, - "name": "The Council", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 7049, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7050, - "name": "Reigns: Her Majesty", - "site": "gamespot_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 7051, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7052, - "name": "Pinstripe", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 7053, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7054, - "name": "Невероятные Приключения Трояна 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7055, - "name": "Superhot", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7056, - "name": "Plague Inc: Evolved", - "site": "mobygames_com", - "genres": [ - "Simulation", - "Strategy / tactics" - ] - }, - { - "id": 7057, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7058, - "name": "The Curse of Blackwater", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7059, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 7060, - "name": "Невероятные Приключения Трояна 3", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7061, - "name": "Pirates of the Caribbean: At World’s End", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7062, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Приключения" - ] - }, - { - "id": 7063, - "name": "Surgeon Simulator", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7064, - "name": "Resident Evil", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 7065, - "name": "Titan Souls", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Инди" - ] - }, - { - "id": 7066, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7067, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7068, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7069, - "name": "Предатор", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7070, - "name": "Plants vs. Zombies", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 7071, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7072, - "name": "Titanfall 2", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Роботы" - ] - }, - { - "id": 7073, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7074, - "name": "Pizza Delivery 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7075, - "name": "Tales from the Borderlands", - "site": "iwantgames_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7076, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7077, - "name": "The Darkness II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7078, - "name": "The Witcher 3: Wild Hunt", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 7079, - "name": "Titan Souls", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 7080, - "name": "Стоп-Гоп", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7081, - "name": "To The Moon", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 7082, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7083, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7084, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7085, - "name": "The Elder Scrolls IV: Oblivion", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7086, - "name": "Tales of Berseria", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7087, - "name": "Стоп-Гоп 2", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7088, - "name": "Tomb Raider", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Квест" - ] - }, - { - "id": 7089, - "name": "Spooky's House of Jump Scares", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 7090, - "name": "Titanfall 2", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 7091, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7092, - "name": "Plague Inc: Evolved", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Simulation", - "Strategy", - "Indie" - ] - }, - { - "id": 7093, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7094, - "name": "Resident Evil 4", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 7095, - "name": "Torchlight", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 7096, - "name": "Pony Island", - "site": "mobygames_com", - "genres": [ - "Action", - "Puzzle", - "Adventure" - ] - }, - { - "id": 7097, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 7098, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7099, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 7100, - "name": "To The Moon", - "site": "gamer_info_com", - "genres": [ - "RPG", - "визуальные новеллы" - ] - }, - { - "id": 7101, - "name": "Tales of Zestiria", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "РПГ" - ] - }, - { - "id": 7102, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7103, - "name": "Torchlight II", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7104, - "name": "Plants vs. Zombies", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7105, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7106, - "name": "The Wolf Among Us", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7107, - "name": "Toren", - "site": "gameguru_ru", - "genres": [ - "Инди", - "Аркада", - "Платформер", - "Квест" - ] - }, - { - "id": 7108, - "name": "Star Wars: The Force Unleashed", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 7109, - "name": "Tomb Raider", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 7110, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7111, - "name": "Resident Evil 5", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 7112, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7113, - "name": "The Elder Scrolls V: Skyrim", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7114, - "name": "Tormentum: Dark Sorrow", - "site": "gameguru_ru", - "genres": [ - "Point-and-click", - "Квест" - ] - }, - { - "id": 7115, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7116, - "name": "Portal", - "site": "mobygames_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 7117, - "name": "Star Wars: The Force Unleashed II", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 7118, - "name": "Torchlight", - "site": "gamer_info_com", - "genres": [ - "RPG", - "hack & slash" - ] - }, - { - "id": 7119, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7120, - "name": "Tasty Planet: Back for Seconds", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7121, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7122, - "name": "Total Overdose", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7123, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7124, - "name": "Torchlight II", - "site": "gamer_info_com", - "genres": [ - "RPG", - "hack & slash" - ] - }, - { - "id": 7125, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7126, - "name": "Transistor", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 7127, - "name": "Thief", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения" - ] - }, - { - "id": 7128, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7129, - "name": "Tekken 7", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Аркады" - ] - }, - { - "id": 7130, - "name": "Pony Island", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 7131, - "name": "SteamWorld Dig", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Экшены", - "Головоломки", - "Платформеры" - ] - }, - { - "id": 7132, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7133, - "name": "Trine", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Платформер", - "Квест" - ] - }, - { - "id": 7134, - "name": "Toren", - "site": "gamer_info_com", - "genres": [ - "инди", - "приключения", - "платформеры" - ] - }, - { - "id": 7135, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7136, - "name": "This Is The Police", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Приключения" - ] - }, - { - "id": 7137, - "name": "Portal 2", - "site": "mobygames_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 7138, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7139, - "name": "SteamWorld Dig 2", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Платформеры", - "Приключения" - ] - }, - { - "id": 7140, - "name": "Resident Evil 6", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 7141, - "name": "Trine 2", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "На логику", - "Головоломка" - ] - }, - { - "id": 7142, - "name": "Portal", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7143, - "name": "Tormentum: Dark Sorrow", - "site": "gamer_info_com", - "genres": [ - "инди", - "приключения", - "квесты" - ] - }, - { - "id": 7144, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7145, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7146, - "name": "TimeShift", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 7147, - "name": "Trine 3: The Artifacts of Power", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Платформер" - ] - }, - { - "id": 7148, - "name": "SteamWorld Heist", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Приключения", - "Экшены", - "Платформеры", - "Стратегии" - ] - }, - { - "id": 7149, - "name": "Tesla vs Lovecraft", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7150, - "name": "Prey (2006)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7151, - "name": "Total Overdose", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7152, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7153, - "name": "Resident Evil 7: Biohazard", - "site": "gamespot_com", - "genres": [ - "VR", - "3D", - "Action", - "Survival", - "Adventure" - ] - }, - { - "id": 7154, - "name": "Troll Tale", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7155, - "name": "The Evil Within", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 7156, - "name": "Titan Quest", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 7157, - "name": "Steampunk Tower 2", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Стратегии" - ] - }, - { - "id": 7158, - "name": "Teslagrad", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7159, - "name": "Portal 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7160, - "name": "Transistor", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 7161, - "name": "Trollface Quest", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7162, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7163, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7164, - "name": "Titan Quest Anniversary Edition", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7165, - "name": "The Evil Within 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ужасы" - ] - }, - { - "id": 7166, - "name": "Steel Rats", - "site": "ag_ru", - "genres": [ - "Экшены", - "Гонки", - "Симуляторы" - ] - }, - { - "id": 7167, - "name": "The Banner Saga", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7168, - "name": "Trollface Quest 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7169, - "name": "Prey (2006)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7170, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7171, - "name": "Trine", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "платформеры" - ] - }, - { - "id": 7172, - "name": "Prince of Persia (2008)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7173, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7174, - "name": "Trollface Quest 3", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7175, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7176, - "name": "The Evil Within: The Assignment (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7177, - "name": "The Banner Saga 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7178, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7179, - "name": "Prince of Persia (2008)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7180, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7181, - "name": "Trine 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7182, - "name": "Trollface Quest 4", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7183, - "name": "Stories: The Path of Destinies", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Платформеры" - ] - }, - { - "id": 7184, - "name": "Spooky's House of Jump Scares", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7185, - "name": "The Evil Within: The Consequence (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7186, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7187, - "name": "The Bard’s Tale (2004)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7188, - "name": "Trollface Quest 5", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7189, - "name": "Prince of Persia: The Two Thrones", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7190, - "name": "Titan Souls", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 7191, - "name": "Trine 3: The Artifacts of Power", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "платформеры" - ] - }, - { - "id": 7192, - "name": "Street Fighter X Tekken", - "site": "ag_ru", - "genres": [ - "Экшены", - "Файтинги" - ] - }, - { - "id": 7193, - "name": "Prince of Persia: The Two Thrones", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7194, - "name": "Trollface Quest 6", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7195, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 7196, - "name": "The Evil Within: The Executioner (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7197, - "name": "The Beginner's Guide", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7198, - "name": "Titanfall 2", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Многопользоватеский шутер", - "Битвы машин", - "Боевик/Экшн" - ] - }, - { - "id": 7199, - "name": "Troll Tale", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7200, - "name": "Resident Evil: Operation Raccoon City", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 7201, - "name": "Trollface Quest 7", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7203, - "name": "Prospekt", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7204, - "name": "Star Wars: The Force Unleashed", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7205, - "name": "The Expendabros", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада" - ] - }, - { - "id": 7206, - "name": "Trollface Quest: Video Memes", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7207, - "name": "To The Moon", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Ролевые игры" - ] - }, - { - "id": 7208, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7209, - "name": "Trollface Quest", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7210, - "name": "Prospekt", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7211, - "name": "Stupidella", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7212, - "name": "Resident Evil: Revelations", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 7213, - "name": "Twin Sector", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 7214, - "name": "Trollface Quest 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7215, - "name": "Star Wars: The Force Unleashed II", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7216, - "name": "The Binding of Isaac: Rebirth", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "РПГ" - ] - }, - { - "id": 7217, - "name": "The Final Station", - "site": "playground_ru", - "genres": [ - "Ретро", - "Постапокалипсис", - "Выживание", - "Зомби", - "Платформер" - ] - }, - { - "id": 7218, - "name": "Prototype", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7219, - "name": "Two Worlds", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 7220, - "name": "Sudeki", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7221, - "name": "Trollface Quest 3", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7222, - "name": "Prototype", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7223, - "name": "Typical Nightmare", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7224, - "name": "The First Tree", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Инди" - ] - }, - { - "id": 7225, - "name": "The Bunker", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7226, - "name": "SteamWorld Dig", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7227, - "name": "Resident Evil: Revelations 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 7228, - "name": "Prototype 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7229, - "name": "Ultimate Epic Battle Simulator", - "site": "gameguru_ru", - "genres": [ - "В реальном времени", - "Стратегия", - "Симулятор" - ] - }, - { - "id": 7230, - "name": "Trollface Quest 4", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7231, - "name": "Superhot", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Инди", - "Экшены" - ] - }, - { - "id": 7232, - "name": "The Cat Lady", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7233, - "name": "SteamWorld Dig 2", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7234, - "name": "The Forest", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Ужасы", - "Кооператив", - "Выживание", - "Экшен", - "Открытый мир", - "От первого лица" - ] - }, - { - "id": 7235, - "name": "Ultra Street Fighter IV", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7236, - "name": "Prototype 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7237, - "name": "Trollface Quest 5", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7238, - "name": "Tomb Raider", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Платформер", - "Боевик/Экшн" - ] - }, - { - "id": 7239, - "name": "Psychonauts", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7240, - "name": "Undertale", - "site": "gameguru_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 7241, - "name": "Surgeon Simulator", - "site": "ag_ru", - "genres": [ - "Инди", - "Ролевые", - "Экшены", - "Симуляторы", - "Образовательные" - ] - }, - { - "id": 7242, - "name": "The Hat Man Shadow Ward", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7243, - "name": "The Council", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7244, - "name": "Reus", - "site": "gamespot_com", - "genres": [ - "Strategy" - ] - }, - { - "id": 7245, - "name": "SteamWorld Heist", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 7246, - "name": "Trollface Quest 6", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7247, - "name": "Torchlight", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7248, - "name": "Unfair Mario", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7249, - "name": "PuniTy", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7250, - "name": "Tales from the Borderlands", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7251, - "name": "Psychonauts", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7252, - "name": "The Curse of Blackwater", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7253, - "name": "The Incredible Adventures of Van Helsing", - "site": "playground_ru", - "genres": [ - "Экшен", - "Стимпанк", - "Ролевая" - ] - }, - { - "id": 7254, - "name": "Unmechanical", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка", - "Квест" - ] - }, - { - "id": 7255, - "name": "Steampunk Tower 2", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 7256, - "name": "Trollface Quest 7", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7257, - "name": "Torchlight II", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7258, - "name": "P·O·L·L·E·N", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 7259, - "name": "The Incredible Adventures of Van Helsing II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7260, - "name": "Unmechanical: Extended (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7261, - "name": "Tales of Berseria", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7262, - "name": "PuniTy", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7263, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7264, - "name": "Steel Rats", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7265, - "name": "Trollface Quest: Video Memes", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7266, - "name": "Until the last", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7267, - "name": "The Incredible Adventures of Van Helsing III", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7268, - "name": "Toren", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7269, - "name": "Rezrog", - "site": "gamespot_com", - "genres": [ - "Roguelike", - "Role-Playing" - ] - }, - { - "id": 7270, - "name": "Quake 4", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7271, - "name": "Tales of Zestiria", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 7272, - "name": "The Darkness II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7273, - "name": "Valiant Hearts: The Great War", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Квест" - ] - }, - { - "id": 7274, - "name": "Tormentum: Dark Sorrow", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7275, - "name": "Twin Sector", - "site": "gamer_info_com", - "genres": [ - "приключения", - "action" - ] - }, - { - "id": 7276, - "name": "Stories: The Path of Destinies", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7277, - "name": "The Last Remnant", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 7278, - "name": "P·O·L·L·E·N", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 7279, - "name": "Vanish", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7280, - "name": "The Elder Scrolls IV: Oblivion", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7281, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7282, - "name": "Total Overdose", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 7283, - "name": "Two Worlds", - "site": "gamer_info_com", - "genres": [ - "RPG" - ] - }, - { - "id": 7284, - "name": "Rise of the Tomb Raider", - "site": "gamespot_com", - "genres": [ - "VR", - "Action", - "Adventure" - ] - }, - { - "id": 7285, - "name": "The Mask Man", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7286, - "name": "Quantum Break", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7287, - "name": "Victor Vran", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "RPG" - ] - }, - { - "id": 7288, - "name": "Quake 4", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7289, - "name": "Street Fighter X Tekken", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7290, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7291, - "name": "Tasty Planet: Back for Seconds", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Семейные", - "Экшены", - "Казуальные" - ] - }, - { - "id": 7292, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7293, - "name": "Transistor", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7294, - "name": "The Punisher (2005)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7295, - "name": "Typical Nightmare", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7296, - "name": "Vikings: Wolves of Midgard", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 7297, - "name": "Raft", - "site": "mobygames_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 7299, - "name": "Visage", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 7300, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7301, - "name": "Tekken 7", - "site": "ag_ru", - "genres": [ - "Экшены", - "Спортивные", - "Файтинги" - ] - }, - { - "id": 7302, - "name": "Trine", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 7303, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7304, - "name": "Ultimate Epic Battle Simulator", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7305, - "name": "Quantum Break", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7306, - "name": "The Secret of Monkey Island: Special Edition", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Юмор", - "Квест" - ] - }, - { - "id": 7307, - "name": "Wanted: Weapons of Fate", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 7308, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7309, - "name": "Stupidella", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7310, - "name": "The Slippery Slope", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7311, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 7312, - "name": "Trine 2", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 7313, - "name": "Ultra Street Fighter IV", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7314, - "name": "Rage", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 7315, - "name": "The Elder Scrolls V: Skyrim", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7316, - "name": "Warcraft III: Reign of Chaos", - "site": "gameguru_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 7317, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7318, - "name": "Raft", - "site": "store_steampowered_com", - "genres": [ - "Early Access", - "Simulation", - "Indie", - "Adventure" - ] - }, - { - "id": 7319, - "name": "Warcraft III: The Frozen Throne", - "site": "gameguru_ru", - "genres": [ - "Стратегия" - ] - }, - { - "id": 7320, - "name": "Undertale", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди" - ] - }, - { - "id": 7321, - "name": "Trine 3: The Artifacts of Power", - "site": "gamebomb_ru", - "genres": [ - "Платформер", - "Головоломки/Пазл" - ] - }, - { - "id": 7322, - "name": "The Stanley Parable", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Инди", - "Юмор", - "От первого лица" - ] - }, - { - "id": 7323, - "name": "Sudeki", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7324, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7325, - "name": "Rampage Knights", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7326, - "name": "Tesla vs Lovecraft", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7327, - "name": "Warhammer 40.000: Space Marine", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 7328, - "name": "Troll Tale", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7329, - "name": "Unfair Mario", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7330, - "name": "Superhot", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7331, - "name": "The Walking Dead: A New Frontier", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Зомби" - ] - }, - { - "id": 7332, - "name": "Teslagrad", - "site": "ag_ru", - "genres": [ - "Приключения", - "Инди", - "Семейные", - "Экшены", - "Головоломки", - "Платформеры" - ] - }, - { - "id": 7333, - "name": "Warhammer: Chaosbane", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG" - ] - }, - { - "id": 7334, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7335, - "name": "Trollface Quest", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7336, - "name": "Risen", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 7337, - "name": "Rage", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7338, - "name": "Rayman Legends", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7339, - "name": "Unmechanical", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические", - "инди" - ] - }, - { - "id": 7340, - "name": "We Happy Few", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 7341, - "name": "Trollface Quest 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7342, - "name": "The Banner Saga", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Стратегии" - ] - }, - { - "id": 7343, - "name": "Surgeon Simulator", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7344, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7345, - "name": "The Walking Dead: Michonne", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7346, - "name": "Rock of Ages", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 7347, - "name": "Whack The Thief", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7348, - "name": "Trollface Quest 3", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7349, - "name": "Unmechanical: Extended (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7350, - "name": "Rayman Origins", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7351, - "name": "The Banner Saga 2", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Стратегии" - ] - }, - { - "id": 7352, - "name": "Rampage Knights", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 7353, - "name": "Trollface Quest 4", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7354, - "name": "The Walking Dead: Season One", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7355, - "name": "Whack Your Boss", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7356, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7357, - "name": "Tales from the Borderlands", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7358, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 7359, - "name": "Until the last", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7360, - "name": "The Bard’s Tale (2004)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7361, - "name": "Trollface Quest 5", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7362, - "name": "Whack Your Ex", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7363, - "name": "The Walking Dead: Season Two", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7364, - "name": "Rayman Raving Rabbids", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7365, - "name": "The Evil Within", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Экшен" - ] - }, - { - "id": 7366, - "name": "Tales of Berseria", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7367, - "name": "Rayman Legends", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7368, - "name": "Valiant Hearts: The Great War", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 7369, - "name": "Whack Your Neighbour", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7370, - "name": "Trollface Quest 6", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7371, - "name": "The Beginner's Guide", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 7372, - "name": "The Witcher", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Адвенчура", - "Ролевая", - "Экшен", - "Открытый мир", - "Фэнтези" - ] - }, - { - "id": 7373, - "name": "The Evil Within 2", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Приключения" - ] - }, - { - "id": 7374, - "name": "Whack Your Teacher", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7375, - "name": "Rayman Raving Rabbids 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7376, - "name": "Tales of Zestiria", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7377, - "name": "Trollface Quest 7", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7378, - "name": "Vanish", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7379, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7380, - "name": "Root Of Evil: The Tailor", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 7381, - "name": "Rayman Origins", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7382, - "name": "What Remains of Edith Finch", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 7383, - "name": "Trollface Quest: Video Memes", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7384, - "name": "The Witcher 2: Assassins of Kings", - "site": "playground_ru", - "genres": [ - "Экшен", - "От третьего лица", - "Ролевая", - "Фэнтези" - ] - }, - { - "id": 7385, - "name": "Real Horror Stories", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7386, - "name": "The Evil Within: The Assignment (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7387, - "name": "Victor Vran", - "site": "gamer_info_com", - "genres": [ - "RPG", - "action" - ] - }, - { - "id": 7388, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7389, - "name": "Will Rock", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 7390, - "name": "The Binding of Isaac: Rebirth", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7391, - "name": "Twin Sector", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Головоломки/Пазл" - ] - }, - { - "id": 7392, - "name": "Redeemer", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7393, - "name": "Vikings: Wolves of Midgard", - "site": "gamer_info_com", - "genres": [ - "hack & slash" - ] - }, - { - "id": 7394, - "name": "The Evil Within: The Consequence (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7395, - "name": "Willy-Nilly Knight", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG", - "Инди" - ] - }, - { - "id": 7396, - "name": "Rayman Raving Rabbids", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7397, - "name": "The Witcher 3: Wild Hunt", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Ролевая", - "Экшен", - "Открытый мир", - "Фэнтези" - ] - }, - { - "id": 7398, - "name": "Tasty Planet: Back for Seconds", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7399, - "name": "Runic Rampage", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7400, - "name": "The Bunker", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 7401, - "name": "Wizard of Legend", - "site": "gameguru_ru", - "genres": [ - "RPG", - "aRPG", - "Инди" - ] - }, - { - "id": 7402, - "name": "Two Worlds", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Ролевые игры" - ] - }, - { - "id": 7403, - "name": "Visage", - "site": "gamer_info_com", - "genres": [ - "инди", - "ужасы" - ] - }, - { - "id": 7404, - "name": "The Evil Within: The Executioner (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7405, - "name": "Reign Of Kings", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7406, - "name": "Tekken 7", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7407, - "name": "Rayman Raving Rabbids 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7408, - "name": "Wolfenstein", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7409, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7410, - "name": "Typical Nightmare", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7411, - "name": "Wanted: Weapons of Fate", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7412, - "name": "The Expendabros", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7413, - "name": "Wolfenstein: The New Order", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 7414, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7415, - "name": "The Cat Lady", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 7416, - "name": "Ultimate Epic Battle Simulator", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7417, - "name": "Real Horror Stories", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7418, - "name": "Reigns", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 7419, - "name": "Rust", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 7420, - "name": "Wolfenstein: The Old Blood", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер", - "Аддон", - "Самостоятельный" - ] - }, - { - "id": 7421, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7422, - "name": "Warcraft III: Reign of Chaos", - "site": "gamer_info_com", - "genres": [ - "RTS" - ] - }, - { - "id": 7423, - "name": "The Final Station", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7424, - "name": "Tesla vs Lovecraft", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7425, - "name": "Ultra Street Fighter IV", - "site": "gamebomb_ru", - "genres": [ - "Драки/Файтинг" - ] - }, - { - "id": 7426, - "name": "Woolfe - The Red Hood Diaries", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Платформер" - ] - }, - { - "id": 7427, - "name": "The Council", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 7428, - "name": "Redeemer", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7429, - "name": "Reigns: Her Majesty", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 7430, - "name": "Warcraft III: The Frozen Throne", - "site": "gamer_info_com", - "genres": [ - "RTS" - ] - }, - { - "id": 7431, - "name": "The Wolf Among Us", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 7432, - "name": "Ryse: Son of Rome", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Adventure" - ] - }, - { - "id": 7433, - "name": "The First Tree", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7434, - "name": "Worms 2", - "site": "gameguru_ru", - "genres": [ - "В пошаговом режиме", - "Стратегия" - ] - }, - { - "id": 7435, - "name": "Undertale", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры" - ] - }, - { - "id": 7436, - "name": "Teslagrad", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7437, - "name": "The Curse of Blackwater", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7438, - "name": "Thief", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7439, - "name": "Warhammer 40.000: Space Marine", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 7440, - "name": "X-Blades / Ониблэйд", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7441, - "name": "Resident Evil", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7442, - "name": "Unfair Mario", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7443, - "name": "Reign Of Kings", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Action", - "Indie", - "Adventure", - "Simulation" - ] - }, - { - "id": 7444, - "name": "The Forest", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Экшен", - "Приключения", - "Симуляторы" - ] - }, - { - "id": 7445, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 7446, - "name": "The Banner Saga", - "site": "stopgame_ru", - "genres": [ - "strategy", - "rpg" - ] - }, - { - "id": 7447, - "name": "X-Morph: Defense", - "site": "gameguru_ru", - "genres": [ - "Shoot-em-up", - "Аркада", - "Стратегия", - "В реальном времени" - ] - }, - { - "id": 7448, - "name": "Warhammer: Chaosbane", - "site": "gamer_info_com", - "genres": [ - "hack & slash" - ] - }, - { - "id": 7449, - "name": "This Is The Police", - "site": "playground_ru", - "genres": [ - "Пошаговая", - "Адвенчура", - "Стратегия", - "Менеджер" - ] - }, - { - "id": 7450, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7451, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7452, - "name": "Unmechanical", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл" - ] - }, - { - "id": 7453, - "name": "The Hat Man Shadow Ward", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7454, - "name": "Yandere School", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7455, - "name": "The Banner Saga 2", - "site": "stopgame_ru", - "genres": [ - "strategy", - "rpg" - ] - }, - { - "id": 7456, - "name": "We Happy Few", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 7457, - "name": "Unmechanical: Extended (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7458, - "name": "Reigns", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Simulation", - "Indie", - "Adventure" - ] - }, - { - "id": 7459, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7460, - "name": "SCP-087", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7461, - "name": "TimeShift", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер", - "Научная фантастика" - ] - }, - { - "id": 7462, - "name": "Year Walk", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка", - "Квест" - ] - }, - { - "id": 7463, - "name": "The Darkness II", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 7464, - "name": "The Incredible Adventures of Van Helsing", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7465, - "name": "Until the last", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7466, - "name": "Whack The Thief", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7467, - "name": "ZOMBI", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7468, - "name": "The Bard’s Tale (2004)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7469, - "name": "SCP-087-B", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7470, - "name": "Titan Quest", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая", - "Фэнтези", - "Вид сверху" - ] - }, - { - "id": 7471, - "name": "Resident Evil 4", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7472, - "name": "The Incredible Adventures of Van Helsing II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7473, - "name": "Zero Escape: Zero Time Dilemma", - "site": "gameguru_ru", - "genres": [ - "Визуальный роман", - "Квест" - ] - }, - { - "id": 7474, - "name": "Reigns: Her Majesty", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Simulation", - "Indie" - ] - }, - { - "id": 7475, - "name": "Whack Your Boss", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7476, - "name": "The Elder Scrolls IV: Oblivion", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 7477, - "name": "Valiant Hearts: The Great War", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Головоломки/Пазл" - ] - }, - { - "id": 7478, - "name": "The Beginner's Guide", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7479, - "name": "Titan Quest Anniversary Edition", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7480, - "name": "Ziggurat", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 7481, - "name": "SINNER: Sacrifice for Redemption", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7482, - "name": "The Incredible Adventures of Van Helsing III", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7483, - "name": "Resident Evil 5", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7484, - "name": "Whack Your Ex", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7485, - "name": "Resident Evil", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7486, - "name": "Zombie Driver HD", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Аркада", - "Гонки/вождение" - ] - }, - { - "id": 7487, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7488, - "name": "Vanish", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7489, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7490, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7491, - "name": "Zombie Party", - "site": "gameguru_ru", - "genres": [ - "Shoot-em-up", - "Аркада", - "Инди" - ] - }, - { - "id": 7492, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7493, - "name": "SWAT 4", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 7494, - "name": "Whack Your Neighbour", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7495, - "name": "The Last Remnant", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7496, - "name": "Victor Vran", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7497, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7498, - "name": "Zombie Vikings", - "site": "gameguru_ru", - "genres": [ - "Аркада", - "Beat-em-up" - ] - }, - { - "id": 7499, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 7500, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7501, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7502, - "name": "The Binding of Isaac: Rebirth", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7503, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7504, - "name": "Whack Your Teacher", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7505, - "name": "The Mask Man", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7506, - "name": "Zuma Deluxe", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка" - ] - }, - { - "id": 7507, - "name": "Vikings: Wolves of Midgard", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7508, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7509, - "name": "Titan Souls", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 7510, - "name": "The Bunker", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7511, - "name": "The Elder Scrolls V: Skyrim", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7512, - "name": "What Remains of Edith Finch", - "site": "gamer_info_com", - "genres": [ - "приключения" - ] - }, - { - "id": 7513, - "name": "Zuma's Revenge!", - "site": "gameguru_ru", - "genres": [ - "На логику", - "Головоломка" - ] - }, - { - "id": 7514, - "name": "Resident Evil 6", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7515, - "name": "The Punisher (2005)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7516, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7517, - "name": "Visage", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7518, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7519, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7520, - "name": "Titanfall 2", - "site": "playground_ru", - "genres": [ - "Экшен", - "Мультиплеер", - "От первого лица", - "Шутер" - ] - }, - { - "id": 7521, - "name": "The Cat Lady", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7522, - "name": "Will Rock", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7523, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 7524, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7525, - "name": "Wanted: Weapons of Fate", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 7526, - "name": "The Secret of Monkey Island: Special Edition", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7527, - "name": "Resident Evil 7: Biohazard", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7528, - "name": "Resident Evil 4", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7529, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7530, - "name": "Willy-Nilly Knight", - "site": "gamer_info_com", - "genres": [ - "RPG", - "инди" - ] - }, - { - "id": 7531, - "name": "To The Moon", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Квест" - ] - }, - { - "id": 7532, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7533, - "name": "The Council", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7534, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7535, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7536, - "name": "The Slippery Slope", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7537, - "name": "Warcraft III: Reign of Chaos", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Стратегия в реальном времени/РТС" - ] - }, - { - "id": 7538, - "name": "Resident Evil 5", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7539, - "name": "Tomb Raider", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7540, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7541, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7542, - "name": "Wizard of Legend", - "site": "gamer_info_com", - "genres": [ - "инди", - "RPG", - "hack & slash" - ] - }, - { - "id": 7543, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7544, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7545, - "name": "The Curse of Blackwater", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7546, - "name": "Warcraft III: The Frozen Throne", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Стратегия в реальном времени/РТС" - ] - }, - { - "id": 7547, - "name": "The Stanley Parable", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7548, - "name": "Невероятные Приключения Трояна", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7549, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7550, - "name": "Wolfenstein", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 7551, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7552, - "name": "Torchlight", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая", - "Фэнтези", - "Вид сверху" - ] - }, - { - "id": 7553, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7554, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 7555, - "name": "Невероятные Приключения Трояна 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7556, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7557, - "name": "The Walking Dead: A New Frontier", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7558, - "name": "Warhammer 40.000: Space Marine", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 7559, - "name": "Wolfenstein: The New Order", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 7560, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7561, - "name": "Torchlight II", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7562, - "name": "Невероятные Приключения Трояна 3", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7563, - "name": "Sacred", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 7564, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7565, - "name": "The Evil Within", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 7566, - "name": "The Darkness II", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7567, - "name": "Warhammer: Chaosbane", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7568, - "name": "The Walking Dead: Michonne", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7569, - "name": "Предатор", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7570, - "name": "Wolfenstein: The Old Blood", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 7571, - "name": "Toren", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Инди", - "От первого лица" - ] - }, - { - "id": 7572, - "name": "Resident Evil 6", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7573, - "name": "The Evil Within 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Приключения", - "Экшены" - ] - }, - { - "id": 7574, - "name": "Стоп-Гоп", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7575, - "name": "Sacred 3", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 7576, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7577, - "name": "We Happy Few", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения", - "Приключения", - "Ролевые игры" - ] - }, - { - "id": 7578, - "name": "The Elder Scrolls IV: Oblivion", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7579, - "name": "Woolfe - The Red Hood Diaries", - "site": "gamer_info_com", - "genres": [ - "платформеры" - ] - }, - { - "id": 7580, - "name": "The Walking Dead: Season One", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7581, - "name": "Tormentum: Dark Sorrow", - "site": "playground_ru", - "genres": [ - "Инди", - "Ужасы", - "Адвенчура", - "Квест", - "Логические", - "Фэнтези" - ] - }, - { - "id": 7582, - "name": "Resident Evil 7: Biohazard", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7583, - "name": "Стоп-Гоп 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7584, - "name": "Whack The Thief", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7585, - "name": "The Evil Within: The Assignment (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 7586, - "name": "Worms 2", - "site": "gamer_info_com", - "genres": [ - "аркадные", - "TBS" - ] - }, - { - "id": 7587, - "name": "Sacred Citadel", - "site": "gamespot_com", - "genres": [ - "Beat-'Em-Up", - "Action", - "2D" - ] - }, - { - "id": 7588, - "name": "The Walking Dead: Season Two", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7589, - "name": "Resident Evil: Operation Raccoon City", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7590, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 7591, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7592, - "name": "Whack Your Boss", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7593, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7594, - "name": "Total Overdose", - "site": "playground_ru", - "genres": [ - "Экшен", - "Открытый мир", - "От третьего лица" - ] - }, - { - "id": 7595, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7596, - "name": "X-Blades / Ониблэйд", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7597, - "name": "Whack Your Ex", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7598, - "name": "The Evil Within: The Consequence (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 7599, - "name": "The Witcher", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7600, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7601, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7602, - "name": "Whack Your Neighbour", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7603, - "name": "Transistor", - "site": "playground_ru", - "genres": [ - "Инди", - "Адвенчура", - "Пошаговая", - "Научная фантастика", - "Ролевая", - "Экшен" - ] - }, - { - "id": 7604, - "name": "Resident Evil: Revelations", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7605, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7606, - "name": "X-Morph: Defense", - "site": "gamer_info_com", - "genres": [ - "shoot 'em up", - "tower defense" - ] - }, - { - "id": 7607, - "name": "The Witcher 2: Assassins of Kings", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7608, - "name": "Whack Your Teacher", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7609, - "name": "The Evil Within: The Executioner (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 7610, - "name": "The Elder Scrolls V: Skyrim", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7611, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7612, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7613, - "name": "Trine", - "site": "playground_ru", - "genres": [ - "Аркада", - "Платформер" - ] - }, - { - "id": 7614, - "name": "Yandere School", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7615, - "name": "Resident Evil: Revelations 2", - "site": "mobygames_com", - "genres": [ - "Compilation" - ] - }, - { - "id": 7616, - "name": "The Expendabros", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Платформеры" - ] - }, - { - "id": 7617, - "name": "What Remains of Edith Finch", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7618, - "name": "The Witcher 3: Wild Hunt", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7619, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7620, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7621, - "name": "Year Walk", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "логические", - "приключения" - ] - }, - { - "id": 7622, - "name": "Trine 2", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 7623, - "name": "The Final Station", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 7624, - "name": "Saints Row IV", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7625, - "name": "Will Rock", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 7626, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7627, - "name": "Reus", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 7628, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7629, - "name": "ZOMBI", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "action" - ] - }, - { - "id": 7630, - "name": "Willy-Nilly Knight", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7631, - "name": "Trine 3: The Artifacts of Power", - "site": "playground_ru", - "genres": [ - "Экшен", - "Аркада", - "Адвенчура", - "Платформер" - ] - }, - { - "id": 7632, - "name": "The First Tree", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 7633, - "name": "Resident Evil: Operation Raccoon City", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7634, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7635, - "name": "Saints Row: Gat out of Hell", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7636, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7637, - "name": "Wizard of Legend", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Боевик/Экшн" - ] - }, - { - "id": 7638, - "name": "Zero Escape: Zero Time Dilemma", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "визуальные новеллы", - "логические" - ] - }, - { - "id": 7639, - "name": "Troll Tale", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7640, - "name": "Rezrog", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 7641, - "name": "Resident Evil: Revelations", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7642, - "name": "The Forest", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 7643, - "name": "The Wolf Among Us", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 7644, - "name": "Salt and Sanctuary", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 7645, - "name": "Trollface Quest", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7646, - "name": "Wolfenstein", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 7647, - "name": "Ziggurat", - "site": "gamer_info_com", - "genres": [ - "FPS", - "инди" - ] - }, - { - "id": 7648, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7649, - "name": "The Hat Man Shadow Ward", - "site": "ag_ru", - "genres": [ - "Спортивные", - "Ролевые", - "Приключения", - "Инди", - "Экшены", - "Гонки", - "Казуальные", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 7650, - "name": "Resident Evil: Revelations 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7651, - "name": "Thief", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7652, - "name": "Trollface Quest 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7653, - "name": "Samsara Room", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7654, - "name": "Zombie Driver HD", - "site": "gamer_info_com", - "genres": [ - "гонки" - ] - }, - { - "id": 7655, - "name": "Rise of the Tomb Raider", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7656, - "name": "Wolfenstein: The New Order", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик-приключения" - ] - }, - { - "id": 7657, - "name": "The Evil Within", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7658, - "name": "The Incredible Adventures of Van Helsing", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7659, - "name": "Trollface Quest 3", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7660, - "name": "Saw: The Video Game", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7661, - "name": "This Is The Police", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7662, - "name": "Zombie Party", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7663, - "name": "Reus", - "site": "store_steampowered_com", - "genres": [ - "Simulation", - "Strategy", - "Indie" - ] - }, - { - "id": 7664, - "name": "The Evil Within 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7665, - "name": "Trollface Quest 4", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7666, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7667, - "name": "Wolfenstein: The Old Blood", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 7668, - "name": "The Incredible Adventures of Van Helsing II", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7669, - "name": "TimeShift", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7670, - "name": "Zombie Vikings", - "site": "gamer_info_com", - "genres": [ - "hack & slash" - ] - }, - { - "id": 7671, - "name": "Woolfe - The Red Hood Diaries", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7672, - "name": "Trollface Quest 5", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7673, - "name": "Scratches", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 7674, - "name": "The Evil Within: The Assignment (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7675, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7676, - "name": "The Incredible Adventures of Van Helsing III", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 7677, - "name": "Titan Quest", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7678, - "name": "Rezrog", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 7679, - "name": "Zuma Deluxe", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7680, - "name": "Trollface Quest 6", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7681, - "name": "Worms 2", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Боевик/Экшн" - ] - }, - { - "id": 7682, - "name": "The Evil Within: The Consequence (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7683, - "name": "Seasons after Fall", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 7684, - "name": "The Last Remnant", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Стратегии" - ] - }, - { - "id": 7685, - "name": "X-Blades / Ониблэйд", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7686, - "name": "Trollface Quest 7", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7687, - "name": "Zuma's Revenge!", - "site": "gamer_info_com", - "genres": [ - "головоломки", - "аркадные", - "логические" - ] - }, - { - "id": 7688, - "name": "Titan Quest Anniversary Edition", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7689, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7690, - "name": "The Evil Within: The Executioner (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7691, - "name": "X-Morph: Defense", - "site": "gamebomb_ru", - "genres": [ - "Стратегия", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 7692, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7693, - "name": "The Mask Man", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7694, - "name": "Rise of the Tomb Raider", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7695, - "name": "Trollface Quest: Video Memes", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7696, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7697, - "name": "Yandere School", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7698, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 7699, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7700, - "name": "The Expendabros", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7701, - "name": "The Punisher (2005)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7702, - "name": "Twin Sector", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 7703, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7704, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7705, - "name": "Serena", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 7706, - "name": "Year Walk", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Головоломки/Пазл" - ] - }, - { - "id": 7707, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7708, - "name": "The Final Station", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7709, - "name": "Risen", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 7710, - "name": "Serious Sam 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7711, - "name": "The Secret of Monkey Island: Special Edition", - "site": "ag_ru", - "genres": [ - "Приключения", - "Семейные" - ] - }, - { - "id": 7712, - "name": "Two Worlds", - "site": "playground_ru", - "genres": [ - "Ролевая" - ] - }, - { - "id": 7713, - "name": "Titan Souls", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7714, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7715, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7716, - "name": "ZOMBI", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Боевик/Экшн" - ] - }, - { - "id": 7717, - "name": "The First Tree", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7718, - "name": "The Slippery Slope", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7719, - "name": "Typical Nightmare", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7720, - "name": "Serious Sam 3: BFE", - "site": "gamespot_com", - "genres": [ - "VR", - "3D", - "Action", - "First-Person", - "Shooter" - ] - }, - { - "id": 7721, - "name": "Titanfall 2", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 7722, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7723, - "name": "Невероятные Приключения Трояна", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7724, - "name": "Zero Escape: Zero Time Dilemma", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7725, - "name": "Rock of Ages", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7726, - "name": "The Forest", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7727, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7728, - "name": "Ultimate Epic Battle Simulator", - "site": "playground_ru", - "genres": [ - "Другой симулятор", - "Стратегия" - ] - }, - { - "id": 7729, - "name": "The Stanley Parable", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 7730, - "name": "Ziggurat", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 7731, - "name": "Невероятные Приключения Трояна 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7732, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7733, - "name": "To The Moon", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7734, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7735, - "name": "Serious Sam Classic: The First Encounter", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7736, - "name": "The Hat Man Shadow Ward", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7737, - "name": "Ultra Street Fighter IV", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7738, - "name": "Zombie Driver HD", - "site": "gamebomb_ru", - "genres": [ - "Гоночные/Рейсинг", - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 7739, - "name": "Невероятные Приключения Трояна 3", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7740, - "name": "Tomb Raider", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7741, - "name": "Serious Sam Classic: The Second Encounter", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7742, - "name": "The Walking Dead: A New Frontier", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7743, - "name": "Zombie Party", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7744, - "name": "Risen", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 7745, - "name": "The Incredible Adventures of Van Helsing", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7746, - "name": "Undertale", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ролевая" - ] - }, - { - "id": 7747, - "name": "Предатор", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7748, - "name": "Root Of Evil: The Tailor", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7749, - "name": "Torchlight", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7750, - "name": "Zombie Vikings", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 7751, - "name": "Unfair Mario", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7752, - "name": "Shadow Warrior", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 7753, - "name": "The Walking Dead: Michonne", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7754, - "name": "The Incredible Adventures of Van Helsing II", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7755, - "name": "Стоп-Гоп", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7756, - "name": "Torchlight II", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7757, - "name": "Zuma Deluxe", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7758, - "name": "Runic Rampage", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7759, - "name": "Rock of Ages", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Racing", - "Indie" - ] - }, - { - "id": 7760, - "name": "Unmechanical", - "site": "playground_ru", - "genres": [ - "Логические" - ] - }, - { - "id": 7761, - "name": "The Walking Dead: Season One", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7762, - "name": "Стоп-Гоп 2", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7763, - "name": "Shadow of the Tomb Raider", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7764, - "name": "The Incredible Adventures of Van Helsing III", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7765, - "name": "Toren", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 7766, - "name": "Zuma's Revenge!", - "site": "gamebomb_ru", - "genres": [ - "Головоломки/Пазл" - ] - }, - { - "id": 7767, - "name": "Unmechanical: Extended (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7768, - "name": "Rust", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7769, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7770, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 7771, - "name": "The Walking Dead: Season Two", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7772, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Racing", - "Indie" - ] - }, - { - "id": 7773, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7774, - "name": "The Last Remnant", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7775, - "name": "Tormentum: Dark Sorrow", - "site": "iwantgames_ru", - "genres": [ - "Головоломки", - "Приключения" - ] - }, - { - "id": 7776, - "name": "Until the last", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7777, - "name": "Ryse: Son of Rome", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7778, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7779, - "name": "Shantae: Half-Genie Hero", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 7780, - "name": "The Witcher", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7781, - "name": "The Mask Man", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7782, - "name": "Total Overdose", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7783, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7784, - "name": "Valiant Hearts: The Great War", - "site": "playground_ru", - "genres": [ - "Логические", - "Аркада", - "Адвенчура" - ] - }, - { - "id": 7785, - "name": "Root Of Evil: The Tailor", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Action", - "Indie", - "Adventure", - "Simulation" - ] - }, - { - "id": 7786, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7787, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7788, - "name": "The Witcher 2: Assassins of Kings", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7789, - "name": "Vanish", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7790, - "name": "The Punisher (2005)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7791, - "name": "Transistor", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7792, - "name": "Невероятные Приключения Трояна", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7793, - "name": "Runic Rampage", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7794, - "name": "Shoot Your Nightmare Halloween Special", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 7795, - "name": "SCP-087", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 7796, - "name": "Trine", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7797, - "name": "Victor Vran", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 7798, - "name": "The Secret of Monkey Island: Special Edition", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7799, - "name": "Невероятные Приключения Трояна 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7800, - "name": "The Witcher 3: Wild Hunt", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7801, - "name": "Shovel Knight: Shovel of Hope", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7802, - "name": "Невероятные Приключения Трояна 3", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7803, - "name": "Trine 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7804, - "name": "Rust", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Action", - "Massively Multiplayer", - "Indie", - "Adventure" - ] - }, - { - "id": 7805, - "name": "Vikings: Wolves of Midgard", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Ролевая", - "Вид сверху" - ] - }, - { - "id": 7806, - "name": "The Slippery Slope", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7807, - "name": "SCP-087-B", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7808, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7809, - "name": "Shrek 2: The Game", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7810, - "name": "Предатор", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7811, - "name": "Trine 3: The Artifacts of Power", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Головоломки", - "Приключения" - ] - }, - { - "id": 7812, - "name": "Стоп-Гоп", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7813, - "name": "The Stanley Parable", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7814, - "name": "Visage", - "site": "playground_ru", - "genres": [ - "Ужасы", - "От первого лица" - ] - }, - { - "id": 7815, - "name": "SINNER: Sacrifice for Redemption", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 7816, - "name": "Silence on The Line", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7817, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7818, - "name": "Ryse: Son of Rome", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7819, - "name": "Стоп-Гоп 2", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7820, - "name": "Troll Tale", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7821, - "name": "Silent Hill: Alchemilla", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7822, - "name": "Wanted: Weapons of Fate", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 7823, - "name": "The Walking Dead: A New Frontier", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7824, - "name": "The Wolf Among Us", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7825, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 7826, - "name": "SWAT 4", - "site": "mobygames_com", - "genres": [ - "Action", - "Simulation", - "Strategy / tactics" - ] - }, - { - "id": 7827, - "name": "Warcraft III: Reign of Chaos", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7828, - "name": "Trollface Quest", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7829, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 7830, - "name": "The Walking Dead: Michonne", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7831, - "name": "Thief", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 7832, - "name": "Warcraft III: The Frozen Throne", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7833, - "name": "Silverfall", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 7834, - "name": "Trollface Quest 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7835, - "name": "SCP-087", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7836, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition", - "Puzzle", - "Adventure" - ] - }, - { - "id": 7837, - "name": "The Walking Dead: Season One", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7838, - "name": "This Is The Police", - "site": "ag_ru", - "genres": [ - "Приключения", - "Инди", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 7839, - "name": "Warhammer 40.000: Space Marine", - "site": "playground_ru", - "genres": [ - "От третьего лица", - "Кооператив", - "Научная фантастика", - "Шутер", - "Экшен" - ] - }, - { - "id": 7840, - "name": "Silverfall: Earth Awakening", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 7841, - "name": "Trollface Quest 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7842, - "name": "SCP-087-B", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7843, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7844, - "name": "The Walking Dead: Season Two", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7845, - "name": "TimeShift", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 7846, - "name": "Warhammer: Chaosbane", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая", - "Фэнтези" - ] - }, - { - "id": 7847, - "name": "Trollface Quest 4", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7848, - "name": "Titan Quest", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7849, - "name": "SINNER: Sacrifice for Redemption", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 7850, - "name": "The Witcher", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7851, - "name": "We Happy Few", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Инди", - "Ужасы" - ] - }, - { - "id": 7852, - "name": "Singularity", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 7853, - "name": "Trollface Quest 5", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7854, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition" - ] - }, - { - "id": 7855, - "name": "Titan Quest Anniversary Edition", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7856, - "name": "Whack The Thief", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7857, - "name": "SWAT 4", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7858, - "name": "Slap-Man", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7859, - "name": "The Witcher 2: Assassins of Kings", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7860, - "name": "Trollface Quest 6", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7861, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7862, - "name": "Whack Your Boss", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7863, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7864, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7865, - "name": "Slay the Spire", - "site": "gamespot_com", - "genres": [ - "Card Game" - ] - }, - { - "id": 7866, - "name": "The Witcher 3: Wild Hunt", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7867, - "name": "Trollface Quest 7", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7868, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7869, - "name": "Whack Your Ex", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7870, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7871, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7872, - "name": "Trollface Quest: Video Memes", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7873, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7874, - "name": "Whack Your Neighbour", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7875, - "name": "Sleeping Dogs", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7876, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition", - "Adventure" - ] - }, - { - "id": 7877, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7878, - "name": "Twin Sector", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7879, - "name": "Whack Your Teacher", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7880, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7881, - "name": "Titan Souls", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди" - ] - }, - { - "id": 7882, - "name": "Slender: The Arrival", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 7883, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7884, - "name": "Two Worlds", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7885, - "name": "What Remains of Edith Finch", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "От первого лица" - ] - }, - { - "id": 7886, - "name": "Titanfall 2", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 7887, - "name": "The Wolf Among Us", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7888, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition", - "Adventure" - ] - }, - { - "id": 7889, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7890, - "name": "Slendytubbies", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7891, - "name": "Typical Nightmare", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7892, - "name": "Will Rock", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 7893, - "name": "To The Moon", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 7894, - "name": "Thief", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7895, - "name": "Sacred", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 7896, - "name": "Ultimate Epic Battle Simulator", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7897, - "name": "Slime Rancher", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 7898, - "name": "Willy-Nilly Knight", - "site": "playground_ru", - "genres": [ - "Тактика", - "Ролевая", - "Инди", - "Фэнтези" - ] - }, - { - "id": 7899, - "name": "Tomb Raider", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 7900, - "name": "This Is The Police", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 7901, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 7902, - "name": "Ultra Street Fighter IV", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7903, - "name": "Sacred 3", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 7904, - "name": "Torchlight", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7905, - "name": "Wizard of Legend", - "site": "playground_ru", - "genres": [ - "Слэшер", - "Подземелья", - "Инди", - "Ролевая", - "Экшен" - ] - }, - { - "id": 7906, - "name": "Sniper Elite", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "Tactical", - "Shooter" - ] - }, - { - "id": 7907, - "name": "TimeShift", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7908, - "name": "Undertale", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7909, - "name": "Torchlight II", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 7910, - "name": "Sniper Elite 3", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7911, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 7912, - "name": "Sacred Citadel", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7913, - "name": "Wolfenstein", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Шутер" - ] - }, - { - "id": 7914, - "name": "Titan Quest", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7915, - "name": "Unfair Mario", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7916, - "name": "Toren", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 7917, - "name": "Sacred", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7918, - "name": "Wolfenstein: The New Order", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица" - ] - }, - { - "id": 7919, - "name": "Titan Quest Anniversary Edition", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7920, - "name": "Sniper Elite V2", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "Shooter", - "Tactical" - ] - }, - { - "id": 7921, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7922, - "name": "Unmechanical", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7923, - "name": "Tormentum: Dark Sorrow", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 7924, - "name": "Wolfenstein: The Old Blood", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7925, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7926, - "name": "Unmechanical: Extended (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7927, - "name": "Sacred 3", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 7928, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7929, - "name": "Total Overdose", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 7930, - "name": "Sophie's Curse", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 7931, - "name": "Woolfe - The Red Hood Diaries", - "site": "playground_ru", - "genres": [ - "Экшен", - "Адвенчура", - "Слэшер", - "Платформер" - ] - }, - { - "id": 7932, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7933, - "name": "Until the last", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7934, - "name": "Transistor", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди" - ] - }, - { - "id": 7935, - "name": "Worms 2", - "site": "playground_ru", - "genres": [ - "Логические", - "Стратегия" - ] - }, - { - "id": 7936, - "name": "Titan Souls", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 7937, - "name": "Sacred Citadel", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7938, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7939, - "name": "South Park: The Fractured But Whole", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 7940, - "name": "Valiant Hearts: The Great War", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7941, - "name": "Trine", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Аркадные" - ] - }, - { - "id": 7942, - "name": "X-Blades / Ониблэйд", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7943, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7944, - "name": "Titanfall 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7945, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7946, - "name": "Saints Row IV", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 7947, - "name": "Trine 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7948, - "name": "Vanish", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7949, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7950, - "name": "X-Morph: Defense", - "site": "playground_ru", - "genres": [ - "Инди", - "Стратегия", - "Tower defence" - ] - }, - { - "id": 7951, - "name": "To The Moon", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7952, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7953, - "name": "Trine 3: The Artifacts of Power", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 7954, - "name": "Victor Vran", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7955, - "name": "Saints Row: Gat out of Hell", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 7956, - "name": "Yandere School", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7957, - "name": "South Park: The Stick of Truth", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 7958, - "name": "Troll Tale", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7959, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7960, - "name": "Vikings: Wolves of Midgard", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7961, - "name": "Year Walk", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7962, - "name": "Tomb Raider", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7963, - "name": "Salt and Sanctuary", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7964, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7965, - "name": "ZOMBI", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7966, - "name": "Visage", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7967, - "name": "Samsara Room", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7968, - "name": "Torchlight", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 7969, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7970, - "name": "Saints Row IV", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 7971, - "name": "Zero Escape: Zero Time Dilemma", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7972, - "name": "Trollface Quest", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7973, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7974, - "name": "Wanted: Weapons of Fate", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7975, - "name": "Saw: The Video Game", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 7976, - "name": "Torchlight II", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 7977, - "name": "Ziggurat", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 7978, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7979, - "name": "Trollface Quest 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7980, - "name": "Saints Row: Gat out of Hell", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 7981, - "name": "Warcraft III: Reign of Chaos", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7982, - "name": "Scratches", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 7983, - "name": "Toren", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7984, - "name": "Zombie Driver HD", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7985, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7986, - "name": "Trollface Quest 3", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7987, - "name": "Warcraft III: The Frozen Throne", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7988, - "name": "Zombie Party", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7989, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7990, - "name": "Seasons after Fall", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 7991, - "name": "Salt and Sanctuary", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 7992, - "name": "Tormentum: Dark Sorrow", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 7993, - "name": "Trollface Quest 4", - "site": "ag_ru", - "genres": [] - }, - { - "id": 7994, - "name": "Warhammer 40.000: Space Marine", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 7995, - "name": "Zombie Vikings", - "site": "playground_ru", - "genres": [] - }, - { - "id": 7996, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 7997, - "name": "Samsara Room", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 7998, - "name": "Total Overdose", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 7999, - "name": "Serena", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8000, - "name": "Trollface Quest 5", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8001, - "name": "Zuma Deluxe", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8002, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8003, - "name": "Warhammer: Chaosbane", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "РПГ", - "Приключения" - ] - }, - { - "id": 8004, - "name": "Saw: The Video Game", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8005, - "name": "Transistor", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8006, - "name": "Trollface Quest 6", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8007, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8008, - "name": "Serious Sam 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8009, - "name": "Zuma's Revenge!", - "site": "playground_ru", - "genres": [ - "Логические" - ] - }, - { - "id": 8010, - "name": "We Happy Few", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Приключения" - ] - }, - { - "id": 8011, - "name": "Scratches", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8012, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8013, - "name": "Trine", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 8014, - "name": "Trollface Quest 7", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8015, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8016, - "name": "Whack The Thief", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8017, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8018, - "name": "Serious Sam 3: BFE", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8019, - "name": "Trollface Quest: Video Memes", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8020, - "name": "Trine 2", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 8021, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8022, - "name": "Seasons after Fall", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8023, - "name": "Whack Your Boss", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8024, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8025, - "name": "Twin Sector", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 8026, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8027, - "name": "Trine 3: The Artifacts of Power", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 8028, - "name": "Spooky's House of Jump Scares", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8029, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8030, - "name": "Whack Your Ex", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8031, - "name": "Serena", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8032, - "name": "Two Worlds", - "site": "ag_ru", - "genres": [ - "Ролевые" - ] - }, - { - "id": 8033, - "name": "Troll Tale", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8034, - "name": "Serious Sam Classic: The First Encounter", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8035, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8036, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8037, - "name": "Whack Your Neighbour", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8038, - "name": "Typical Nightmare", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 8039, - "name": "Serious Sam Classic: The Second Encounter", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8040, - "name": "Trollface Quest", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8041, - "name": "Невероятные Приключения Трояна", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8042, - "name": "Whack Your Teacher", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8043, - "name": "Serious Sam 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 8044, - "name": "Star Wars: The Force Unleashed", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 8045, - "name": "Ultimate Epic Battle Simulator", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения", - "Экшены", - "Симуляторы", - "Стратегии" - ] - }, - { - "id": 8046, - "name": "Невероятные Приключения Трояна 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8047, - "name": "Trollface Quest 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8048, - "name": "What Remains of Edith Finch", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8049, - "name": "Shadow Warrior", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8050, - "name": "Невероятные Приключения Трояна 3", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8051, - "name": "Serious Sam 3: BFE", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 8052, - "name": "Ultra Street Fighter IV", - "site": "ag_ru", - "genres": [ - "Экшены", - "Файтинги" - ] - }, - { - "id": 8053, - "name": "Star Wars: The Force Unleashed II", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 8054, - "name": "Trollface Quest 3", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8055, - "name": "Will Rock", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8056, - "name": "Предатор", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8057, - "name": "Undertale", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди" - ] - }, - { - "id": 8058, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8059, - "name": "Willy-Nilly Knight", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8060, - "name": "Shadow of the Tomb Raider", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8061, - "name": "Trollface Quest 4", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8062, - "name": "SteamWorld Dig", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8063, - "name": "Стоп-Гоп", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8064, - "name": "Unfair Mario", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8065, - "name": "Стоп-Гоп 2", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8066, - "name": "Wizard of Legend", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8067, - "name": "Trollface Quest 5", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8068, - "name": "Serious Sam Classic: The First Encounter", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 8069, - "name": "SteamWorld Dig 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8070, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8071, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "playground_ru", - "genres": [] - }, - { - "id": 8072, - "name": "Unmechanical", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Казуальные", - "Инди", - "Приключения" - ] - }, - { - "id": 8073, - "name": "Wolfenstein", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8074, - "name": "Trollface Quest 6", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8075, - "name": "SteamWorld Heist", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 8076, - "name": "Serious Sam Classic: The Second Encounter", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 8077, - "name": "Unmechanical: Extended (DLC)", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения", - "Головоломки" - ] - }, - { - "id": 8078, - "name": "Shantae: Half-Genie Hero", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8079, - "name": "Wolfenstein: The New Order", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8080, - "name": "Trollface Quest 7", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8081, - "name": "Until the last", - "site": "ag_ru", - "genres": [ - "Инди" - ] - }, - { - "id": 8082, - "name": "Shoot Your Nightmare Halloween Special", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8083, - "name": "Wolfenstein: The Old Blood", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 8084, - "name": "Trollface Quest: Video Memes", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8085, - "name": "Steampunk Tower 2", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 8086, - "name": "Shadow Warrior", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8087, - "name": "Shovel Knight: Shovel of Hope", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8088, - "name": "Valiant Hearts: The Great War", - "site": "ag_ru", - "genres": [ - "Головоломки", - "Ролевые", - "Приключения" - ] - }, - { - "id": 8089, - "name": "Woolfe - The Red Hood Diaries", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8090, - "name": "Twin Sector", - "site": "stopgame_ru", - "genres": [ - "logic", - "action" - ] - }, - { - "id": 8091, - "name": "Shadow of the Tomb Raider", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8092, - "name": "Steel Rats", - "site": "gamespot_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 8093, - "name": "Vanish", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 8094, - "name": "Worms 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8095, - "name": "Shrek 2: The Game", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8096, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8097, - "name": "Two Worlds", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8098, - "name": "Stories: The Path of Destinies", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8099, - "name": "X-Blades / Ониблэйд", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8100, - "name": "Victor Vran", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 8101, - "name": "Typical Nightmare", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8102, - "name": "Silence on The Line", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8103, - "name": "Shantae: Half-Genie Hero", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8104, - "name": "Street Fighter X Tekken", - "site": "gamespot_com", - "genres": [ - "Action", - "2D", - "Fighting" - ] - }, - { - "id": 8105, - "name": "Vikings: Wolves of Midgard", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые" - ] - }, - { - "id": 8106, - "name": "X-Morph: Defense", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8107, - "name": "Silent Hill: Alchemilla", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8108, - "name": "Ultimate Epic Battle Simulator", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8110, - "name": "Visage", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы", - "Приключения" - ] - }, - { - "id": 8111, - "name": "Shoot Your Nightmare Halloween Special", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8112, - "name": "Yandere School", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8113, - "name": "Ultra Street Fighter IV", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 8114, - "name": "Silverfall", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8115, - "name": "Wanted: Weapons of Fate", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 8116, - "name": "Stupidella", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8117, - "name": "Shovel Knight: Shovel of Hope", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8118, - "name": "Undertale", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 8119, - "name": "Year Walk", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8120, - "name": "Warcraft III: Reign of Chaos", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8121, - "name": "Silverfall: Earth Awakening", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8122, - "name": "Shrek 2: The Game", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8123, - "name": "Unfair Mario", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8124, - "name": "ZOMBI", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8125, - "name": "Sudeki", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8126, - "name": "Warcraft III: The Frozen Throne", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8127, - "name": "Unmechanical", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 8128, - "name": "Silence on The Line", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8129, - "name": "Singularity", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8130, - "name": "Zero Escape: Zero Time Dilemma", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8131, - "name": "Warhammer 40.000: Space Marine", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 8132, - "name": "Silent Hill: Alchemilla", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8133, - "name": "Unmechanical: Extended (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8134, - "name": "Superhot", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "Tactical", - "Shooter" - ] - }, - { - "id": 8135, - "name": "Warhammer: Chaosbane", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Приключения" - ] - }, - { - "id": 8136, - "name": "Ziggurat", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8137, - "name": "Slap-Man", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8138, - "name": "Surgeon Simulator", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8139, - "name": "Until the last", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8140, - "name": "We Happy Few", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 8141, - "name": "Silverfall", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 8142, - "name": "Zombie Driver HD", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8143, - "name": "Tales from the Borderlands", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8144, - "name": "Slay the Spire", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 8145, - "name": "Whack The Thief", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8146, - "name": "Valiant Hearts: The Great War", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 8147, - "name": "Silverfall: Earth Awakening", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 8148, - "name": "Zombie Party", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8149, - "name": "Tales of Berseria", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8150, - "name": "Whack Your Boss", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8151, - "name": "Sleeping Dogs", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 8152, - "name": "Vanish", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8153, - "name": "Zombie Vikings", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8154, - "name": "Whack Your Ex", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 8155, - "name": "Tales of Zestiria", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8156, - "name": "Singularity", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8157, - "name": "Victor Vran", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 8158, - "name": "Zuma Deluxe", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8159, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8160, - "name": "Slender: The Arrival", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8161, - "name": "Whack Your Neighbour", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8162, - "name": "Slap-Man", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8163, - "name": "Vikings: Wolves of Midgard", - "site": "stopgame_ru", - "genres": [ - "action", - "rpg" - ] - }, - { - "id": 8164, - "name": "Tasty Planet: Back for Seconds", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8165, - "name": "Zuma's Revenge!", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8166, - "name": "Slendytubbies", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8167, - "name": "Whack Your Teacher", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8168, - "name": "Visage", - "site": "stopgame_ru", - "genres": [ - "adventure", - "action" - ] - }, - { - "id": 8169, - "name": "Slay the Spire", - "site": "store_steampowered_com", - "genres": [ - "Strategy", - "Indie" - ] - }, - { - "id": 8170, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8171, - "name": "Tekken 7", - "site": "gamespot_com", - "genres": [ - "VR", - "Action", - "3D", - "Fighting" - ] - }, - { - "id": 8172, - "name": "What Remains of Edith Finch", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 8173, - "name": "Wanted: Weapons of Fate", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8174, - "name": "Slime Rancher", - "site": "mobygames_com", - "genres": [ - "Simulation", - "Role-Playing (RPG)" - ] - }, - { - "id": 8175, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8176, - "name": "Sleeping Dogs", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8177, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8178, - "name": "Warcraft III: Reign of Chaos", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8179, - "name": "Will Rock", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8180, - "name": "Sniper Elite", - "site": "mobygames_com", - "genres": [ - "Action", - "Simulation" - ] - }, - { - "id": 8181, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8182, - "name": "Tesla vs Lovecraft", - "site": "gamespot_com", - "genres": [ - "Shooter", - "Action", - "2D", - "Fixed-Screen" - ] - }, - { - "id": 8183, - "name": "Warcraft III: The Frozen Throne", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8184, - "name": "Slender: The Arrival", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8185, - "name": "Willy-Nilly Knight", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 8186, - "name": "Sniper Elite 3", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8187, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8188, - "name": "Slendytubbies", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8189, - "name": "Wizard of Legend", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 8190, - "name": "Warhammer 40.000: Space Marine", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8191, - "name": "Teslagrad", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 8192, - "name": "Невероятные Приключения Трояна", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8193, - "name": "Sniper Elite V2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8194, - "name": "Warhammer: Chaosbane", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 8195, - "name": "Slime Rancher", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Simulation", - "Indie", - "Adventure" - ] - }, - { - "id": 8196, - "name": "Wolfenstein", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 8197, - "name": "Невероятные Приключения Трояна 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8198, - "name": "The Banner Saga", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 8199, - "name": "Wolfenstein: The New Order", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 8200, - "name": "We Happy Few", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8201, - "name": "Невероятные Приключения Трояна 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8202, - "name": "Sophie's Curse", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8203, - "name": "Sniper Elite", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8204, - "name": "The Banner Saga 2", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 8205, - "name": "Whack The Thief", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8206, - "name": "Wolfenstein: The Old Blood", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 8207, - "name": "Предатор", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8208, - "name": "The Bard’s Tale (2004)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8209, - "name": "South Park: The Fractured But Whole", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8210, - "name": "Whack Your Boss", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8211, - "name": "Woolfe - The Red Hood Diaries", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Приключения" - ] - }, - { - "id": 8212, - "name": "Sniper Elite 3", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8213, - "name": "Стоп-Гоп", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8214, - "name": "The Beginner's Guide", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8215, - "name": "Worms 2", - "site": "ag_ru", - "genres": [ - "Файтинги", - "Стратегии" - ] - }, - { - "id": 8216, - "name": "Whack Your Ex", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8217, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8218, - "name": "Стоп-Гоп 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8219, - "name": "Sniper Elite V2", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8220, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8221, - "name": "X-Blades / Ониблэйд", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8222, - "name": "Whack Your Neighbour", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8223, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 8224, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8225, - "name": "X-Morph: Defense", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Инди", - "Экшены", - "Стратегии" - ] - }, - { - "id": 8226, - "name": "Sophie's Curse", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Simulation", - "Indie" - ] - }, - { - "id": 8227, - "name": "Whack Your Teacher", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8228, - "name": "The Binding of Isaac: Rebirth", - "site": "gamespot_com", - "genres": [ - "Action", - "Roguelike", - "3D", - "Adventure" - ] - }, - { - "id": 8229, - "name": "Yandere School", - "site": "ag_ru", - "genres": [ - "Экшены", - "Инди", - "Симуляторы" - ] - }, - { - "id": 8230, - "name": "What Remains of Edith Finch", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 8231, - "name": "South Park: The Stick of Truth", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8232, - "name": "The Bunker", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8233, - "name": "South Park: The Fractured But Whole", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 8234, - "name": "Year Walk", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 8235, - "name": "Will Rock", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8236, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8237, - "name": "The Cat Lady", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8238, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition", - "Adventure" - ] - }, - { - "id": 8239, - "name": "Willy-Nilly Knight", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 8240, - "name": "ZOMBI", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 8241, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8242, - "name": "Zero Escape: Zero Time Dilemma", - "site": "ag_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 8243, - "name": "Wizard of Legend", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 8244, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition", - "Adventure" - ] - }, - { - "id": 8245, - "name": "The Council", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8246, - "name": "Ziggurat", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Ролевые", - "Инди", - "Экшены" - ] - }, - { - "id": 8247, - "name": "South Park: The Stick of Truth", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Adventure" - ] - }, - { - "id": 8248, - "name": "Wolfenstein", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8249, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8250, - "name": "Zombie Driver HD", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Гонки", - "Инди", - "Экшены" - ] - }, - { - "id": 8251, - "name": "Wolfenstein: The New Order", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8252, - "name": "The Curse of Blackwater", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 8253, - "name": "Zombie Party", - "site": "ag_ru", - "genres": [ - "Экшены", - "Ролевые", - "Инди", - "Приключения" - ] - }, - { - "id": 8254, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 8255, - "name": "Wolfenstein: The Old Blood", - "site": "stopgame_ru", - "genres": [ - "add-on", - "action" - ] - }, - { - "id": 8256, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8257, - "name": "Zombie Vikings", - "site": "ag_ru", - "genres": [ - "Экшены", - "Приключения" - ] - }, - { - "id": 8258, - "name": "Woolfe - The Red Hood Diaries", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8259, - "name": "The Darkness II", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8260, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 8261, - "name": "Zuma Deluxe", - "site": "ag_ru", - "genres": [ - "Казуальные" - ] - }, - { - "id": 8262, - "name": "Worms 2", - "site": "stopgame_ru", - "genres": [ - "strategy", - "logic" - ] - }, - { - "id": 8263, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8264, - "name": "Zuma's Revenge!", - "site": "ag_ru", - "genres": [ - "Экшены", - "Казуальные", - "Головоломки" - ] - }, - { - "id": 8265, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8266, - "name": "The Elder Scrolls IV: Oblivion", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 8267, - "name": "X-Blades / Ониблэйд", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8268, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8269, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8270, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8271, - "name": "X-Morph: Defense", - "site": "stopgame_ru", - "genres": [ - "strategy" - ] - }, - { - "id": 8272, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8273, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8274, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 8275, - "name": "Yandere School", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8276, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8277, - "name": "The Elder Scrolls V: Skyrim", - "site": "gamespot_com", - "genres": [ - "VR", - "Role-Playing" - ] - }, - { - "id": 8278, - "name": "Year Walk", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 8279, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8280, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 8281, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8282, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition", - "Adventure" - ] - }, - { - "id": 8283, - "name": "Невероятные Приключения Трояна", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8284, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8285, - "name": "ZOMBI", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8286, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8287, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8288, - "name": "Невероятные Приключения Трояна 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8289, - "name": "Zero Escape: Zero Time Dilemma", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8290, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8291, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Puzzle", - "Adventure" - ] - }, - { - "id": 8292, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8293, - "name": "Невероятные Приключения Трояна 3", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8294, - "name": "Ziggurat", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 8295, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8296, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8297, - "name": "Предатор", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8298, - "name": "The Evil Within", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Adventure" - ] - }, - { - "id": 8299, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8300, - "name": "Zombie Driver HD", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 8301, - "name": "Стоп-Гоп", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8302, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8303, - "name": "The Evil Within 2", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 8304, - "name": "Zombie Party", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8305, - "name": "Стоп-Гоп 2", - "site": "ag_ru", - "genres": [] - }, - { - "id": 8306, - "name": "The Evil Within: The Assignment (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8307, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Adventure" - ] - }, - { - "id": 8308, - "name": "Zombie Vikings", - "site": "stopgame_ru", - "genres": [ - "arcade" - ] - }, - { - "id": 8309, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8310, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "ag_ru", - "genres": [ - "Казуальные", - "Инди", - "Симуляторы" - ] - }, - { - "id": 8311, - "name": "The Evil Within: The Consequence (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8312, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8313, - "name": "Zuma Deluxe", - "site": "stopgame_ru", - "genres": [ - "logic", - "arcade" - ] - }, - { - "id": 8314, - "name": "The Evil Within: The Executioner (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8315, - "name": "Spooky's House of Jump Scares", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8316, - "name": "Zuma's Revenge!", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8317, - "name": "The Expendabros", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8318, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8319, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8320, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8321, - "name": "The Final Station", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8322, - "name": "Star Wars: The Force Unleashed", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8323, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8324, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "mobygames_com", - "genres": [ - "Special edition", - "Puzzle", - "Adventure" - ] - }, - { - "id": 8325, - "name": "The First Tree", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "3D", - "Adventure" - ] - }, - { - "id": 8326, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8327, - "name": "Star Wars: The Force Unleashed II", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8328, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8329, - "name": "The Forest", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 8330, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8331, - "name": "SteamWorld Dig", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8332, - "name": "Невероятные Приключения Трояна", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8333, - "name": "The Hat Man Shadow Ward", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 8334, - "name": "Невероятные Приключения Трояна 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8335, - "name": "The Incredible Adventures of Van Helsing", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8336, - "name": "SteamWorld Dig 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8337, - "name": "Spooky's House of Jump Scares", - "site": "mobygames_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8338, - "name": "Невероятные Приключения Трояна 3", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8339, - "name": "The Incredible Adventures of Van Helsing II", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8340, - "name": "SteamWorld Heist", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy", - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8341, - "name": "Предатор", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8342, - "name": "The Incredible Adventures of Van Helsing III", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8343, - "name": "Стоп-Гоп", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8344, - "name": "Steampunk Tower 2", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie" - ] - }, - { - "id": 8345, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8346, - "name": "The Last Remnant", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 8347, - "name": "Стоп-Гоп 2", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8348, - "name": "The Mask Man", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8349, - "name": "Star Wars: The Force Unleashed", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8350, - "name": "Steel Rats", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Racing" - ] - }, - { - "id": 8351, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 8352, - "name": "The Punisher (2005)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8353, - "name": "Star Wars: The Force Unleashed II", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8354, - "name": "The Secret of Monkey Island: Special Edition", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8355, - "name": "Stories: The Path of Destinies", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8356, - "name": "SteamWorld Dig", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8357, - "name": "The Slippery Slope", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8358, - "name": "Street Fighter X Tekken", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8359, - "name": "SteamWorld Dig 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8361, - "name": "SteamWorld Heist", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 8362, - "name": "The Stanley Parable", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 8363, - "name": "Stupidella", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8364, - "name": "The Walking Dead: A New Frontier", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8365, - "name": "Steampunk Tower 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8366, - "name": "The Walking Dead: Michonne", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8367, - "name": "Sudeki", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Adventure" - ] - }, - { - "id": 8368, - "name": "Steel Rats", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8369, - "name": "The Walking Dead: Season One", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8370, - "name": "The Walking Dead: Season Two", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8371, - "name": "Stories: The Path of Destinies", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 8372, - "name": "Superhot", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 8373, - "name": "The Witcher", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8374, - "name": "Surgeon Simulator", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Simulation", - "Indie" - ] - }, - { - "id": 8375, - "name": "Street Fighter X Tekken", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8376, - "name": "The Witcher 2: Assassins of Kings", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8378, - "name": "Tales from the Borderlands", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8379, - "name": "The Witcher 3: Wild Hunt", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8380, - "name": "Stupidella", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8381, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8382, - "name": "Tales of Berseria", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 8383, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8384, - "name": "Sudeki", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8385, - "name": "Tales of Zestiria", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 8386, - "name": "The Wolf Among Us", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8387, - "name": "Superhot", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8388, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8389, - "name": "Thief", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8390, - "name": "Surgeon Simulator", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8391, - "name": "This Is The Police", - "site": "gamespot_com", - "genres": [ - "Simulation" - ] - }, - { - "id": 8392, - "name": "Tasty Planet: Back for Seconds", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Casual", - "Adventure" - ] - }, - { - "id": 8393, - "name": "Tales from the Borderlands", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Adventure" - ] - }, - { - "id": 8394, - "name": "TimeShift", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8395, - "name": "Tekken 7", - "site": "store_steampowered_com", - "genres": [ - "Sports", - "Action" - ] - }, - { - "id": 8396, - "name": "Tales of Berseria", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8397, - "name": "Titan Quest", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8398, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8399, - "name": "Tales of Zestiria", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8400, - "name": "Titan Quest Anniversary Edition", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8401, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8402, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8403, - "name": "Tesla vs Lovecraft", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 8404, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8405, - "name": "Tasty Planet: Back for Seconds", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8406, - "name": "Titan Souls", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 8407, - "name": "Teslagrad", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 8408, - "name": "Titanfall 2", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8409, - "name": "Tekken 7", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8410, - "name": "The Banner Saga", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy", - "Indie" - ] - }, - { - "id": 8411, - "name": "To The Moon", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8412, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8413, - "name": "The Banner Saga 2", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Strategy", - "Indie" - ] - }, - { - "id": 8414, - "name": "Tomb Raider", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8415, - "name": "The Bard’s Tale (2004)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8416, - "name": "Tesla vs Lovecraft", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8417, - "name": "Torchlight", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8418, - "name": "Teslagrad", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8419, - "name": "The Beginner's Guide", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8420, - "name": "Torchlight II", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8421, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8422, - "name": "The Banner Saga", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8423, - "name": "Toren", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8424, - "name": "The Binding of Isaac: Rebirth", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8425, - "name": "The Banner Saga 2", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8426, - "name": "Tormentum: Dark Sorrow", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8427, - "name": "The Bard’s Tale (2004)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8428, - "name": "The Bunker", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8429, - "name": "Total Overdose", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8430, - "name": "The Beginner's Guide", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8431, - "name": "The Cat Lady", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8432, - "name": "Transistor", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8433, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8434, - "name": "The Council", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Adventure" - ] - }, - { - "id": 8435, - "name": "The Binding of Isaac: Rebirth", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8436, - "name": "The Curse of Blackwater", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8437, - "name": "Trine", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 8438, - "name": "The Bunker", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8439, - "name": "Trine 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 8440, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8441, - "name": "The Cat Lady", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8442, - "name": "Trine 3: The Artifacts of Power", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 8443, - "name": "Troll Tale", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8444, - "name": "The Council", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8445, - "name": "The Darkness II", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8446, - "name": "Trollface Quest", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8447, - "name": "The Elder Scrolls IV: Oblivion", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8448, - "name": "The Curse of Blackwater", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8449, - "name": "Trollface Quest 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8450, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8451, - "name": "Trollface Quest 3", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8452, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8453, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8454, - "name": "Trollface Quest 4", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8455, - "name": "Trollface Quest 5", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8456, - "name": "The Darkness II", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8457, - "name": "The Elder Scrolls V: Skyrim", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8458, - "name": "Trollface Quest 6", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8459, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8460, - "name": "Trollface Quest 7", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8461, - "name": "The Elder Scrolls IV: Oblivion", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8462, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8463, - "name": "Trollface Quest: Video Memes", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8464, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8465, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8466, - "name": "Twin Sector", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8467, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8468, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8469, - "name": "Two Worlds", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 8470, - "name": "The Elder Scrolls V: Skyrim", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8471, - "name": "The Evil Within", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8472, - "name": "Typical Nightmare", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 8473, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8474, - "name": "The Evil Within 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8475, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8476, - "name": "Ultimate Epic Battle Simulator", - "site": "gamespot_com", - "genres": [ - "Strategy" - ] - }, - { - "id": 8477, - "name": "The Evil Within: The Assignment (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8478, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8479, - "name": "The Evil Within: The Consequence (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8480, - "name": "Ultra Street Fighter IV", - "site": "gamespot_com", - "genres": [ - "Action", - "Fighting", - "2D" - ] - }, - { - "id": 8481, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8482, - "name": "The Evil Within: The Executioner (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8483, - "name": "The Evil Within", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8484, - "name": "Undertale", - "site": "gamespot_com", - "genres": [ - "Role-Playing" - ] - }, - { - "id": 8485, - "name": "The Expendabros", - "site": "store_steampowered_com", - "genres": [ - "Free to Play", - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8486, - "name": "The Evil Within 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8487, - "name": "Unfair Mario", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 8488, - "name": "The Final Station", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8489, - "name": "The Evil Within: The Assignment (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8490, - "name": "The First Tree", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8491, - "name": "The Evil Within: The Consequence (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8492, - "name": "Unmechanical", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 8493, - "name": "Unmechanical: Extended (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8494, - "name": "The Forest", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Simulation", - "Indie", - "Adventure" - ] - }, - { - "id": 8495, - "name": "The Evil Within: The Executioner (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8496, - "name": "Until the last", - "site": "gamespot_com", - "genres": [ - "Action", - "Survival", - "3D", - "Adventure" - ] - }, - { - "id": 8497, - "name": "The Hat Man Shadow Ward", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8498, - "name": "The Expendabros", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8499, - "name": "Valiant Hearts: The Great War", - "site": "gamespot_com", - "genres": [ - "Action", - "Platformer", - "2D" - ] - }, - { - "id": 8500, - "name": "The Incredible Adventures of Van Helsing", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8501, - "name": "The Final Station", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8502, - "name": "The Incredible Adventures of Van Helsing II", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8503, - "name": "Vanish", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 8504, - "name": "The First Tree", - "site": "mobygames_com", - "genres": [ - "Simulation", - "Adventure" - ] - }, - { - "id": 8505, - "name": "The Incredible Adventures of Van Helsing III", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8506, - "name": "The Forest", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8507, - "name": "Victor Vran", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8508, - "name": "The Last Remnant", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8509, - "name": "The Hat Man Shadow Ward", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8510, - "name": "Vikings: Wolves of Midgard", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8511, - "name": "The Mask Man", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8512, - "name": "The Punisher (2005)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8513, - "name": "The Incredible Adventures of Van Helsing", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 8514, - "name": "Visage", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 8515, - "name": "The Incredible Adventures of Van Helsing II", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 8516, - "name": "The Secret of Monkey Island: Special Edition", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8517, - "name": "Wanted: Weapons of Fate", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 8518, - "name": "The Slippery Slope", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8519, - "name": "The Incredible Adventures of Van Helsing III", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 8520, - "name": "Warcraft III: Reign of Chaos", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 8521, - "name": "The Stanley Parable", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8522, - "name": "Warcraft III: The Frozen Throne", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8523, - "name": "The Last Remnant", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8524, - "name": "The Walking Dead: A New Frontier", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8525, - "name": "Warhammer 40.000: Space Marine", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 8526, - "name": "The Walking Dead: Michonne", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8527, - "name": "The Mask Man", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8528, - "name": "Warhammer: Chaosbane", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8529, - "name": "The Walking Dead: Season One", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8530, - "name": "The Punisher (2005)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8531, - "name": "The Walking Dead: Season Two", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8532, - "name": "We Happy Few", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8533, - "name": "The Secret of Monkey Island: Special Edition", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8534, - "name": "Whack The Thief", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8535, - "name": "The Witcher", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8536, - "name": "Whack Your Boss", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8537, - "name": "The Slippery Slope", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8538, - "name": "The Witcher 2: Assassins of Kings", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8539, - "name": "Whack Your Ex", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8540, - "name": "The Stanley Parable", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8541, - "name": "Whack Your Neighbour", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8542, - "name": "The Witcher 3: Wild Hunt", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 8543, - "name": "Whack Your Teacher", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8544, - "name": "The Walking Dead: A New Frontier", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Adventure" - ] - }, - { - "id": 8545, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8546, - "name": "What Remains of Edith Finch", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 8547, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8548, - "name": "The Walking Dead: Michonne", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Adventure" - ] - }, - { - "id": 8549, - "name": "Will Rock", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8550, - "name": "The Walking Dead: Season One", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8551, - "name": "The Wolf Among Us", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8552, - "name": "The Walking Dead: Season Two", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8553, - "name": "Willy-Nilly Knight", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 8554, - "name": "Thief", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8555, - "name": "The Witcher", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8556, - "name": "Wizard of Legend", - "site": "gamespot_com", - "genres": [ - "Action", - "Role-Playing" - ] - }, - { - "id": 8557, - "name": "This Is The Police", - "site": "store_steampowered_com", - "genres": [ - "Strategy", - "Indie", - "Adventure" - ] - }, - { - "id": 8558, - "name": "The Witcher 2: Assassins of Kings", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8559, - "name": "Wolfenstein", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8560, - "name": "TimeShift", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8561, - "name": "Wolfenstein: The New Order", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8562, - "name": "The Witcher 3: Wild Hunt", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8563, - "name": "Titan Quest", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8564, - "name": "Wolfenstein: The Old Blood", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8565, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8566, - "name": "Titan Quest Anniversary Edition", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 8567, - "name": "Woolfe - The Red Hood Diaries", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "Survival", - "Adventure" - ] - }, - { - "id": 8568, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8569, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8570, - "name": "Worms 2", - "site": "gamespot_com", - "genres": [ - "Turn-Based", - "Strategy" - ] - }, - { - "id": 8571, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8572, - "name": "The Wolf Among Us", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Adventure" - ] - }, - { - "id": 8573, - "name": "X-Blades / Ониблэйд", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8574, - "name": "X-Morph: Defense", - "site": "gamespot_com", - "genres": [ - "Strategy", - "Real-Time" - ] - }, - { - "id": 8575, - "name": "Titan Souls", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8576, - "name": "Thief", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8577, - "name": "Titanfall 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8578, - "name": "Yandere School", - "site": "gamespot_com", - "genres": [ - "Third-Person", - "3D", - "Adventure" - ] - }, - { - "id": 8579, - "name": "This Is The Police", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics", - "Simulation", - "Adventure" - ] - }, - { - "id": 8580, - "name": "To The Moon", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8581, - "name": "Year Walk", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 8582, - "name": "TimeShift", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8583, - "name": "ZOMBI", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8584, - "name": "Tomb Raider", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8585, - "name": "Titan Quest", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8586, - "name": "Zero Escape: Zero Time Dilemma", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8587, - "name": "Torchlight", - "site": "store_steampowered_com", - "genres": [ - "RPG" - ] - }, - { - "id": 8588, - "name": "Titan Quest Anniversary Edition", - "site": "mobygames_com", - "genres": [ - "Compilation", - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 8589, - "name": "Ziggurat", - "site": "gamespot_com", - "genres": [ - "Action", - "First-Person", - "3D", - "Shooter" - ] - }, - { - "id": 8590, - "name": "Torchlight II", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8591, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8592, - "name": "Zombie Driver HD", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 8593, - "name": "Toren", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8594, - "name": "Zombie Party", - "site": "gamespot_com", - "genres": [ - "Shooter", - "Action", - "2D", - "Fixed-Screen" - ] - }, - { - "id": 8595, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8596, - "name": "Tormentum: Dark Sorrow", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 8597, - "name": "Zombie Vikings", - "site": "gamespot_com", - "genres": [ - "Beat-'Em-Up", - "Action", - "2D" - ] - }, - { - "id": 8598, - "name": "Total Overdose", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8599, - "name": "Titan Souls", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8600, - "name": "Zuma Deluxe", - "site": "gamespot_com", - "genres": [ - "Matching/Stacking", - "Puzzle" - ] - }, - { - "id": 8601, - "name": "Transistor", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 8602, - "name": "Titanfall 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8603, - "name": "Zuma's Revenge!", - "site": "gamespot_com", - "genres": [ - "Action", - "Puzzle" - ] - }, - { - "id": 8604, - "name": "Trine", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8605, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8606, - "name": "To The Moon", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8607, - "name": "Trine 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8608, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8609, - "name": "Tomb Raider", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8610, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8611, - "name": "Trine 3: The Artifacts of Power", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8612, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8613, - "name": "Torchlight", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8614, - "name": "Troll Tale", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8615, - "name": "Невероятные Приключения Трояна", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8616, - "name": "Torchlight II", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8617, - "name": "Невероятные Приключения Трояна 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8618, - "name": "Trollface Quest", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8619, - "name": "Невероятные Приключения Трояна 3", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8620, - "name": "Toren", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8621, - "name": "Trollface Quest 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8622, - "name": "Предатор", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8623, - "name": "Tormentum: Dark Sorrow", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8624, - "name": "Trollface Quest 3", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8625, - "name": "Стоп-Гоп", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8626, - "name": "Total Overdose", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8627, - "name": "Trollface Quest 4", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8628, - "name": "Стоп-Гоп 2", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8629, - "name": "Trollface Quest 5", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8630, - "name": "Transistor", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8631, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 8632, - "name": "Trollface Quest 6", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8633, - "name": "Trine", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8634, - "name": "Trollface Quest 7", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8635, - "name": "Trine 2", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8636, - "name": "Trollface Quest: Video Memes", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8637, - "name": "Trine 3: The Artifacts of Power", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8638, - "name": "Twin Sector", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8639, - "name": "Troll Tale", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8640, - "name": "Two Worlds", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8641, - "name": "Trollface Quest", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8642, - "name": "Typical Nightmare", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Simulation", - "Indie", - "Adventure" - ] - }, - { - "id": 8643, - "name": "Trollface Quest 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8644, - "name": "Trollface Quest 3", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8645, - "name": "Ultimate Epic Battle Simulator", - "site": "store_steampowered_com", - "genres": [ - "Strategy", - "Action", - "Indie", - "Adventure", - "Simulation" - ] - }, - { - "id": 8646, - "name": "Trollface Quest 4", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8647, - "name": "Trollface Quest 5", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8648, - "name": "Ultra Street Fighter IV", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8649, - "name": "Trollface Quest 6", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8650, - "name": "Undertale", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Indie" - ] - }, - { - "id": 8651, - "name": "Trollface Quest 7", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8652, - "name": "Unfair Mario", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8653, - "name": "Trollface Quest: Video Memes", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8654, - "name": "Unmechanical", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 8655, - "name": "Twin Sector", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8656, - "name": "Unmechanical: Extended (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8657, - "name": "Two Worlds", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8658, - "name": "Until the last", - "site": "store_steampowered_com", - "genres": [ - "Indie" - ] - }, - { - "id": 8659, - "name": "Typical Nightmare", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8660, - "name": "Valiant Hearts: The Great War", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8661, - "name": "Ultimate Epic Battle Simulator", - "site": "mobygames_com", - "genres": [ - "Action", - "Simulation", - "Strategy / tactics" - ] - }, - { - "id": 8662, - "name": "Vanish", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8663, - "name": "Ultra Street Fighter IV", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8664, - "name": "Victor Vran", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8665, - "name": "Undertale", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 8666, - "name": "Vikings: Wolves of Midgard", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 8667, - "name": "Unfair Mario", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8668, - "name": "Visage", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure", - "Simulation", - "Early Access" - ] - }, - { - "id": 8669, - "name": "Unmechanical", - "site": "mobygames_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 8670, - "name": "Wanted: Weapons of Fate", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8671, - "name": "Unmechanical: Extended (DLC)", - "site": "mobygames_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 8672, - "name": "Warcraft III: Reign of Chaos", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8673, - "name": "Warcraft III: The Frozen Throne", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8674, - "name": "Until the last", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8675, - "name": "Warhammer 40.000: Space Marine", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8676, - "name": "Valiant Hearts: The Great War", - "site": "mobygames_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8677, - "name": "Vanish", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8678, - "name": "Warhammer: Chaosbane", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Adventure" - ] - }, - { - "id": 8679, - "name": "Victor Vran", - "site": "mobygames_com", - "genres": [ - "Role-Playing (RPG)" - ] - }, - { - "id": 8680, - "name": "We Happy Few", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8681, - "name": "Vikings: Wolves of Midgard", - "site": "mobygames_com", - "genres": [ - "Action", - "Role-Playing (RPG)" - ] - }, - { - "id": 8682, - "name": "Whack The Thief", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8683, - "name": "Visage", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8684, - "name": "Whack Your Boss", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8685, - "name": "Wanted: Weapons of Fate", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8686, - "name": "Whack Your Ex", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8687, - "name": "Whack Your Neighbour", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8688, - "name": "Warcraft III: Reign of Chaos", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 8689, - "name": "Whack Your Teacher", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8690, - "name": "Warcraft III: The Frozen Throne", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 8691, - "name": "What Remains of Edith Finch", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8692, - "name": "Warhammer 40.000: Space Marine", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8693, - "name": "Will Rock", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8694, - "name": "Warhammer: Chaosbane", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8695, - "name": "Willy-Nilly Knight", - "site": "store_steampowered_com", - "genres": [ - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8696, - "name": "We Happy Few", - "site": "mobygames_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8697, - "name": "Wizard of Legend", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8698, - "name": "Whack The Thief", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8699, - "name": "Whack Your Boss", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8700, - "name": "Wolfenstein", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8701, - "name": "Whack Your Ex", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8702, - "name": "Wolfenstein: The New Order", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8703, - "name": "Whack Your Neighbour", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8704, - "name": "Whack Your Teacher", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8705, - "name": "Wolfenstein: The Old Blood", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 8706, - "name": "What Remains of Edith Finch", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8707, - "name": "Woolfe - The Red Hood Diaries", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie", - "Adventure" - ] - }, - { - "id": 8708, - "name": "Will Rock", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8709, - "name": "Worms 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8710, - "name": "Willy-Nilly Knight", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8711, - "name": "X-Blades / Ониблэйд", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8712, - "name": "Wizard of Legend", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8713, - "name": "X-Morph: Defense", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Strategy", - "Indie" - ] - }, - { - "id": 8714, - "name": "Wolfenstein", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8715, - "name": "Yandere School", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Indie" - ] - }, - { - "id": 8716, - "name": "Wolfenstein: The New Order", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8717, - "name": "Year Walk", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 8718, - "name": "Wolfenstein: The Old Blood", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8719, - "name": "ZOMBI", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8720, - "name": "Woolfe - The Red Hood Diaries", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8721, - "name": "Worms 2", - "site": "mobygames_com", - "genres": [ - "Strategy / tactics" - ] - }, - { - "id": 8722, - "name": "Zero Escape: Zero Time Dilemma", - "site": "store_steampowered_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8723, - "name": "X-Blades / Ониблэйд", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8724, - "name": "Ziggurat", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie" - ] - }, - { - "id": 8725, - "name": "X-Morph: Defense", - "site": "mobygames_com", - "genres": [ - "Action", - "Strategy / tactics" - ] - }, - { - "id": 8726, - "name": "Yandere School", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8727, - "name": "Zombie Driver HD", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Racing", - "Indie" - ] - }, - { - "id": 8728, - "name": "Year Walk", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8729, - "name": "Zombie Party", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Indie", - "Adventure" - ] - }, - { - "id": 8730, - "name": "ZOMBI", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8731, - "name": "Zombie Vikings", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8732, - "name": "Zero Escape: Zero Time Dilemma", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8733, - "name": "Zuma Deluxe", - "site": "store_steampowered_com", - "genres": [ - "Casual" - ] - }, - { - "id": 8734, - "name": "Ziggurat", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8735, - "name": "Zuma's Revenge!", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Casual" - ] - }, - { - "id": 8736, - "name": "Zombie Driver HD", - "site": "mobygames_com", - "genres": [ - "Action", - "Racing / driving" - ] - }, - { - "id": 8737, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8738, - "name": "Zombie Party", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 8739, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8740, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8741, - "name": "Zombie Vikings", - "site": "mobygames_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 8742, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8743, - "name": "Zuma Deluxe", - "site": "mobygames_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 8744, - "name": "Невероятные Приключения Трояна", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8745, - "name": "Zuma's Revenge!", - "site": "mobygames_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 8746, - "name": "Невероятные Приключения Трояна 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8747, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8748, - "name": "Невероятные Приключения Трояна 3", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8749, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8750, - "name": "Предатор", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8751, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8752, - "name": "Стоп-Гоп", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8753, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8754, - "name": "Стоп-Гоп 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 8755, - "name": "Невероятные Приключения Трояна", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8756, - "name": "Невероятные Приключения Трояна 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8757, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Simulation", - "Indie" - ] - }, - { - "id": 8758, - "name": "Невероятные Приключения Трояна 3", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8759, - "name": "Предатор", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8760, - "name": "Стоп-Гоп", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8761, - "name": "Стоп-Гоп 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8762, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 8763, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8764, - "name": "BLACK ROSE", - "site": "metacritic_com", - "genres": [ - "3D", - "First-Person", - "Adventure" - ] - }, - { - "id": 8765, - "name": "Bad Dream: Bridge", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8766, - "name": "Bad Dream: Butcher", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8767, - "name": "Bad Dream: Coma", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "General", - "Adventure" - ] - }, - { - "id": 8768, - "name": "Bad Dream: Cyclops", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8769, - "name": "Bad Dream: Graveyard", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8770, - "name": "Bad Dream: Hospital", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8771, - "name": "Bad Dream: Memories", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8772, - "name": "Baldur's Gate", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "PC-style RPG", - "Western-Style" - ] - }, - { - "id": 8773, - "name": "Bastion", - "site": "metacritic_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 8774, - "name": "Batman: The Telltale Series", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 8775, - "name": "Battlefield 1", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Tactical", - "General", - "Action" - ] - }, - { - "id": 8776, - "name": "Battlefield 1942", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Historic", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 8777, - "name": "Battlefield 4", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Modern", - "Arcade", - "Action" - ] - }, - { - "id": 8778, - "name": "Battlefield Hardline", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Modern", - "Tactical", - "Arcade", - "Action" - ] - }, - { - "id": 8779, - "name": "Battlefield V", - "site": "metacritic_com", - "genres": [ - "Action", - "Shooter", - "Tactical", - "First-Person" - ] - }, - { - "id": 8780, - "name": "Battlefield Vietnam", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Historic", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 8781, - "name": "Bayonetta", - "site": "metacritic_com", - "genres": [ - "Linear", - "Action Adventure" - ] - }, - { - "id": 8782, - "name": "Beholder", - "site": "metacritic_com", - "genres": [ - "General", - "Strategy" - ] - }, - { - "id": 8783, - "name": "Beholder (Beta)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8784, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8785, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8786, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8787, - "name": "Ben and Ed", - "site": "metacritic_com", - "genres": [ - "Platformer", - "Action", - "3D" - ] - }, - { - "id": 8788, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8789, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8790, - "name": "Ben and Ed: Blood Party", - "site": "metacritic_com", - "genres": [ - "Platformer", - "Action", - "3D" - ] - }, - { - "id": 8791, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8792, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8793, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8794, - "name": "Besiege", - "site": "metacritic_com", - "genres": [ - "Strategy", - "Real-Time", - "General" - ] - }, - { - "id": 8795, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8796, - "name": "Borderlands: The Pre-Sequel!", - "site": "spong_com", - "genres": [ - "Adventure: Free Roaming", - "Shoot 'Em Up" - ] - }, - { - "id": 8797, - "name": "Beyond Good and Evil", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8798, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8799, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8800, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8801, - "name": "Brink", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8802, - "name": "Binary Domain", - "site": "metacritic_com", - "genres": [ - "Tactical", - "General", - "Action Adventure", - "Third-Person", - "Action", - "Sci-Fi", - "Shooter" - ] - }, - { - "id": 8803, - "name": "Broforce", - "site": "spong_com", - "genres": [] - }, - { - "id": 8804, - "name": "Broken Age", - "site": "spong_com", - "genres": [ - "Adventure: Point and Click" - ] - }, - { - "id": 8805, - "name": "Brothers: A Tale Of Two Sons", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8806, - "name": "Brutal Legend", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8807, - "name": "BioShock", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8808, - "name": "Buff Knight Advanced", - "site": "spong_com", - "genres": [] - }, - { - "id": 8809, - "name": "Bulb Boy", - "site": "spong_com", - "genres": [] - }, - { - "id": 8810, - "name": "Bulletstorm", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8811, - "name": "Call of Cthulhu (2018)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8812, - "name": "BioShock 2", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8813, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8814, - "name": "Call of Duty", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Combat Game: Infantry" - ] - }, - { - "id": 8815, - "name": "BioShock Infinite", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8816, - "name": "Call of Duty 2", - "site": "spong_com", - "genres": [ - "Combat Game: Infantry" - ] - }, - { - "id": 8817, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8818, - "name": "Call of Duty 4: Modern Warfare", - "site": "spong_com", - "genres": [] - }, - { - "id": 8819, - "name": "Call of Duty: Advanced Warfare", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8820, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8821, - "name": "Call of Duty: Black Ops", - "site": "spong_com", - "genres": [] - }, - { - "id": 8822, - "name": "Call of Duty: Black Ops II", - "site": "spong_com", - "genres": [] - }, - { - "id": 8823, - "name": "Call of Duty: Black Ops III", - "site": "spong_com", - "genres": [] - }, - { - "id": 8824, - "name": "Call of Duty: Ghosts", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8825, - "name": "Bionic Commando", - "site": "metacritic_com", - "genres": [ - "Adventure", - "General", - "Action Adventure", - "Third-Person", - "Sci-Fi" - ] - }, - { - "id": 8826, - "name": "Call of Duty: Infinite Warfare", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8827, - "name": "Call of Duty: Modern Warfare 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 8828, - "name": "Call of Duty: Modern Warfare 3", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8829, - "name": "Black Mesa", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8830, - "name": "Call of Duty: United Offensive", - "site": "spong_com", - "genres": [ - "Add-on pack", - "Combat Game", - "Shoot 'Em Up" - ] - }, - { - "id": 8831, - "name": "Call of Duty: WWII", - "site": "spong_com", - "genres": [] - }, - { - "id": 8832, - "name": "Call of Duty: World at War", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8833, - "name": "BlackSite: Area 51", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8834, - "name": "Canabalt", - "site": "spong_com", - "genres": [] - }, - { - "id": 8835, - "name": "Castlevania: Lords of Shadow", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8836, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8837, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8838, - "name": "Blades of Time", - "site": "metacritic_com", - "genres": [ - "Action", - "General" - ] - }, - { - "id": 8839, - "name": "Castlevania: Lords of Shadow 2", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8840, - "name": "Cat Mario", - "site": "spong_com", - "genres": [] - }, - { - "id": 8841, - "name": "Cat Quest", - "site": "spong_com", - "genres": [] - }, - { - "id": 8842, - "name": "Child Of Light", - "site": "spong_com", - "genres": [] - }, - { - "id": 8843, - "name": "Blades of Time: Dismal Swamp", - "site": "metacritic_com", - "genres": [ - "Action", - "General" - ] - }, - { - "id": 8844, - "name": "Children of Morta", - "site": "spong_com", - "genres": [] - }, - { - "id": 8845, - "name": "Blood Omen 2: Legacy of Kain", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8846, - "name": "Choice Chamber", - "site": "spong_com", - "genres": [] - }, - { - "id": 8847, - "name": "Claire", - "site": "spong_com", - "genres": [] - }, - { - "id": 8848, - "name": "Clustertruck", - "site": "spong_com", - "genres": [] - }, - { - "id": 8849, - "name": "Cold Fear", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8850, - "name": "Costume Quest", - "site": "spong_com", - "genres": [] - }, - { - "id": 8851, - "name": "BloodRayne", - "site": "metacritic_com", - "genres": [ - "Fantasy", - "Third-Person", - "Action", - "Shooter", - "Arcade" - ] - }, - { - "id": 8852, - "name": "Costume Quest 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 8853, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "spong_com", - "genres": [] - }, - { - "id": 8854, - "name": "Cross of the Dutchman", - "site": "spong_com", - "genres": [] - }, - { - "id": 8855, - "name": "BloodRayne 2", - "site": "metacritic_com", - "genres": [ - "Fantasy", - "Third-Person", - "Action", - "Shooter", - "Arcade" - ] - }, - { - "id": 8856, - "name": "Cry Of Fear", - "site": "spong_com", - "genres": [] - }, - { - "id": 8857, - "name": "Crysis", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8858, - "name": "Crysis 2", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8859, - "name": "Crysis Warhead", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8860, - "name": "BloodRayne: Betrayal", - "site": "metacritic_com", - "genres": [ - "Fantasy", - "Platformer", - "General", - "Action Adventure", - "2D", - "Third-Person", - "Action", - "Shooter" - ] - }, - { - "id": 8861, - "name": "Cube Escape: Arles", - "site": "spong_com", - "genres": [] - }, - { - "id": 8862, - "name": "Cube Escape: Case 23", - "site": "spong_com", - "genres": [] - }, - { - "id": 8863, - "name": "Cube Escape: Harvey's Box", - "site": "spong_com", - "genres": [] - }, - { - "id": 8864, - "name": "Bloodbath kavkaz", - "site": "metacritic_com", - "genres": [ - "Top-Down", - "Shooter", - "Action", - "Shoot-'Em-Up" - ] - }, - { - "id": 8865, - "name": "Cube Escape: Seasons", - "site": "spong_com", - "genres": [] - }, - { - "id": 8866, - "name": "Cube Escape: The Lake", - "site": "spong_com", - "genres": [] - }, - { - "id": 8867, - "name": "Cube Escape: The Mill", - "site": "spong_com", - "genres": [] - }, - { - "id": 8868, - "name": "Bloodline: Линия крови", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8869, - "name": "DOOM (2016)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8870, - "name": "Dark Deception", - "site": "spong_com", - "genres": [] - }, - { - "id": 8871, - "name": "Dark Messiah of Might and Magic", - "site": "spong_com", - "genres": [ - "Strategy", - "Adventure: Role Playing" - ] - }, - { - "id": 8872, - "name": "Dark Sector", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8873, - "name": "Dark Souls II", - "site": "spong_com", - "genres": [] - }, - { - "id": 8874, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "spong_com", - "genres": [ - "Compilation", - "Adventure: Role Playing" - ] - }, - { - "id": 8875, - "name": "Blue Estate", - "site": "metacritic_com", - "genres": [ - "Light Gun", - "Shooter", - "Action", - "Rail" - ] - }, - { - "id": 8876, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8877, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8878, - "name": "Blues and Bullets: Episode 1", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8879, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8880, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8881, - "name": "Dark Souls III", - "site": "spong_com", - "genres": [] - }, - { - "id": 8882, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8883, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8884, - "name": "Dark Souls: Prepare to Die Edition", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 8885, - "name": "Boogeyman", - "site": "metacritic_com", - "genres": [ - "Adventure", - "First-Person", - "Action Adventure", - "3D", - "Survival" - ] - }, - { - "id": 8886, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8887, - "name": "Dark Souls: Remastered", - "site": "spong_com", - "genres": [] - }, - { - "id": 8888, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8889, - "name": "Borderlands", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8890, - "name": "Darksiders", - "site": "spong_com", - "genres": [] - }, - { - "id": 8891, - "name": "Darksiders II", - "site": "spong_com", - "genres": [] - }, - { - "id": 8892, - "name": "Darksiders III", - "site": "spong_com", - "genres": [] - }, - { - "id": 8893, - "name": "Daylight", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8894, - "name": "Dead Island", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8895, - "name": "Dead Island: Riptide", - "site": "spong_com", - "genres": [ - "Adventure: Free Roaming", - "Adventure: Survival Horror" - ] - }, - { - "id": 8896, - "name": "Borderlands 2", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8897, - "name": "Dead Island: Ryder White", - "site": "spong_com", - "genres": [] - }, - { - "id": 8898, - "name": "Dead Rising", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8899, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8900, - "name": "Dead Space", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8901, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8902, - "name": "Dead Space 2", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8903, - "name": "Dead Space 3", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8904, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8905, - "name": "Dead Space 3: Awakened (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8906, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8907, - "name": "Dead by Daylight", - "site": "spong_com", - "genres": [] - }, - { - "id": 8908, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8909, - "name": "Deadlight", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8910, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8911, - "name": "Deadpool", - "site": "spong_com", - "genres": [ - "Beat 'Em Up: Hack and Slash" - ] - }, - { - "id": 8912, - "name": "DeathSpank", - "site": "spong_com", - "genres": [] - }, - { - "id": 8913, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8914, - "name": "DeathSpank: Thongs of Virtue", - "site": "spong_com", - "genres": [] - }, - { - "id": 8915, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8916, - "name": "Deathtrap", - "site": "spong_com", - "genres": [] - }, - { - "id": 8917, - "name": "Demigod", - "site": "spong_com", - "genres": [ - "Strategy", - "Beat 'Em Up", - "Adventure: Role Playing" - ] - }, - { - "id": 8918, - "name": "Descenders", - "site": "spong_com", - "genres": [] - }, - { - "id": 8919, - "name": "Detention", - "site": "spong_com", - "genres": [] - }, - { - "id": 8920, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8921, - "name": "Borderlands: The Pre-Sequel!", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8922, - "name": "Devil May Cry 4", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 8923, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8924, - "name": "Devil May Cry 5", - "site": "spong_com", - "genres": [] - }, - { - "id": 8925, - "name": "Diablo II", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 8926, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8927, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing", - "Add-on pack" - ] - }, - { - "id": 8928, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8929, - "name": "Diablo III", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 8930, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing", - "Add-on pack" - ] - }, - { - "id": 8931, - "name": "Dig or Die", - "site": "spong_com", - "genres": [] - }, - { - "id": 8932, - "name": "Disciples II: Dark Prophecy", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 8933, - "name": "Disciples III: Renaissance", - "site": "spong_com", - "genres": [ - "Strategy: Combat", - "Adventure: Role Playing" - ] - }, - { - "id": 8934, - "name": "Brink", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8935, - "name": "Dishonored", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 8936, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8937, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8938, - "name": "Distraint", - "site": "spong_com", - "genres": [] - }, - { - "id": 8939, - "name": "Disturbed", - "site": "spong_com", - "genres": [] - }, - { - "id": 8940, - "name": "Broforce", - "site": "metacritic_com", - "genres": [ - "Platformer", - "Action", - "2D" - ] - }, - { - "id": 8941, - "name": "DmC: Devil May Cry", - "site": "spong_com", - "genres": [ - "Beat 'Em Up: Hack and Slash", - "Beat 'Em Up" - ] - }, - { - "id": 8942, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8943, - "name": "Doki Doki Literature Club", - "site": "spong_com", - "genres": [] - }, - { - "id": 8944, - "name": "Don't Knock Twice", - "site": "spong_com", - "genres": [] - }, - { - "id": 8945, - "name": "Doom 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 8946, - "name": "Broken Age", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "Point-and-Click", - "Adventure", - "3D" - ] - }, - { - "id": 8947, - "name": "Double Dragon Neon", - "site": "spong_com", - "genres": [] - }, - { - "id": 8948, - "name": "Down Stairs", - "site": "spong_com", - "genres": [] - }, - { - "id": 8949, - "name": "Downfall", - "site": "spong_com", - "genres": [] - }, - { - "id": 8950, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "spong_com", - "genres": [] - }, - { - "id": 8951, - "name": "Brothers: A Tale Of Two Sons", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Fantasy", - "Linear" - ] - }, - { - "id": 8952, - "name": "Dragon Age II", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 8953, - "name": "Dragon Age: Inquisition", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 8954, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8955, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8956, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8957, - "name": "Brutal Legend", - "site": "metacritic_com", - "genres": [ - "Fantasy", - "General", - "Action Adventure", - "Action", - "Open-World" - ] - }, - { - "id": 8958, - "name": "Dragon Age: Origins", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing", - "Combat Game" - ] - }, - { - "id": 8959, - "name": "Dragon Age: Origins - Awakening", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing", - "Add-on pack" - ] - }, - { - "id": 8960, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8961, - "name": "Buff Knight Advanced", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 8962, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8963, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8964, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8965, - "name": "Dragon's Dogma: Dark Arisen", - "site": "spong_com", - "genres": [] - }, - { - "id": 8966, - "name": "Bulb Boy", - "site": "metacritic_com", - "genres": [ - "Adventure", - "Point-and-Click" - ] - }, - { - "id": 8967, - "name": "Draw a Stickman: Episode 1", - "site": "spong_com", - "genres": [] - }, - { - "id": 8968, - "name": "Draw a Stickman: Episode 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 8969, - "name": "DreadOut", - "site": "spong_com", - "genres": [] - }, - { - "id": 8970, - "name": "Dropsy", - "site": "spong_com", - "genres": [] - }, - { - "id": 8971, - "name": "Dude, Stop", - "site": "spong_com", - "genres": [] - }, - { - "id": 8972, - "name": "Duke Nukem Forever", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 8973, - "name": "Dungeon Nightmares", - "site": "spong_com", - "genres": [] - }, - { - "id": 8974, - "name": "Bulletstorm", - "site": "metacritic_com", - "genres": [ - "First-Person", - "General", - "Action", - "Sci-Fi", - "Shooter", - "Arcade" - ] - }, - { - "id": 8975, - "name": "Dying Light", - "site": "spong_com", - "genres": [] - }, - { - "id": 8976, - "name": "Call of Cthulhu (2018)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8977, - "name": "Dying Light: The Following (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8978, - "name": "Emily Wants To Play", - "site": "spong_com", - "genres": [] - }, - { - "id": 8979, - "name": "Escape Dead Island", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 8980, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Survival", - "Horror" - ] - }, - { - "id": 8981, - "name": "Evil Defenders", - "site": "spong_com", - "genres": [] - }, - { - "id": 8982, - "name": "Evoland", - "site": "spong_com", - "genres": [] - }, - { - "id": 8983, - "name": "Call of Duty", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 8984, - "name": "Evoland 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 8985, - "name": "Eyes The Horror Game", - "site": "spong_com", - "genres": [] - }, - { - "id": 8986, - "name": "F.E.A.R.", - "site": "spong_com", - "genres": [ - "Combat Game", - "Shoot 'Em Up" - ] - }, - { - "id": 8987, - "name": "F.E.A.R. 2: Project Origin", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror", - "Shoot 'Em Up" - ] - }, - { - "id": 8988, - "name": "F.E.A.R. 2: Reborn", - "site": "spong_com", - "genres": [] - }, - { - "id": 8989, - "name": "F.E.A.R. 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 8990, - "name": "F.E.A.R. Extraction Point", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Combat Game", - "Add-on pack" - ] - }, - { - "id": 8991, - "name": "Call of Duty 2", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Historic", - "Arcade" - ] - }, - { - "id": 8992, - "name": "F.E.A.R. Perseus Mandate", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror", - "Add-on pack" - ] - }, - { - "id": 8993, - "name": "Fable (2004)", - "site": "spong_com", - "genres": [] - }, - { - "id": 8994, - "name": "Call of Duty 4: Modern Warfare", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Arcade", - "Modern" - ] - }, - { - "id": 8995, - "name": "Fable III", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 8996, - "name": "Fahrenheit", - "site": "spong_com", - "genres": [] - }, - { - "id": 8997, - "name": "Failman", - "site": "spong_com", - "genres": [] - }, - { - "id": 8998, - "name": "Fallout 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 8999, - "name": "Call of Duty: Advanced Warfare", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Arcade", - "Modern" - ] - }, - { - "id": 9000, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9001, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9002, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9003, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9004, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9005, - "name": "Fallout 4", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9006, - "name": "Call of Duty: Black Ops", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Historic", - "Arcade" - ] - }, - { - "id": 9007, - "name": "Fallout: New Vegas", - "site": "spong_com", - "genres": [] - }, - { - "id": 9008, - "name": "Family Trouble", - "site": "spong_com", - "genres": [] - }, - { - "id": 9009, - "name": "Far Cry", - "site": "spong_com", - "genres": [ - "Adventure", - "Shoot 'Em Up" - ] - }, - { - "id": 9010, - "name": "Far Cry 2", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9011, - "name": "Call of Duty: Black Ops II", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Historic", - "Arcade", - "Modern" - ] - }, - { - "id": 9012, - "name": "Far Cry 3: Blood Dragon", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9013, - "name": "FarSky", - "site": "spong_com", - "genres": [] - }, - { - "id": 9014, - "name": "Final Fantasy III (Remake)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9015, - "name": "Final Fantasy IV (Remake)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9016, - "name": "Call of Duty: Black Ops III", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Action", - "Arcade" - ] - }, - { - "id": 9017, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9018, - "name": "Final Fantasy X HD Remaster", - "site": "spong_com", - "genres": [] - }, - { - "id": 9019, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9020, - "name": "Final Fantasy XIII", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9021, - "name": "Final Fantasy XIII-2", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9022, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9023, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9024, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9025, - "name": "Final Fantasy XV", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9026, - "name": "Call of Duty: Ghosts", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Arcade", - "Modern" - ] - }, - { - "id": 9027, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9028, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9029, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9030, - "name": "Call of Duty: Infinite Warfare", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Action", - "Tactical" - ] - }, - { - "id": 9031, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9032, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9033, - "name": "Firewatch", - "site": "spong_com", - "genres": [] - }, - { - "id": 9034, - "name": "First Winter", - "site": "spong_com", - "genres": [] - }, - { - "id": 9035, - "name": "Five Minutes", - "site": "spong_com", - "genres": [] - }, - { - "id": 9036, - "name": "Call of Duty: Modern Warfare 2", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Arcade", - "Modern" - ] - }, - { - "id": 9037, - "name": "Five Nights At Freddy's", - "site": "spong_com", - "genres": [] - }, - { - "id": 9038, - "name": "Five Nights At Freddy's 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9039, - "name": "Five Nights At Freddy's 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 9040, - "name": "Five Nights At Freddy's 4", - "site": "spong_com", - "genres": [] - }, - { - "id": 9041, - "name": "Five Nights at Freddy's 3D", - "site": "spong_com", - "genres": [] - }, - { - "id": 9042, - "name": "Five Nights in Anime v3", - "site": "spong_com", - "genres": [] - }, - { - "id": 9043, - "name": "Fran Bow", - "site": "spong_com", - "genres": [] - }, - { - "id": 9044, - "name": "Call of Duty: Modern Warfare 3", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Arcade", - "Modern" - ] - }, - { - "id": 9045, - "name": "Front Mission Evolved", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9046, - "name": "GUN", - "site": "spong_com", - "genres": [] - }, - { - "id": 9047, - "name": "Gemini Rue", - "site": "spong_com", - "genres": [ - "Adventure: Point and Click" - ] - }, - { - "id": 9048, - "name": "Geometry Dash", - "site": "spong_com", - "genres": [] - }, - { - "id": 9049, - "name": "Ghost of a Tale", - "site": "spong_com", - "genres": [] - }, - { - "id": 9050, - "name": "Call of Duty: United Offensive", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Tactical", - "Third-Person", - "Action", - "Shooter", - "Historic", - "Arcade" - ] - }, - { - "id": 9051, - "name": "Goat Simulator", - "site": "spong_com", - "genres": [] - }, - { - "id": 9052, - "name": "Grand Theft Auto III", - "site": "spong_com", - "genres": [] - }, - { - "id": 9053, - "name": "Grand Theft Auto: San Andreas", - "site": "spong_com", - "genres": [ - "Adventure: Free Roaming", - "Combat Game" - ] - }, - { - "id": 9054, - "name": "Grand Theft Auto: Vice City", - "site": "spong_com", - "genres": [ - "Adventure: Free Roaming", - "Combat Game" - ] - }, - { - "id": 9055, - "name": "Grandpa", - "site": "spong_com", - "genres": [] - }, - { - "id": 9056, - "name": "Call of Duty: WWII", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Action", - "Arcade" - ] - }, - { - "id": 9057, - "name": "Grey", - "site": "spong_com", - "genres": [] - }, - { - "id": 9058, - "name": "Grim Fandango Remastered", - "site": "spong_com", - "genres": [] - }, - { - "id": 9059, - "name": "Grow Home", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9060, - "name": "Grow Up", - "site": "spong_com", - "genres": [] - }, - { - "id": 9061, - "name": "Call of Duty: World at War", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Action", - "Shooter", - "Historic", - "Arcade" - ] - }, - { - "id": 9062, - "name": "Guns, Gore & Cannoli", - "site": "spong_com", - "genres": [] - }, - { - "id": 9063, - "name": "Guns, Gore & Cannoli 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9064, - "name": "Half-Life", - "site": "spong_com", - "genres": [ - "Puzzle", - "Shoot 'Em Up" - ] - }, - { - "id": 9065, - "name": "Half-Life 2", - "site": "spong_com", - "genres": [ - "Adventure", - "Shoot 'Em Up" - ] - }, - { - "id": 9066, - "name": "Half-Life 2: Episode One", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Add-on pack" - ] - }, - { - "id": 9067, - "name": "Canabalt", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action", - "General" - ] - }, - { - "id": 9068, - "name": "Half-Life 2: Episode Two", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9069, - "name": "Half-Life: Blue Shift", - "site": "spong_com", - "genres": [ - "Puzzle", - "Shoot 'Em Up" - ] - }, - { - "id": 9070, - "name": "Half-Life: Opposing Force", - "site": "spong_com", - "genres": [ - "Add-on pack" - ] - }, - { - "id": 9071, - "name": "Half-Life: Paranoia", - "site": "spong_com", - "genres": [] - }, - { - "id": 9072, - "name": "Castlevania: Lords of Shadow", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Fantasy", - "General" - ] - }, - { - "id": 9073, - "name": "Half-Life: The Xeno Project", - "site": "spong_com", - "genres": [] - }, - { - "id": 9074, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9075, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9076, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9077, - "name": "Halo: Combat Evolved", - "site": "spong_com", - "genres": [ - "Adventure", - "Shoot 'Em Up" - ] - }, - { - "id": 9078, - "name": "Hand of Fate", - "site": "spong_com", - "genres": [ - "Strategy: Trading" - ] - }, - { - "id": 9079, - "name": "Hand of Fate 2", - "site": "spong_com", - "genres": [ - "Strategy: Combat" - ] - }, - { - "id": 9080, - "name": "Happy Room", - "site": "spong_com", - "genres": [] - }, - { - "id": 9081, - "name": "Happy Wheels", - "site": "spong_com", - "genres": [] - }, - { - "id": 9082, - "name": "Castlevania: Lords of Shadow 2", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Fantasy", - "General" - ] - }, - { - "id": 9083, - "name": "Hard Reset", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9084, - "name": "Cat Mario", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9085, - "name": "Hard Reset: Exile (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9086, - "name": "Harts", - "site": "spong_com", - "genres": [] - }, - { - "id": 9087, - "name": "Hatred", - "site": "spong_com", - "genres": [] - }, - { - "id": 9088, - "name": "Hellblade: Senua's Sacrifice", - "site": "spong_com", - "genres": [] - }, - { - "id": 9089, - "name": "Hellgate: London", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 9090, - "name": "Her Story", - "site": "spong_com", - "genres": [] - }, - { - "id": 9091, - "name": "Cat Quest", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9092, - "name": "Hero Academy", - "site": "spong_com", - "genres": [] - }, - { - "id": 9093, - "name": "Heroes of Might and Magic III", - "site": "spong_com", - "genres": [ - "Adventure", - "Combat Game" - ] - }, - { - "id": 9094, - "name": "Heroes of Might and Magic IV", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9095, - "name": "Heroes of Might and Magic V", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9096, - "name": "Child Of Light", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action", - "General" - ] - }, - { - "id": 9097, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9098, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "spong_com", - "genres": [ - "Strategy: God game", - "Add-on pack" - ] - }, - { - "id": 9099, - "name": "Homefront", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9100, - "name": "Hotel Remorse", - "site": "spong_com", - "genres": [] - }, - { - "id": 9101, - "name": "Children of Morta", - "site": "metacritic_com", - "genres": [ - "Adventure", - "General" - ] - }, - { - "id": 9102, - "name": "Hotline Miami", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9103, - "name": "Hotline Miami 2: Wrong Number", - "site": "spong_com", - "genres": [] - }, - { - "id": 9104, - "name": "House Flipper", - "site": "spong_com", - "genres": [] - }, - { - "id": 9105, - "name": "Choice Chamber", - "site": "metacritic_com", - "genres": [ - "Action", - "General" - ] - }, - { - "id": 9106, - "name": "I Am Alive", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9107, - "name": "I am Setsuna", - "site": "spong_com", - "genres": [] - }, - { - "id": 9108, - "name": "I hate this game", - "site": "spong_com", - "genres": [] - }, - { - "id": 9109, - "name": "INSIDE", - "site": "spong_com", - "genres": [ - "Platform", - "Puzzle" - ] - }, - { - "id": 9110, - "name": "Claire", - "site": "metacritic_com", - "genres": [ - "Adventure", - "General" - ] - }, - { - "id": 9111, - "name": "Indivisible", - "site": "spong_com", - "genres": [] - }, - { - "id": 9112, - "name": "Injustice: Gods Among Us", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 9113, - "name": "Into the Breach", - "site": "spong_com", - "genres": [] - }, - { - "id": 9114, - "name": "Clustertruck", - "site": "metacritic_com", - "genres": [ - "Platformer", - "Action", - "3D" - ] - }, - { - "id": 9115, - "name": "Jade Empire", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9116, - "name": "Jotun", - "site": "spong_com", - "genres": [] - }, - { - "id": 9117, - "name": "Just Cause", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9118, - "name": "Just Cause 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9119, - "name": "Cold Fear", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Horror" - ] - }, - { - "id": 9120, - "name": "Kane & Lynch 2: Dog Days", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9121, - "name": "Killer is Dead: Nightmare Edition", - "site": "spong_com", - "genres": [] - }, - { - "id": 9122, - "name": "King's Bounty: Armored Princess", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing", - "Add-on pack" - ] - }, - { - "id": 9123, - "name": "King's Bounty: Crossworlds", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing", - "Add-on pack" - ] - }, - { - "id": 9124, - "name": "Costume Quest", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9125, - "name": "King's Bounty: Dark Side", - "site": "spong_com", - "genres": [ - "Strategy: Combat", - "Adventure: Role Playing" - ] - }, - { - "id": 9126, - "name": "King's Bounty: The Legend", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9127, - "name": "King's Bounty: Warriors of the North", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9128, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9129, - "name": "Kingdom Rush", - "site": "spong_com", - "genres": [] - }, - { - "id": 9130, - "name": "Kingdom: Classic", - "site": "spong_com", - "genres": [] - }, - { - "id": 9131, - "name": "Kingdoms of Amalur: Reckoning", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9132, - "name": "Knock-Knock", - "site": "spong_com", - "genres": [] - }, - { - "id": 9133, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "spong_com", - "genres": [] - }, - { - "id": 9134, - "name": "Lara Croft and the Guardian of Light", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9135, - "name": "Lara Croft and the Temple of Osiris", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9136, - "name": "Late Shift", - "site": "spong_com", - "genres": [] - }, - { - "id": 9137, - "name": "Layers of Fear", - "site": "spong_com", - "genres": [] - }, - { - "id": 9138, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9139, - "name": "Left 4 Dead", - "site": "spong_com", - "genres": [] - }, - { - "id": 9140, - "name": "Left 4 Dead 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9141, - "name": "Legend Hand of God", - "site": "spong_com", - "genres": [] - }, - { - "id": 9142, - "name": "Legend of Grimrock", - "site": "spong_com", - "genres": [ - "Adventure", - "Combat Game" - ] - }, - { - "id": 9143, - "name": "Legendary", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9144, - "name": "Life After Us: Shipwrecked", - "site": "spong_com", - "genres": [] - }, - { - "id": 9145, - "name": "Life Goes On: Done to Death", - "site": "spong_com", - "genres": [] - }, - { - "id": 9146, - "name": "Life Is Strange", - "site": "spong_com", - "genres": [] - }, - { - "id": 9147, - "name": "Limbo", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9148, - "name": "Little Nightmares", - "site": "spong_com", - "genres": [] - }, - { - "id": 9149, - "name": "Little Nightmares - The Depths (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9150, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9151, - "name": "Loki: Heroes of Mythology", - "site": "spong_com", - "genres": [] - }, - { - "id": 9152, - "name": "Lost Planet: Colonies", - "site": "spong_com", - "genres": [] - }, - { - "id": 9153, - "name": "Lost: Via Domus", - "site": "spong_com", - "genres": [] - }, - { - "id": 9154, - "name": "Love Is Strange (DEMO)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9155, - "name": "METAL SLUG", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9156, - "name": "METAL SLUG 3", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9157, - "name": "MINERVA: Metastasis", - "site": "spong_com", - "genres": [] - }, - { - "id": 9158, - "name": "MOTHERGUNSHIP", - "site": "spong_com", - "genres": [] - }, - { - "id": 9159, - "name": "Machinarium", - "site": "spong_com", - "genres": [] - }, - { - "id": 9160, - "name": "Magibot", - "site": "spong_com", - "genres": [] - }, - { - "id": 9161, - "name": "Magicka", - "site": "spong_com", - "genres": [ - "Strategy" - ] - }, - { - "id": 9162, - "name": "Magicka 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9163, - "name": "Manual Samuel", - "site": "spong_com", - "genres": [] - }, - { - "id": 9164, - "name": "Mario.EXE", - "site": "spong_com", - "genres": [] - }, - { - "id": 9165, - "name": "Marvel vs. Capcom: Infinite", - "site": "spong_com", - "genres": [] - }, - { - "id": 9166, - "name": "Masochisia", - "site": "spong_com", - "genres": [] - }, - { - "id": 9167, - "name": "Masters of Anima", - "site": "spong_com", - "genres": [] - }, - { - "id": 9168, - "name": "Max Payne", - "site": "spong_com", - "genres": [ - "Adventure", - "Shoot 'Em Up" - ] - }, - { - "id": 9169, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "spong_com", - "genres": [ - "Adventure", - "Shoot 'Em Up" - ] - }, - { - "id": 9170, - "name": "Max Payne 3", - "site": "spong_com", - "genres": [ - "Adventure", - "Shoot 'Em Up" - ] - }, - { - "id": 9171, - "name": "Max: The Curse Of Brotherhood", - "site": "spong_com", - "genres": [] - }, - { - "id": 9172, - "name": "McPixel", - "site": "spong_com", - "genres": [] - }, - { - "id": 9173, - "name": "Metal Gear Rising: Revengeance", - "site": "spong_com", - "genres": [ - "Strategy: Stealth" - ] - }, - { - "id": 9174, - "name": "Metal Wolf Chaos XD", - "site": "spong_com", - "genres": [] - }, - { - "id": 9175, - "name": "Metro 2033", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 9176, - "name": "Metro 2033 Redux", - "site": "spong_com", - "genres": [] - }, - { - "id": 9177, - "name": "Metro Exodus", - "site": "spong_com", - "genres": [] - }, - { - "id": 9178, - "name": "Metro Last Light", - "site": "spong_com", - "genres": [] - }, - { - "id": 9179, - "name": "Metro Last Light Redux", - "site": "spong_com", - "genres": [] - }, - { - "id": 9180, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9181, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9182, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9183, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9184, - "name": "Middle-earth: Shadow of Mordor", - "site": "spong_com", - "genres": [] - }, - { - "id": 9185, - "name": "Might & Magic Heroes VI", - "site": "spong_com", - "genres": [ - "Strategy: Combat", - "Adventure: Role Playing" - ] - }, - { - "id": 9186, - "name": "Minecraft Story Mode", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9187, - "name": "Mini Ninjas", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9188, - "name": "Mirror's Edge", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9189, - "name": "Monst", - "site": "spong_com", - "genres": [] - }, - { - "id": 9190, - "name": "Mortal Kombat IX", - "site": "spong_com", - "genres": [] - }, - { - "id": 9191, - "name": "Mortal Kombat X", - "site": "spong_com", - "genres": [] - }, - { - "id": 9192, - "name": "Mother Russia Bleeds", - "site": "spong_com", - "genres": [] - }, - { - "id": 9193, - "name": "Mount Your Friends", - "site": "spong_com", - "genres": [] - }, - { - "id": 9194, - "name": "Mr. Robot", - "site": "spong_com", - "genres": [] - }, - { - "id": 9195, - "name": "Mr.President!", - "site": "spong_com", - "genres": [] - }, - { - "id": 9196, - "name": "Murdered: Soul Suspect", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9197, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "spong_com", - "genres": [ - "Compilation", - "Beat 'Em Up" - ] - }, - { - "id": 9198, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "spong_com", - "genres": [] - }, - { - "id": 9199, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 9200, - "name": "Costume Quest 2", - "site": "metacritic_com", - "genres": [ - "Japanese-Style", - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 9201, - "name": "Need for Speed: Underground", - "site": "spong_com", - "genres": [ - "Racing" - ] - }, - { - "id": 9202, - "name": "Never Alone", - "site": "spong_com", - "genres": [] - }, - { - "id": 9203, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9204, - "name": "Neverending Nightmares", - "site": "spong_com", - "genres": [] - }, - { - "id": 9205, - "name": "Nevermind", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9206, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9207, - "name": "Cross of the Dutchman", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Fantasy", - "General" - ] - }, - { - "id": 9208, - "name": "Neverwinter Nights", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9209, - "name": "Neverwinter Nights 2", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9210, - "name": "NieR: Automata", - "site": "spong_com", - "genres": [] - }, - { - "id": 9211, - "name": "Nightmare House 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9212, - "name": "Cry Of Fear", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Horror", - "Survival" - ] - }, - { - "id": 9213, - "name": "Nosferatu: The Wrath of Malachi", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9214, - "name": "Nox Timore", - "site": "spong_com", - "genres": [] - }, - { - "id": 9215, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "spong_com", - "genres": [] - }, - { - "id": 9216, - "name": "Omensight", - "site": "spong_com", - "genres": [] - }, - { - "id": 9217, - "name": "Crysis", - "site": "metacritic_com", - "genres": [ - "Shooter", - "Sci-Fi", - "Arcade", - "First-Person", - "Action" - ] - }, - { - "id": 9218, - "name": "One Hand Clapping (DEMO)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9219, - "name": "One Night at Flumpty's", - "site": "spong_com", - "genres": [] - }, - { - "id": 9220, - "name": "One Night at Flumpty's 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9221, - "name": "One Piece: Burning Blood", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 9222, - "name": "Orcs Must Die!", - "site": "spong_com", - "genres": [ - "Strategy: Combat", - "Adventure" - ] - }, - { - "id": 9223, - "name": "Crysis 2", - "site": "metacritic_com", - "genres": [ - "Shooter", - "Sci-Fi", - "Arcade", - "First-Person", - "Action" - ] - }, - { - "id": 9224, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9225, - "name": "Ori and the Blind Forest", - "site": "spong_com", - "genres": [] - }, - { - "id": 9226, - "name": "Outlast", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 9227, - "name": "Crysis Warhead", - "site": "metacritic_com", - "genres": [ - "Shooter", - "Sci-Fi", - "Arcade", - "First-Person", - "Action" - ] - }, - { - "id": 9228, - "name": "Outlast: Whistleblower", - "site": "spong_com", - "genres": [] - }, - { - "id": 9229, - "name": "Overlord", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9230, - "name": "Overlord: Raising Hell", - "site": "spong_com", - "genres": [ - "Add-on pack", - "Adventure: Role Playing" - ] - }, - { - "id": 9231, - "name": "Cube Escape: Arles", - "site": "metacritic_com", - "genres": [ - "Puzzle", - "General" - ] - }, - { - "id": 9232, - "name": "Oxenfree", - "site": "spong_com", - "genres": [] - }, - { - "id": 9233, - "name": "Cube Escape: Case 23", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9234, - "name": "Painkiller", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror", - "Shoot 'Em Up" - ] - }, - { - "id": 9235, - "name": "Painkiller: Battle Out of Hell", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9236, - "name": "Painkiller: Overdose", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9237, - "name": "Cube Escape: Harvey's Box", - "site": "metacritic_com", - "genres": [ - "Puzzle", - "General" - ] - }, - { - "id": 9238, - "name": "Paint the Town Red", - "site": "spong_com", - "genres": [] - }, - { - "id": 9239, - "name": "Panty Party", - "site": "spong_com", - "genres": [] - }, - { - "id": 9240, - "name": "Papers, Please", - "site": "spong_com", - "genres": [] - }, - { - "id": 9241, - "name": "Peace, Death!", - "site": "spong_com", - "genres": [] - }, - { - "id": 9242, - "name": "Cube Escape: Seasons", - "site": "metacritic_com", - "genres": [ - "Puzzle", - "General" - ] - }, - { - "id": 9243, - "name": "Pillars of Eternity", - "site": "spong_com", - "genres": [] - }, - { - "id": 9244, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9245, - "name": "Cube Escape: The Lake", - "site": "metacritic_com", - "genres": [ - "Puzzle", - "General" - ] - }, - { - "id": 9246, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9247, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9248, - "name": "Cube Escape: The Mill", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9249, - "name": "Pinstripe", - "site": "spong_com", - "genres": [] - }, - { - "id": 9250, - "name": "DOOM (2016)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9251, - "name": "Pirates of the Caribbean: At World’s End", - "site": "spong_com", - "genres": [] - }, - { - "id": 9252, - "name": "Pizza Delivery 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9253, - "name": "Plague Inc: Evolved", - "site": "spong_com", - "genres": [] - }, - { - "id": 9254, - "name": "Dark Deception", - "site": "metacritic_com", - "genres": [ - "Action", - "General" - ] - }, - { - "id": 9255, - "name": "Plants vs. Zombies", - "site": "spong_com", - "genres": [ - "Strategy: Combat" - ] - }, - { - "id": 9256, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9257, - "name": "Pony Island", - "site": "spong_com", - "genres": [] - }, - { - "id": 9258, - "name": "Portal", - "site": "spong_com", - "genres": [ - "Puzzle: Physics" - ] - }, - { - "id": 9259, - "name": "Portal 2", - "site": "spong_com", - "genres": [ - "Puzzle: Physics" - ] - }, - { - "id": 9260, - "name": "Prey (2006)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9261, - "name": "Dark Messiah of Might and Magic", - "site": "metacritic_com", - "genres": [ - "Fantasy", - "Shooter", - "Arcade", - "First-Person", - "Action" - ] - }, - { - "id": 9262, - "name": "Prince of Persia (2008)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9263, - "name": "Prince of Persia: The Two Thrones", - "site": "spong_com", - "genres": [ - "Adventure", - "Platform" - ] - }, - { - "id": 9264, - "name": "Prospekt", - "site": "spong_com", - "genres": [] - }, - { - "id": 9265, - "name": "Prototype", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9266, - "name": "Dark Sector", - "site": "metacritic_com", - "genres": [ - "Shooter", - "Sci-Fi", - "Action", - "Third-Person", - "Arcade" - ] - }, - { - "id": 9267, - "name": "Prototype 2", - "site": "spong_com", - "genres": [ - "Adventure: Free Roaming" - ] - }, - { - "id": 9268, - "name": "Psychonauts", - "site": "spong_com", - "genres": [ - "Adventure", - "Platform" - ] - }, - { - "id": 9269, - "name": "PuniTy", - "site": "spong_com", - "genres": [] - }, - { - "id": 9270, - "name": "P·O·L·L·E·N", - "site": "spong_com", - "genres": [] - }, - { - "id": 9271, - "name": "Dark Souls II", - "site": "metacritic_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 9272, - "name": "Quake 4", - "site": "spong_com", - "genres": [] - }, - { - "id": 9273, - "name": "Quantum Break", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9274, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "metacritic_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 9275, - "name": "Raft", - "site": "spong_com", - "genres": [] - }, - { - "id": 9276, - "name": "Rage", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 9277, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9278, - "name": "Rampage Knights", - "site": "spong_com", - "genres": [] - }, - { - "id": 9279, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9280, - "name": "Rayman Legends", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9281, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9282, - "name": "Rayman Origins", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9283, - "name": "Rayman Raving Rabbids", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9284, - "name": "Rayman Raving Rabbids 2", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9285, - "name": "Real Horror Stories", - "site": "spong_com", - "genres": [] - }, - { - "id": 9286, - "name": "Dark Souls III", - "site": "metacritic_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 9287, - "name": "Redeemer", - "site": "spong_com", - "genres": [] - }, - { - "id": 9288, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9289, - "name": "Reign Of Kings", - "site": "spong_com", - "genres": [] - }, - { - "id": 9290, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9291, - "name": "Reigns", - "site": "spong_com", - "genres": [] - }, - { - "id": 9292, - "name": "Reigns: Her Majesty", - "site": "spong_com", - "genres": [] - }, - { - "id": 9293, - "name": "Dark Souls: Prepare to Die Edition", - "site": "metacritic_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 9294, - "name": "Resident Evil", - "site": "spong_com", - "genres": [] - }, - { - "id": 9295, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9296, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "spong_com", - "genres": [] - }, - { - "id": 9297, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "spong_com", - "genres": [] - }, - { - "id": 9298, - "name": "Resident Evil 4", - "site": "spong_com", - "genres": [] - }, - { - "id": 9299, - "name": "Resident Evil 5", - "site": "spong_com", - "genres": [] - }, - { - "id": 9300, - "name": "Dark Souls: Remastered", - "site": "metacritic_com", - "genres": [ - "Action RPG", - "Role-Playing" - ] - }, - { - "id": 9301, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9302, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9303, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9304, - "name": "Resident Evil 6", - "site": "spong_com", - "genres": [] - }, - { - "id": 9305, - "name": "Resident Evil 7: Biohazard", - "site": "spong_com", - "genres": [] - }, - { - "id": 9306, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9307, - "name": "Darksiders", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Modern", - "Fantasy", - "Open-World" - ] - }, - { - "id": 9308, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9309, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9310, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9311, - "name": "Resident Evil: Operation Raccoon City", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror", - "Shoot 'Em Up" - ] - }, - { - "id": 9312, - "name": "Darksiders II", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Modern", - "Fantasy", - "Open-World" - ] - }, - { - "id": 9313, - "name": "Resident Evil: Revelations", - "site": "spong_com", - "genres": [] - }, - { - "id": 9314, - "name": "Resident Evil: Revelations 2", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 9315, - "name": "Reus", - "site": "spong_com", - "genres": [ - "Strategy: God game" - ] - }, - { - "id": 9316, - "name": "Rezrog", - "site": "spong_com", - "genres": [] - }, - { - "id": 9317, - "name": "Darksiders III", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Open-World" - ] - }, - { - "id": 9318, - "name": "Rise of the Tomb Raider", - "site": "spong_com", - "genres": [] - }, - { - "id": 9319, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9320, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9321, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9322, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9323, - "name": "Daylight", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Horror", - "Survival" - ] - }, - { - "id": 9324, - "name": "Risen", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9325, - "name": "Rock of Ages", - "site": "spong_com", - "genres": [] - }, - { - "id": 9326, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "spong_com", - "genres": [] - }, - { - "id": 9327, - "name": "Root Of Evil: The Tailor", - "site": "spong_com", - "genres": [] - }, - { - "id": 9328, - "name": "Runic Rampage", - "site": "spong_com", - "genres": [] - }, - { - "id": 9329, - "name": "Dead Island", - "site": "metacritic_com", - "genres": [ - "Horror", - "Survival", - "Action Adventure" - ] - }, - { - "id": 9330, - "name": "Rust", - "site": "spong_com", - "genres": [] - }, - { - "id": 9331, - "name": "Ryse: Son of Rome", - "site": "spong_com", - "genres": [ - "Strategy: Combat" - ] - }, - { - "id": 9332, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Adventure" - ] - }, - { - "id": 9333, - "name": "Dead Island: Riptide", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9334, - "name": "SCP-087", - "site": "spong_com", - "genres": [] - }, - { - "id": 9335, - "name": "SCP-087-B", - "site": "spong_com", - "genres": [] - }, - { - "id": 9336, - "name": "Dead Island: Ryder White", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 9337, - "name": "SINNER: Sacrifice for Redemption", - "site": "spong_com", - "genres": [] - }, - { - "id": 9338, - "name": "SWAT 4", - "site": "spong_com", - "genres": [ - "Strategy: Combat" - ] - }, - { - "id": 9339, - "name": "Dead Rising", - "site": "metacritic_com", - "genres": [ - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9340, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9341, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9342, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9343, - "name": "Dead Space", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Sci-Fi", - "Third-Person", - "Action" - ] - }, - { - "id": 9344, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9345, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9346, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9347, - "name": "Dead Space 2", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Sci-Fi", - "Third-Person", - "Action" - ] - }, - { - "id": 9348, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9349, - "name": "Sacred", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9350, - "name": "Sacred 3", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9351, - "name": "Dead Space 3", - "site": "metacritic_com", - "genres": [ - "Sci-Fi", - "Shooter", - "Action Adventure", - "Third-Person", - "Linear", - "Action" - ] - }, - { - "id": 9352, - "name": "Sacred Citadel", - "site": "spong_com", - "genres": [] - }, - { - "id": 9353, - "name": "Dead Space 3: Awakened (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9354, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9355, - "name": "Dead by Daylight", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9356, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9357, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9358, - "name": "Saints Row IV", - "site": "spong_com", - "genres": [ - "Adventure: Free Roaming" - ] - }, - { - "id": 9359, - "name": "Deadlight", - "site": "metacritic_com", - "genres": [ - "General", - "Platformer", - "2D", - "Action" - ] - }, - { - "id": 9360, - "name": "Saints Row: Gat out of Hell", - "site": "spong_com", - "genres": [] - }, - { - "id": 9361, - "name": "Salt and Sanctuary", - "site": "spong_com", - "genres": [] - }, - { - "id": 9362, - "name": "Deadpool", - "site": "metacritic_com", - "genres": [ - "Beat-'Em-Up", - "3D", - "Arcade", - "Shooter", - "Action Adventure", - "Third-Person", - "Linear", - "Action" - ] - }, - { - "id": 9363, - "name": "Samsara Room", - "site": "spong_com", - "genres": [] - }, - { - "id": 9364, - "name": "Saw: The Video Game", - "site": "spong_com", - "genres": [] - }, - { - "id": 9365, - "name": "Scratches", - "site": "spong_com", - "genres": [ - "Adventure: Point and Click" - ] - }, - { - "id": 9366, - "name": "DeathSpank", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Action RPG", - "Action" - ] - }, - { - "id": 9367, - "name": "Seasons after Fall", - "site": "spong_com", - "genres": [ - "Platform", - "Adventure" - ] - }, - { - "id": 9368, - "name": "Serena", - "site": "spong_com", - "genres": [] - }, - { - "id": 9369, - "name": "DeathSpank: Thongs of Virtue", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9370, - "name": "Serious Sam 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9371, - "name": "Serious Sam 3: BFE", - "site": "spong_com", - "genres": [] - }, - { - "id": 9372, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9373, - "name": "Deathtrap", - "site": "metacritic_com", - "genres": [ - "General", - "Strategy" - ] - }, - { - "id": 9374, - "name": "Serious Sam Classic: The First Encounter", - "site": "spong_com", - "genres": [] - }, - { - "id": 9375, - "name": "Serious Sam Classic: The Second Encounter", - "site": "spong_com", - "genres": [] - }, - { - "id": 9376, - "name": "Demigod", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "Fantasy", - "MOBA", - "Strategy" - ] - }, - { - "id": 9377, - "name": "Shadow Warrior", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9378, - "name": "Shadow of the Tomb Raider", - "site": "spong_com", - "genres": [] - }, - { - "id": 9379, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9380, - "name": "Descenders", - "site": "metacritic_com", - "genres": [ - "Automobile", - "Arcade", - "Racing" - ] - }, - { - "id": 9381, - "name": "Shantae: Half-Genie Hero", - "site": "spong_com", - "genres": [] - }, - { - "id": 9382, - "name": "Shoot Your Nightmare Halloween Special", - "site": "spong_com", - "genres": [] - }, - { - "id": 9383, - "name": "Shovel Knight: Shovel of Hope", - "site": "spong_com", - "genres": [] - }, - { - "id": 9384, - "name": "Shrek 2: The Game", - "site": "spong_com", - "genres": [] - }, - { - "id": 9385, - "name": "Silence on The Line", - "site": "spong_com", - "genres": [] - }, - { - "id": 9386, - "name": "Silent Hill: Alchemilla", - "site": "spong_com", - "genres": [] - }, - { - "id": 9387, - "name": "Silverfall", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9388, - "name": "Silverfall: Earth Awakening", - "site": "spong_com", - "genres": [] - }, - { - "id": 9389, - "name": "Singularity", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9390, - "name": "Slap-Man", - "site": "spong_com", - "genres": [] - }, - { - "id": 9391, - "name": "Slay the Spire", - "site": "spong_com", - "genres": [] - }, - { - "id": 9392, - "name": "Sleeping Dogs", - "site": "spong_com", - "genres": [ - "Adventure: Free Roaming" - ] - }, - { - "id": 9393, - "name": "Slender: The Arrival", - "site": "spong_com", - "genres": [] - }, - { - "id": 9394, - "name": "Slendytubbies", - "site": "spong_com", - "genres": [] - }, - { - "id": 9395, - "name": "Slime Rancher", - "site": "spong_com", - "genres": [] - }, - { - "id": 9396, - "name": "Sniper Elite", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9397, - "name": "Sniper Elite 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 9398, - "name": "Sniper Elite V2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9399, - "name": "Sophie's Curse", - "site": "spong_com", - "genres": [] - }, - { - "id": 9400, - "name": "South Park: The Fractured But Whole", - "site": "spong_com", - "genres": [] - }, - { - "id": 9401, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9402, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9403, - "name": "South Park: The Stick of Truth", - "site": "spong_com", - "genres": [] - }, - { - "id": 9404, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9405, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9406, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9407, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9408, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9409, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9410, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9411, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9412, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9413, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9414, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9415, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9416, - "name": "Spooky's House of Jump Scares", - "site": "spong_com", - "genres": [] - }, - { - "id": 9417, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Adventure" - ] - }, - { - "id": 9418, - "name": "Star Wars: The Force Unleashed", - "site": "spong_com", - "genres": [] - }, - { - "id": 9419, - "name": "Star Wars: The Force Unleashed II", - "site": "spong_com", - "genres": [ - "Beat 'Em Up: Hack and Slash", - "Adventure" - ] - }, - { - "id": 9420, - "name": "SteamWorld Dig", - "site": "spong_com", - "genres": [] - }, - { - "id": 9421, - "name": "SteamWorld Dig 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9422, - "name": "SteamWorld Heist", - "site": "spong_com", - "genres": [] - }, - { - "id": 9423, - "name": "Steampunk Tower 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9424, - "name": "Steel Rats", - "site": "spong_com", - "genres": [] - }, - { - "id": 9425, - "name": "Stories: The Path of Destinies", - "site": "spong_com", - "genres": [] - }, - { - "id": 9426, - "name": "Street Fighter X Tekken", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 9428, - "name": "Stupidella", - "site": "spong_com", - "genres": [] - }, - { - "id": 9429, - "name": "Sudeki", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9430, - "name": "Superhot", - "site": "spong_com", - "genres": [] - }, - { - "id": 9431, - "name": "Surgeon Simulator", - "site": "spong_com", - "genres": [] - }, - { - "id": 9432, - "name": "Tales from the Borderlands", - "site": "spong_com", - "genres": [ - "Compilation" - ] - }, - { - "id": 9433, - "name": "Tales of Berseria", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9434, - "name": "Tales of Zestiria", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9435, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9436, - "name": "Tasty Planet: Back for Seconds", - "site": "spong_com", - "genres": [] - }, - { - "id": 9437, - "name": "Tekken 7", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 9438, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "spong_com", - "genres": [] - }, - { - "id": 9439, - "name": "Tesla vs Lovecraft", - "site": "spong_com", - "genres": [] - }, - { - "id": 9440, - "name": "Teslagrad", - "site": "spong_com", - "genres": [] - }, - { - "id": 9441, - "name": "The Banner Saga", - "site": "spong_com", - "genres": [] - }, - { - "id": 9442, - "name": "The Banner Saga 2", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9443, - "name": "The Bard’s Tale (2004)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9444, - "name": "The Beginner's Guide", - "site": "spong_com", - "genres": [] - }, - { - "id": 9445, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9446, - "name": "The Binding of Isaac: Rebirth", - "site": "spong_com", - "genres": [] - }, - { - "id": 9447, - "name": "The Bunker", - "site": "spong_com", - "genres": [] - }, - { - "id": 9448, - "name": "The Cat Lady", - "site": "spong_com", - "genres": [] - }, - { - "id": 9449, - "name": "The Council", - "site": "spong_com", - "genres": [] - }, - { - "id": 9450, - "name": "The Curse of Blackwater", - "site": "spong_com", - "genres": [] - }, - { - "id": 9451, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "spong_com", - "genres": [] - }, - { - "id": 9452, - "name": "The Darkness II", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Adventure: Survival Horror" - ] - }, - { - "id": 9453, - "name": "The Elder Scrolls IV: Oblivion", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9454, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9455, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9456, - "name": "The Elder Scrolls V: Skyrim", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9457, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "spong_com", - "genres": [ - "Add-on pack" - ] - }, - { - "id": 9458, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "spong_com", - "genres": [ - "Add-on pack", - "Adventure: Role Playing" - ] - }, - { - "id": 9459, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9460, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "spong_com", - "genres": [ - "Add-on pack", - "Adventure: Role Playing" - ] - }, - { - "id": 9461, - "name": "The Evil Within", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 9462, - "name": "The Evil Within 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9463, - "name": "The Evil Within: The Assignment (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9464, - "name": "The Evil Within: The Consequence (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9465, - "name": "The Evil Within: The Executioner (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9466, - "name": "The Expendabros", - "site": "spong_com", - "genres": [] - }, - { - "id": 9467, - "name": "The Final Station", - "site": "spong_com", - "genres": [] - }, - { - "id": 9468, - "name": "The First Tree", - "site": "spong_com", - "genres": [] - }, - { - "id": 9469, - "name": "The Forest", - "site": "spong_com", - "genres": [ - "Platform" - ] - }, - { - "id": 9470, - "name": "The Hat Man Shadow Ward", - "site": "spong_com", - "genres": [] - }, - { - "id": 9471, - "name": "The Incredible Adventures of Van Helsing", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9472, - "name": "The Incredible Adventures of Van Helsing II", - "site": "spong_com", - "genres": [] - }, - { - "id": 9473, - "name": "The Incredible Adventures of Van Helsing III", - "site": "spong_com", - "genres": [] - }, - { - "id": 9474, - "name": "The Last Remnant", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9475, - "name": "The Mask Man", - "site": "spong_com", - "genres": [] - }, - { - "id": 9476, - "name": "The Punisher (2005)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9477, - "name": "The Secret of Monkey Island: Special Edition", - "site": "spong_com", - "genres": [ - "Adventure: Graphic" - ] - }, - { - "id": 9478, - "name": "The Slippery Slope", - "site": "spong_com", - "genres": [] - }, - { - "id": 9479, - "name": "The Stanley Parable", - "site": "spong_com", - "genres": [] - }, - { - "id": 9480, - "name": "The Walking Dead: A New Frontier", - "site": "spong_com", - "genres": [] - }, - { - "id": 9481, - "name": "The Walking Dead: Michonne", - "site": "spong_com", - "genres": [] - }, - { - "id": 9482, - "name": "The Walking Dead: Season One", - "site": "spong_com", - "genres": [] - }, - { - "id": 9483, - "name": "The Walking Dead: Season Two", - "site": "spong_com", - "genres": [ - "Adventure: Point and Click" - ] - }, - { - "id": 9484, - "name": "The Witcher", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9485, - "name": "The Witcher 2: Assassins of Kings", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9486, - "name": "The Witcher 3: Wild Hunt", - "site": "spong_com", - "genres": [] - }, - { - "id": 9487, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9488, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9489, - "name": "The Wolf Among Us", - "site": "spong_com", - "genres": [ - "Adventure: Point and Click" - ] - }, - { - "id": 9490, - "name": "Thief", - "site": "spong_com", - "genres": [] - }, - { - "id": 9491, - "name": "This Is The Police", - "site": "spong_com", - "genres": [] - }, - { - "id": 9492, - "name": "TimeShift", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9493, - "name": "Titan Quest", - "site": "spong_com", - "genres": [] - }, - { - "id": 9494, - "name": "Titan Quest Anniversary Edition", - "site": "spong_com", - "genres": [] - }, - { - "id": 9495, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9496, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "spong_com", - "genres": [ - "Add-on pack", - "Combat Game" - ] - }, - { - "id": 9497, - "name": "Titan Souls", - "site": "spong_com", - "genres": [] - }, - { - "id": 9498, - "name": "Titanfall 2", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9499, - "name": "To The Moon", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing", - "Adventure: Point and Click" - ] - }, - { - "id": 9500, - "name": "Tomb Raider", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9501, - "name": "Torchlight", - "site": "spong_com", - "genres": [ - "Beat 'Em Up: Hack and Slash" - ] - }, - { - "id": 9502, - "name": "Torchlight II", - "site": "spong_com", - "genres": [] - }, - { - "id": 9503, - "name": "Toren", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9504, - "name": "Tormentum: Dark Sorrow", - "site": "spong_com", - "genres": [] - }, - { - "id": 9505, - "name": "Total Overdose", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 9506, - "name": "Transistor", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9507, - "name": "Trine", - "site": "spong_com", - "genres": [] - }, - { - "id": 9508, - "name": "Trine 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9509, - "name": "Trine 3: The Artifacts of Power", - "site": "spong_com", - "genres": [] - }, - { - "id": 9510, - "name": "Troll Tale", - "site": "spong_com", - "genres": [] - }, - { - "id": 9511, - "name": "Trollface Quest", - "site": "spong_com", - "genres": [] - }, - { - "id": 9512, - "name": "Trollface Quest 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9513, - "name": "Trollface Quest 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 9514, - "name": "Trollface Quest 4", - "site": "spong_com", - "genres": [] - }, - { - "id": 9515, - "name": "Trollface Quest 5", - "site": "spong_com", - "genres": [] - }, - { - "id": 9516, - "name": "Trollface Quest 6", - "site": "spong_com", - "genres": [] - }, - { - "id": 9517, - "name": "Trollface Quest 7", - "site": "spong_com", - "genres": [] - }, - { - "id": 9518, - "name": "Trollface Quest: Video Memes", - "site": "spong_com", - "genres": [] - }, - { - "id": 9519, - "name": "Twin Sector", - "site": "spong_com", - "genres": [] - }, - { - "id": 9520, - "name": "Two Worlds", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9521, - "name": "Typical Nightmare", - "site": "spong_com", - "genres": [] - }, - { - "id": 9522, - "name": "Ultimate Epic Battle Simulator", - "site": "spong_com", - "genres": [] - }, - { - "id": 9523, - "name": "Ultra Street Fighter IV", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 9524, - "name": "Undertale", - "site": "spong_com", - "genres": [] - }, - { - "id": 9525, - "name": "Unfair Mario", - "site": "spong_com", - "genres": [] - }, - { - "id": 9526, - "name": "Unmechanical", - "site": "spong_com", - "genres": [] - }, - { - "id": 9527, - "name": "Unmechanical: Extended (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 9528, - "name": "Until the last", - "site": "spong_com", - "genres": [] - }, - { - "id": 9529, - "name": "Valiant Hearts: The Great War", - "site": "spong_com", - "genres": [] - }, - { - "id": 9530, - "name": "Vanish", - "site": "spong_com", - "genres": [] - }, - { - "id": 9531, - "name": "Victor Vran", - "site": "spong_com", - "genres": [] - }, - { - "id": 9532, - "name": "Vikings: Wolves of Midgard", - "site": "spong_com", - "genres": [ - "Adventure: Role Playing" - ] - }, - { - "id": 9533, - "name": "Visage", - "site": "spong_com", - "genres": [] - }, - { - "id": 9534, - "name": "Wanted: Weapons of Fate", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Adventure" - ] - }, - { - "id": 9535, - "name": "Warcraft III: Reign of Chaos", - "site": "spong_com", - "genres": [ - "Strategy: Combat" - ] - }, - { - "id": 9536, - "name": "Warcraft III: The Frozen Throne", - "site": "spong_com", - "genres": [ - "Strategy: Combat", - "Adventure: Role Playing" - ] - }, - { - "id": 9537, - "name": "Warhammer 40.000: Space Marine", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9538, - "name": "Warhammer: Chaosbane", - "site": "spong_com", - "genres": [] - }, - { - "id": 9539, - "name": "We Happy Few", - "site": "spong_com", - "genres": [] - }, - { - "id": 9540, - "name": "Whack The Thief", - "site": "spong_com", - "genres": [] - }, - { - "id": 9541, - "name": "Whack Your Boss", - "site": "spong_com", - "genres": [] - }, - { - "id": 9542, - "name": "Whack Your Ex", - "site": "spong_com", - "genres": [] - }, - { - "id": 9543, - "name": "Whack Your Neighbour", - "site": "spong_com", - "genres": [] - }, - { - "id": 9544, - "name": "Whack Your Teacher", - "site": "spong_com", - "genres": [] - }, - { - "id": 9545, - "name": "What Remains of Edith Finch", - "site": "spong_com", - "genres": [] - }, - { - "id": 9546, - "name": "Will Rock", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9547, - "name": "Willy-Nilly Knight", - "site": "spong_com", - "genres": [] - }, - { - "id": 9548, - "name": "Wizard of Legend", - "site": "spong_com", - "genres": [] - }, - { - "id": 9549, - "name": "Wolfenstein", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9550, - "name": "Wolfenstein: The New Order", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9551, - "name": "Wolfenstein: The Old Blood", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9552, - "name": "Woolfe - The Red Hood Diaries", - "site": "spong_com", - "genres": [] - }, - { - "id": 9553, - "name": "Worms 2", - "site": "spong_com", - "genres": [ - "Strategy: Combat", - "Shoot 'Em Up" - ] - }, - { - "id": 9554, - "name": "X-Blades / Ониблэйд", - "site": "spong_com", - "genres": [] - }, - { - "id": 9555, - "name": "X-Morph: Defense", - "site": "spong_com", - "genres": [] - }, - { - "id": 9556, - "name": "Yandere School", - "site": "spong_com", - "genres": [] - }, - { - "id": 9557, - "name": "Year Walk", - "site": "spong_com", - "genres": [] - }, - { - "id": 9558, - "name": "ZOMBI", - "site": "spong_com", - "genres": [ - "Adventure: Point and Click" - ] - }, - { - "id": 9559, - "name": "Zero Escape: Zero Time Dilemma", - "site": "spong_com", - "genres": [] - }, - { - "id": 9560, - "name": "Ziggurat", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 9561, - "name": "Zombie Driver HD", - "site": "spong_com", - "genres": [] - }, - { - "id": 9562, - "name": "Zombie Party", - "site": "spong_com", - "genres": [ - "Various: Party Game" - ] - }, - { - "id": 9563, - "name": "Zombie Vikings", - "site": "spong_com", - "genres": [] - }, - { - "id": 9564, - "name": "Zuma Deluxe", - "site": "spong_com", - "genres": [ - "Puzzle" - ] - }, - { - "id": 9565, - "name": "Zuma's Revenge!", - "site": "spong_com", - "genres": [] - }, - { - "id": 9566, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "spong_com", - "genres": [] - }, - { - "id": 9567, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "spong_com", - "genres": [] - }, - { - "id": 9568, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "spong_com", - "genres": [] - }, - { - "id": 9569, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "spong_com", - "genres": [] - }, - { - "id": 9570, - "name": "Невероятные Приключения Трояна", - "site": "spong_com", - "genres": [] - }, - { - "id": 9571, - "name": "Невероятные Приключения Трояна 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9572, - "name": "Невероятные Приключения Трояна 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 9573, - "name": "Предатор", - "site": "spong_com", - "genres": [] - }, - { - "id": 9574, - "name": "Стоп-Гоп", - "site": "spong_com", - "genres": [] - }, - { - "id": 9575, - "name": "Стоп-Гоп 2", - "site": "spong_com", - "genres": [] - }, - { - "id": 9576, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "spong_com", - "genres": [] - }, - { - "id": 9577, - "name": "Detention", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 9578, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9579, - "name": "Devil May Cry 4", - "site": "metacritic_com", - "genres": [ - "Beat-'Em-Up", - "Action Adventure", - "General", - "Linear", - "Fantasy", - "Action" - ] - }, - { - "id": 9580, - "name": "Devil May Cry 5", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 9581, - "name": "Diablo II", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9582, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9583, - "name": "Diablo III", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9584, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9585, - "name": "Dig or Die", - "site": "metacritic_com", - "genres": [ - "Sandbox", - "General", - "Action", - "Action Adventure" - ] - }, - { - "id": 9586, - "name": "Disciples II: Dark Prophecy", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9587, - "name": "Disciples III: Renaissance", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9588, - "name": "Dishonored", - "site": "metacritic_com", - "genres": [ - "General", - "Modern", - "Action Adventure" - ] - }, - { - "id": 9589, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9590, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9591, - "name": "Distraint", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 9592, - "name": "Disturbed", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 9593, - "name": "DmC: Devil May Cry", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "General", - "Linear", - "Fantasy", - "Action" - ] - }, - { - "id": 9594, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9595, - "name": "Doki Doki Literature Club", - "site": "metacritic_com", - "genres": [ - "Visual Novel", - "Adventure" - ] - }, - { - "id": 9596, - "name": "Don't Knock Twice", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9597, - "name": "Doom 3", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9598, - "name": "Double Dragon Neon", - "site": "metacritic_com", - "genres": [ - "Beat-'Em-Up", - "2D", - "Action" - ] - }, - { - "id": 9599, - "name": "Down Stairs", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9600, - "name": "Downfall", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 9601, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9602, - "name": "Dragon Age II", - "site": "metacritic_com", - "genres": [ - "PC-style RPG", - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9603, - "name": "Dragon Age: Inquisition", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Action RPG", - "Western-Style" - ] - }, - { - "id": 9604, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9605, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9606, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9607, - "name": "Dragon Age: Origins", - "site": "metacritic_com", - "genres": [ - "PC-style RPG", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9608, - "name": "Dragon Age: Origins - Awakening", - "site": "metacritic_com", - "genres": [ - "PC-style RPG", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9609, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9610, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9611, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9612, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9613, - "name": "Dragon's Dogma: Dark Arisen", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9614, - "name": "Draw a Stickman: Episode 1", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9615, - "name": "Draw a Stickman: Episode 2", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 9616, - "name": "DreadOut", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 9617, - "name": "Dropsy", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 9618, - "name": "Dude, Stop", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "Party / Minigame" - ] - }, - { - "id": 9619, - "name": "Duke Nukem Forever", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9620, - "name": "Dungeon Nightmares", - "site": "metacritic_com", - "genres": [ - "Adventure", - "First-Person", - "3D", - "General", - "Action" - ] - }, - { - "id": 9621, - "name": "Dying Light", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 9622, - "name": "Dying Light: The Following (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9623, - "name": "Emily Wants To Play", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9624, - "name": "Escape Dead Island", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Survival", - "Adventure", - "3D", - "Third-Person", - "Modern" - ] - }, - { - "id": 9625, - "name": "Evil Defenders", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "Defense", - "Strategy" - ] - }, - { - "id": 9626, - "name": "Evoland", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Adventure" - ] - }, - { - "id": 9627, - "name": "Evoland 2", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing" - ] - }, - { - "id": 9628, - "name": "Eyes The Horror Game", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 9629, - "name": "F.E.A.R.", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9630, - "name": "F.E.A.R. 2: Project Origin", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9631, - "name": "F.E.A.R. 2: Reborn", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9632, - "name": "F.E.A.R. 3", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Modern", - "Action" - ] - }, - { - "id": 9633, - "name": "F.E.A.R. Extraction Point", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9634, - "name": "F.E.A.R. Perseus Mandate", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9635, - "name": "Fable (2004)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9636, - "name": "Fable III", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Japanese-Style", - "Console-style RPG" - ] - }, - { - "id": 9637, - "name": "Fahrenheit", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9638, - "name": "Failman", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9639, - "name": "Fallout 3", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9640, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9641, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9642, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9643, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9644, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9645, - "name": "Fallout 4", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9646, - "name": "Fallout: New Vegas", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Role-Playing", - "Action RPG", - "Western-Style" - ] - }, - { - "id": 9647, - "name": "Family Trouble", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9648, - "name": "Far Cry", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9649, - "name": "Far Cry 2", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9650, - "name": "Far Cry 3: Blood Dragon", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9651, - "name": "FarSky", - "site": "metacritic_com", - "genres": [ - "General", - "Survival", - "Action", - "Action Adventure" - ] - }, - { - "id": 9652, - "name": "Final Fantasy III (Remake)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9653, - "name": "Final Fantasy IV (Remake)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9654, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9655, - "name": "Final Fantasy X HD Remaster", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Console-style RPG" - ] - }, - { - "id": 9656, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9657, - "name": "Final Fantasy XIII", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Japanese-Style", - "Console-style RPG" - ] - }, - { - "id": 9658, - "name": "Final Fantasy XIII-2", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Japanese-Style", - "Console-style RPG" - ] - }, - { - "id": 9659, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9660, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9661, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9662, - "name": "Final Fantasy XV", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9663, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9664, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9665, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9666, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9667, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9668, - "name": "Firewatch", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9669, - "name": "First Winter", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9670, - "name": "Five Minutes", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9671, - "name": "Five Nights At Freddy's", - "site": "metacritic_com", - "genres": [ - "General", - "Survival", - "Adventure", - "Action Adventure" - ] - }, - { - "id": 9672, - "name": "Five Nights At Freddy's 2", - "site": "metacritic_com", - "genres": [ - "General", - "Survival", - "Adventure", - "Action Adventure" - ] - }, - { - "id": 9673, - "name": "Five Nights At Freddy's 3", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9674, - "name": "Five Nights At Freddy's 4", - "site": "metacritic_com", - "genres": [ - "General", - "Survival", - "Action", - "Action Adventure" - ] - }, - { - "id": 9675, - "name": "Five Nights at Freddy's 3D", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9676, - "name": "Five Nights in Anime v3", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9677, - "name": "Fran Bow", - "site": "metacritic_com", - "genres": [ - "General", - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 9678, - "name": "Front Mission Evolved", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Sci-Fi", - "Third-Person", - "Action" - ] - }, - { - "id": 9679, - "name": "GUN", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9680, - "name": "Gemini Rue", - "site": "metacritic_com", - "genres": [ - "Adventure", - "Sci-Fi", - "Third-Person", - "General", - "Point-and-Click" - ] - }, - { - "id": 9681, - "name": "Geometry Dash", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "Music", - "Rhythm", - "Action" - ] - }, - { - "id": 9682, - "name": "Ghost of a Tale", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 9683, - "name": "Goat Simulator", - "site": "metacritic_com", - "genres": [ - "General", - "Simulation", - "Adventure" - ] - }, - { - "id": 9684, - "name": "Grand Theft Auto III", - "site": "metacritic_com", - "genres": [ - "Modern", - "Action Adventure" - ] - }, - { - "id": 9685, - "name": "Grand Theft Auto: San Andreas", - "site": "metacritic_com", - "genres": [ - "Modern", - "Action Adventure" - ] - }, - { - "id": 9686, - "name": "Grand Theft Auto: Vice City", - "site": "metacritic_com", - "genres": [ - "Modern", - "Action Adventure" - ] - }, - { - "id": 9687, - "name": "Grandpa", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9688, - "name": "Grey", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9689, - "name": "Grim Fandango Remastered", - "site": "metacritic_com", - "genres": [ - "Adventure", - "3D", - "Third-Person", - "General", - "Fantasy" - ] - }, - { - "id": 9690, - "name": "Grow Home", - "site": "metacritic_com", - "genres": [ - "Sandbox", - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9691, - "name": "Grow Up", - "site": "metacritic_com", - "genres": [ - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9692, - "name": "Guns, Gore & Cannoli", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 9693, - "name": "Guns, Gore & Cannoli 2", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 9694, - "name": "Half-Life", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9695, - "name": "Half-Life 2", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9696, - "name": "Half-Life 2: Episode One", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9697, - "name": "Half-Life 2: Episode Two", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9698, - "name": "Half-Life: Blue Shift", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9699, - "name": "Half-Life: Opposing Force", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9700, - "name": "Half-Life: Paranoia", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9701, - "name": "Half-Life: The Xeno Project", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9702, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9703, - "name": "Halo: Combat Evolved", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Action", - "Sci-Fi" - ] - }, - { - "id": 9704, - "name": "Hand of Fate", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "General", - "Role-Playing", - "Board / Card Game" - ] - }, - { - "id": 9705, - "name": "Hand of Fate 2", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "Board / Card Game" - ] - }, - { - "id": 9706, - "name": "Happy Room", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9707, - "name": "Happy Wheels", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9708, - "name": "Hard Reset", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9709, - "name": "Hard Reset: Exile (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9710, - "name": "Harts", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9711, - "name": "Hatred", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shoot-'Em-Up", - "Top-Down", - "Shooter", - "Third-Person", - "Modern", - "Action" - ] - }, - { - "id": 9712, - "name": "Hellblade: Senua's Sacrifice", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 9713, - "name": "Hellgate: London", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9714, - "name": "Her Story", - "site": "metacritic_com", - "genres": [ - "Visual Novel", - "Adventure" - ] - }, - { - "id": 9715, - "name": "Hero Academy", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9716, - "name": "Heroes of Might and Magic III", - "site": "metacritic_com", - "genres": [ - "General", - "Strategy" - ] - }, - { - "id": 9717, - "name": "Heroes of Might and Magic IV", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9718, - "name": "Heroes of Might and Magic V", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9719, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9720, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9721, - "name": "Homefront", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9722, - "name": "Hotel Remorse", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9723, - "name": "Hotline Miami", - "site": "metacritic_com", - "genres": [ - "General", - "Linear", - "Action", - "Action Adventure" - ] - }, - { - "id": 9724, - "name": "Hotline Miami 2: Wrong Number", - "site": "metacritic_com", - "genres": [ - "General", - "Linear", - "Action", - "Action Adventure" - ] - }, - { - "id": 9725, - "name": "House Flipper", - "site": "metacritic_com", - "genres": [ - "Simulation", - "Virtual Life", - "Virtual" - ] - }, - { - "id": 9726, - "name": "I Am Alive", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 9727, - "name": "I am Setsuna", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Japanese-Style" - ] - }, - { - "id": 9728, - "name": "I hate this game", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9729, - "name": "INSIDE", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9730, - "name": "Indivisible", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9731, - "name": "Injustice: Gods Among Us", - "site": "metacritic_com", - "genres": [ - "2D", - "Action", - "Fighting" - ] - }, - { - "id": 9732, - "name": "Into the Breach", - "site": "metacritic_com", - "genres": [ - "Tactics", - "General", - "Turn-Based", - "Strategy" - ] - }, - { - "id": 9733, - "name": "Jade Empire", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9734, - "name": "Jotun", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9735, - "name": "Just Cause", - "site": "metacritic_com", - "genres": [ - "Modern", - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9736, - "name": "Just Cause 2", - "site": "metacritic_com", - "genres": [ - "Modern", - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9737, - "name": "Kane & Lynch 2: Dog Days", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Third-Person", - "Modern", - "Action" - ] - }, - { - "id": 9738, - "name": "Killer is Dead: Nightmare Edition", - "site": "metacritic_com", - "genres": [ - "General", - "Sci-Fi", - "Action Adventure" - ] - }, - { - "id": 9739, - "name": "King's Bounty: Armored Princess", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Turn-Based", - "Strategy" - ] - }, - { - "id": 9740, - "name": "King's Bounty: Crossworlds", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9741, - "name": "King's Bounty: Dark Side", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9742, - "name": "King's Bounty: The Legend", - "site": "metacritic_com", - "genres": [ - "Tactics", - "Turn-Based", - "Strategy", - "General", - "Fantasy" - ] - }, - { - "id": 9743, - "name": "King's Bounty: Warriors of the North", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Fantasy", - "Strategy" - ] - }, - { - "id": 9744, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9745, - "name": "Kingdom Rush", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "Strategy", - "General", - "Defense", - "Action" - ] - }, - { - "id": 9746, - "name": "Kingdom: Classic", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9747, - "name": "Kingdoms of Amalur: Reckoning", - "site": "metacritic_com", - "genres": [ - "PC-style RPG", - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9748, - "name": "Knock-Knock", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "General" - ] - }, - { - "id": 9749, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9750, - "name": "Lara Croft and the Guardian of Light", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9751, - "name": "Lara Croft and the Temple of Osiris", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action Adventure", - "Linear", - "Action" - ] - }, - { - "id": 9752, - "name": "Late Shift", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 9753, - "name": "Layers of Fear", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9754, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9755, - "name": "Left 4 Dead", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9756, - "name": "Left 4 Dead 2", - "site": "metacritic_com", - "genres": [ - "Tactical", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9757, - "name": "Legend Hand of God", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing" - ] - }, - { - "id": 9758, - "name": "Legend of Grimrock", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing" - ] - }, - { - "id": 9759, - "name": "Legendary", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9760, - "name": "Life After Us: Shipwrecked", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9761, - "name": "Life Goes On: Done to Death", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9762, - "name": "Life Is Strange", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure", - "Sci-Fi" - ] - }, - { - "id": 9763, - "name": "Limbo", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9764, - "name": "Little Nightmares", - "site": "metacritic_com", - "genres": [ - "General", - "Linear", - "Action Adventure" - ] - }, - { - "id": 9765, - "name": "Little Nightmares - The Depths (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9766, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9767, - "name": "Loki: Heroes of Mythology", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9768, - "name": "Lost Planet: Colonies", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9769, - "name": "Lost: Via Domus", - "site": "metacritic_com", - "genres": [ - "General", - "Modern", - "Action Adventure" - ] - }, - { - "id": 9770, - "name": "Love Is Strange (DEMO)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9771, - "name": "METAL SLUG", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9772, - "name": "METAL SLUG 3", - "site": "metacritic_com", - "genres": [ - "Horizontal", - "Vertical", - "Shoot-'Em-Up", - "Shooter", - "Action" - ] - }, - { - "id": 9773, - "name": "MINERVA: Metastasis", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9774, - "name": "MOTHERGUNSHIP", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9775, - "name": "Machinarium", - "site": "metacritic_com", - "genres": [ - "Adventure", - "Sci-Fi", - "Third-Person", - "General", - "Point-and-Click" - ] - }, - { - "id": 9776, - "name": "Magibot", - "site": "metacritic_com", - "genres": [ - "Action", - "2D", - "Platformer", - "Puzzle" - ] - }, - { - "id": 9777, - "name": "Magicka", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 9778, - "name": "Magicka 2", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 9779, - "name": "Manual Samuel", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Adventure" - ] - }, - { - "id": 9780, - "name": "Mario.EXE", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9781, - "name": "Marvel vs. Capcom: Infinite", - "site": "metacritic_com", - "genres": [ - "2D", - "Action", - "Fighting" - ] - }, - { - "id": 9782, - "name": "Masochisia", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 9783, - "name": "Masters of Anima", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "MOBA", - "Strategy" - ] - }, - { - "id": 9784, - "name": "Max Payne", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Third-Person", - "Modern", - "Action" - ] - }, - { - "id": 9785, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Third-Person", - "Modern", - "Action" - ] - }, - { - "id": 9786, - "name": "Max Payne 3", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Third-Person", - "Modern", - "Action" - ] - }, - { - "id": 9787, - "name": "Max: The Curse Of Brotherhood", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 9788, - "name": "McPixel", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 9789, - "name": "Metal Gear Rising: Revengeance", - "site": "metacritic_com", - "genres": [ - "Tactical", - "Shooter", - "Action Adventure", - "Third-Person", - "General", - "Modern", - "Action" - ] - }, - { - "id": 9790, - "name": "Metal Wolf Chaos XD", - "site": "metacritic_com", - "genres": [ - "Simulation", - "Combat", - "Vehicle" - ] - }, - { - "id": 9791, - "name": "Metro 2033", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9792, - "name": "Metro 2033 Redux", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9793, - "name": "Metro Exodus", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 9794, - "name": "Metro Last Light", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9795, - "name": "Metro Last Light Redux", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9796, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9797, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9798, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9799, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9800, - "name": "Middle-earth: Shadow of Mordor", - "site": "metacritic_com", - "genres": [ - "Fantasy", - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9801, - "name": "Might & Magic Heroes VI", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Turn-Based", - "Strategy", - "General", - "Fantasy" - ] - }, - { - "id": 9802, - "name": "Minecraft Story Mode", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9803, - "name": "Mini Ninjas", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Adventure", - "Action Adventure" - ] - }, - { - "id": 9804, - "name": "Mirror's Edge", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Action Adventure", - "General", - "Modern", - "Action" - ] - }, - { - "id": 9805, - "name": "Monst", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9806, - "name": "Mortal Kombat IX", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9807, - "name": "Mortal Kombat X", - "site": "metacritic_com", - "genres": [ - "3D", - "2D", - "Action", - "Fighting" - ] - }, - { - "id": 9808, - "name": "Mother Russia Bleeds", - "site": "metacritic_com", - "genres": [ - "Beat-'Em-Up", - "2D", - "Action" - ] - }, - { - "id": 9809, - "name": "Mount Your Friends", - "site": "metacritic_com", - "genres": [ - "General", - "Sports" - ] - }, - { - "id": 9810, - "name": "Mr. Robot", - "site": "metacritic_com", - "genres": [ - "General", - "Sci-Fi", - "Action Adventure" - ] - }, - { - "id": 9811, - "name": "Mr.President!", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9812, - "name": "Murdered: Soul Suspect", - "site": "metacritic_com", - "genres": [ - "Horror", - "Survival", - "Action Adventure", - "General", - "Action" - ] - }, - { - "id": 9813, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "metacritic_com", - "genres": [ - "3D", - "Action", - "Fighting" - ] - }, - { - "id": 9814, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "metacritic_com", - "genres": [ - "3D", - "Action", - "Fighting" - ] - }, - { - "id": 9815, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "metacritic_com", - "genres": [ - "3D", - "Action", - "Fighting" - ] - }, - { - "id": 9816, - "name": "Need for Speed: Underground", - "site": "metacritic_com", - "genres": [ - "Simulation", - "Automobile", - "GT / Street", - "Racing", - "Driving" - ] - }, - { - "id": 9817, - "name": "Never Alone", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9818, - "name": "Neverending Nightmares", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9819, - "name": "Nevermind", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Adventure", - "First-Person", - "3D", - "Linear" - ] - }, - { - "id": 9820, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9821, - "name": "Neverwinter Nights", - "site": "metacritic_com", - "genres": [ - "PC-style RPG", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9822, - "name": "Neverwinter Nights 2", - "site": "metacritic_com", - "genres": [ - "PC-style RPG", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9823, - "name": "NieR: Automata", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9824, - "name": "Nightmare House 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9825, - "name": "Nosferatu: The Wrath of Malachi", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Fantasy", - "Action" - ] - }, - { - "id": 9826, - "name": "Nox Timore", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9827, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "metacritic_com", - "genres": [ - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9828, - "name": "Omensight", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9829, - "name": "One Hand Clapping (DEMO)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9830, - "name": "One Night at Flumpty's", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9831, - "name": "One Night at Flumpty's 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9832, - "name": "One Piece: Burning Blood", - "site": "metacritic_com", - "genres": [ - "3D", - "Action", - "Fighting" - ] - }, - { - "id": 9833, - "name": "Orcs Must Die!", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "Strategy", - "General", - "Defense", - "Action" - ] - }, - { - "id": 9834, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9835, - "name": "Ori and the Blind Forest", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9836, - "name": "Outlast", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 9837, - "name": "Outlast: Whistleblower", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 9838, - "name": "Overlord", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 9839, - "name": "Overlord: Raising Hell", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 9840, - "name": "Oxenfree", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure", - "Action Adventure" - ] - }, - { - "id": 9841, - "name": "Painkiller", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Fantasy", - "Action" - ] - }, - { - "id": 9842, - "name": "Painkiller: Battle Out of Hell", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Fantasy", - "Action" - ] - }, - { - "id": 9843, - "name": "Painkiller: Overdose", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Fantasy", - "Action" - ] - }, - { - "id": 9844, - "name": "Paint the Town Red", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9845, - "name": "Panty Party", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 9846, - "name": "Papers, Please", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 9847, - "name": "Peace, Death!", - "site": "metacritic_com", - "genres": [ - "Simulation", - "Career", - "Virtual" - ] - }, - { - "id": 9848, - "name": "Pillars of Eternity", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "PC-style RPG", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 9849, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9850, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9851, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9852, - "name": "Pinstripe", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "General" - ] - }, - { - "id": 9853, - "name": "Pirates of the Caribbean: At World’s End", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9854, - "name": "Pizza Delivery 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9855, - "name": "Plague Inc: Evolved", - "site": "metacritic_com", - "genres": [ - "General", - "Turn-Based", - "Sci-Fi", - "Strategy" - ] - }, - { - "id": 9856, - "name": "Plants vs. Zombies", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "Strategy", - "General", - "Fantasy", - "Defense" - ] - }, - { - "id": 9857, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "metacritic_com", - "genres": [ - "Tactical", - "Shooter", - "Third-Person", - "General", - "Action" - ] - }, - { - "id": 9858, - "name": "Pony Island", - "site": "metacritic_com", - "genres": [ - "General", - "Puzzle" - ] - }, - { - "id": 9859, - "name": "Portal", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9860, - "name": "Portal 2", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9861, - "name": "Prey (2006)", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9862, - "name": "Prince of Persia (2008)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9863, - "name": "Prince of Persia: The Two Thrones", - "site": "metacritic_com", - "genres": [ - "Linear", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 9864, - "name": "Prospekt", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 9865, - "name": "Prototype", - "site": "metacritic_com", - "genres": [ - "Open-World", - "Sci-Fi", - "Action Adventure" - ] - }, - { - "id": 9866, - "name": "Prototype 2", - "site": "metacritic_com", - "genres": [ - "Open-World", - "Sci-Fi", - "Action Adventure" - ] - }, - { - "id": 9867, - "name": "Psychonauts", - "site": "metacritic_com", - "genres": [ - "Platformer", - "Action", - "3D" - ] - }, - { - "id": 9868, - "name": "PuniTy", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9869, - "name": "P·O·L·L·E·N", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9870, - "name": "Quake 4", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9871, - "name": "Quantum Break", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9872, - "name": "Raft", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9873, - "name": "Rage", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9874, - "name": "Rampage Knights", - "site": "metacritic_com", - "genres": [ - "Beat-'Em-Up", - "2D", - "Action" - ] - }, - { - "id": 9875, - "name": "Rayman Legends", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9876, - "name": "Rayman Origins", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action Adventure", - "Fantasy", - "Action" - ] - }, - { - "id": 9877, - "name": "Rayman Raving Rabbids", - "site": "metacritic_com", - "genres": [ - "Platformer", - "Miscellaneous", - "Party", - "3D", - "Party / Minigame", - "Action" - ] - }, - { - "id": 9878, - "name": "Rayman Raving Rabbids 2", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "Party / Minigame", - "Party" - ] - }, - { - "id": 9879, - "name": "Real Horror Stories", - "site": "metacritic_com", - "genres": [ - "General", - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 9880, - "name": "Redeemer", - "site": "metacritic_com", - "genres": [ - "Linear", - "Action Adventure" - ] - }, - { - "id": 9881, - "name": "Reign Of Kings", - "site": "metacritic_com", - "genres": [ - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9882, - "name": "Reigns", - "site": "metacritic_com", - "genres": [ - "General", - "Simulation" - ] - }, - { - "id": 9883, - "name": "Reigns: Her Majesty", - "site": "metacritic_com", - "genres": [ - "General", - "Simulation" - ] - }, - { - "id": 9884, - "name": "Resident Evil", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9885, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9886, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9887, - "name": "Resident Evil 4", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9888, - "name": "Resident Evil 5", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9889, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9890, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9891, - "name": "Resident Evil 6", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9892, - "name": "Resident Evil 7: Biohazard", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9893, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9894, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9895, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9896, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9897, - "name": "Resident Evil: Operation Raccoon City", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Sci-Fi", - "Horror", - "Shooter", - "Action Adventure", - "Third-Person", - "Action" - ] - }, - { - "id": 9898, - "name": "Resident Evil: Revelations", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9899, - "name": "Resident Evil: Revelations 2", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 9900, - "name": "Reus", - "site": "metacritic_com", - "genres": [ - "General", - "Strategy" - ] - }, - { - "id": 9901, - "name": "Rezrog", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Roguelike" - ] - }, - { - "id": 9902, - "name": "Rise of the Tomb Raider", - "site": "metacritic_com", - "genres": [ - "General", - "Linear", - "Action Adventure" - ] - }, - { - "id": 9903, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9904, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9905, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9906, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9907, - "name": "Risen", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9908, - "name": "Rock of Ages", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "General", - "Action" - ] - }, - { - "id": 9909, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9910, - "name": "Root Of Evil: The Tailor", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9911, - "name": "Runic Rampage", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 9912, - "name": "Rust", - "site": "metacritic_com", - "genres": [ - "Historic", - "Survival", - "Action Adventure" - ] - }, - { - "id": 9913, - "name": "Ryse: Son of Rome", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 9914, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9915, - "name": "SCP-087", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9916, - "name": "SCP-087-B", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9917, - "name": "SINNER: Sacrifice for Redemption", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 9918, - "name": "SWAT 4", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 9919, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9920, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9921, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9922, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9923, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9924, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9925, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9926, - "name": "Sacred", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9927, - "name": "Sacred 3", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9928, - "name": "Sacred Citadel", - "site": "metacritic_com", - "genres": [ - "Beat-'Em-Up", - "2D", - "Action" - ] - }, - { - "id": 9929, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9930, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9931, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9932, - "name": "Saints Row IV", - "site": "metacritic_com", - "genres": [ - "Modern", - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9933, - "name": "Saints Row: Gat out of Hell", - "site": "metacritic_com", - "genres": [ - "General", - "Modern", - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9934, - "name": "Salt and Sanctuary", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9935, - "name": "Samsara Room", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9936, - "name": "Saw: The Video Game", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9937, - "name": "Scratches", - "site": "metacritic_com", - "genres": [ - "General", - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 9938, - "name": "Seasons after Fall", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9939, - "name": "Serena", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Modern", - "Adventure" - ] - }, - { - "id": 9940, - "name": "Serious Sam 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9941, - "name": "Serious Sam 3: BFE", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9942, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9943, - "name": "Serious Sam Classic: The First Encounter", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9944, - "name": "Serious Sam Classic: The Second Encounter", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9945, - "name": "Shadow Warrior", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Fantasy", - "Action" - ] - }, - { - "id": 9946, - "name": "Shadow of the Tomb Raider", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 9947, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9948, - "name": "Shantae: Half-Genie Hero", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 9949, - "name": "Shoot Your Nightmare Halloween Special", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 9950, - "name": "Shovel Knight: Shovel of Hope", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9951, - "name": "Shrek 2: The Game", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9952, - "name": "Silence on The Line", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9953, - "name": "Silent Hill: Alchemilla", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9954, - "name": "Silverfall", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9955, - "name": "Silverfall: Earth Awakening", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9956, - "name": "Singularity", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9957, - "name": "Slap-Man", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9958, - "name": "Slay the Spire", - "site": "metacritic_com", - "genres": [ - "Turn-Based", - "Card Battle", - "Strategy" - ] - }, - { - "id": 9959, - "name": "Sleeping Dogs", - "site": "metacritic_com", - "genres": [ - "Modern", - "Open-World", - "Action Adventure" - ] - }, - { - "id": 9960, - "name": "Slender: The Arrival", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9961, - "name": "Slendytubbies", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9962, - "name": "Slime Rancher", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 9963, - "name": "Sniper Elite", - "site": "metacritic_com", - "genres": [ - "Historic", - "Tactical", - "Shooter", - "Third-Person", - "Action" - ] - }, - { - "id": 9964, - "name": "Sniper Elite 3", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9965, - "name": "Sniper Elite V2", - "site": "metacritic_com", - "genres": [ - "Historic", - "Tactical", - "Shooter", - "Third-Person", - "Action" - ] - }, - { - "id": 9966, - "name": "Sophie's Curse", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 9967, - "name": "South Park: The Fractured But Whole", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing" - ] - }, - { - "id": 9968, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9969, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9970, - "name": "South Park: The Stick of Truth", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing" - ] - }, - { - "id": 9971, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "metacritic_com", - "genres": [ - "General", - "Hidden Object", - "Puzzle", - "Adventure" - ] - }, - { - "id": 9972, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9973, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9974, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9975, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9976, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9977, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9978, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9979, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9980, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9981, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9982, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9983, - "name": "Spooky's House of Jump Scares", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9984, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 9985, - "name": "Star Wars: The Force Unleashed", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Third-Person", - "General", - "Action" - ] - }, - { - "id": 9986, - "name": "Star Wars: The Force Unleashed II", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 9987, - "name": "SteamWorld Dig", - "site": "metacritic_com", - "genres": [ - "Platformer", - "2D", - "Action Adventure", - "General", - "Action" - ] - }, - { - "id": 9988, - "name": "SteamWorld Dig 2", - "site": "metacritic_com", - "genres": [ - "Platformer", - "2D", - "Action Adventure", - "General", - "Action" - ] - }, - { - "id": 9989, - "name": "SteamWorld Heist", - "site": "metacritic_com", - "genres": [ - "Strategy", - "Turn-Based", - "General", - "Sci-Fi" - ] - }, - { - "id": 9990, - "name": "Steampunk Tower 2", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "Defense", - "Strategy" - ] - }, - { - "id": 9991, - "name": "Steel Rats", - "site": "metacritic_com", - "genres": [ - "Simulation", - "Combat", - "Vehicle" - ] - }, - { - "id": 9992, - "name": "Stories: The Path of Destinies", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9993, - "name": "Street Fighter X Tekken", - "site": "metacritic_com", - "genres": [ - "3D", - "2D", - "Action", - "Fighting" - ] - }, - { - "id": 9995, - "name": "Stupidella", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 9996, - "name": "Sudeki", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 9997, - "name": "Superhot", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Tactical", - "Action" - ] - }, - { - "id": 9998, - "name": "Surgeon Simulator", - "site": "metacritic_com", - "genres": [ - "Simulation", - "Virtual", - "Career", - "General", - "Action" - ] - }, - { - "id": 9999, - "name": "Tales from the Borderlands", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10000, - "name": "Tales of Berseria", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG", - "Japanese-Style" - ] - }, - { - "id": 10001, - "name": "Tales of Zestiria", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG", - "Japanese-Style" - ] - }, - { - "id": 10002, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10003, - "name": "Tasty Planet: Back for Seconds", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "General", - "Puzzle", - "Adventure" - ] - }, - { - "id": 10004, - "name": "Tekken 7", - "site": "metacritic_com", - "genres": [ - "3D", - "Action", - "Fighting" - ] - }, - { - "id": 10005, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 10006, - "name": "Tesla vs Lovecraft", - "site": "metacritic_com", - "genres": [ - "Shooter", - "Top-Down", - "Shoot-'Em-Up", - "Action" - ] - }, - { - "id": 10007, - "name": "Teslagrad", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 10008, - "name": "The Banner Saga", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Tactics", - "Turn-Based", - "Strategy", - "General" - ] - }, - { - "id": 10009, - "name": "The Banner Saga 2", - "site": "metacritic_com", - "genres": [ - "Tactics", - "Turn-Based", - "Strategy" - ] - }, - { - "id": 10010, - "name": "The Bard’s Tale (2004)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10011, - "name": "The Beginner's Guide", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 10012, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10013, - "name": "The Binding of Isaac: Rebirth", - "site": "metacritic_com", - "genres": [ - "Horror", - "Shoot-'Em-Up", - "Top-Down", - "Survival", - "Shooter", - "Action Adventure", - "Action" - ] - }, - { - "id": 10014, - "name": "The Bunker", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 10015, - "name": "The Cat Lady", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 10016, - "name": "The Council", - "site": "metacritic_com", - "genres": [ - "General", - "Adventure" - ] - }, - { - "id": 10017, - "name": "The Curse of Blackwater", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 10018, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10019, - "name": "The Darkness II", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 10020, - "name": "The Elder Scrolls IV: Oblivion", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 10021, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10022, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10023, - "name": "The Elder Scrolls V: Skyrim", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Western-Style" - ] - }, - { - "id": 10024, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10025, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10026, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10027, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10028, - "name": "The Evil Within", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 10029, - "name": "The Evil Within 2", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 10030, - "name": "The Evil Within: The Assignment (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10031, - "name": "The Evil Within: The Consequence (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10032, - "name": "The Evil Within: The Executioner (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10033, - "name": "The Expendabros", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10034, - "name": "The Final Station", - "site": "metacritic_com", - "genres": [ - "Linear", - "Action Adventure" - ] - }, - { - "id": 10035, - "name": "The First Tree", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "3D", - "Adventure" - ] - }, - { - "id": 10036, - "name": "The Forest", - "site": "metacritic_com", - "genres": [ - "Survival", - "Horror", - "Action Adventure" - ] - }, - { - "id": 10037, - "name": "The Hat Man Shadow Ward", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 10038, - "name": "The Incredible Adventures of Van Helsing", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10039, - "name": "The Incredible Adventures of Van Helsing II", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10040, - "name": "The Incredible Adventures of Van Helsing III", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10041, - "name": "The Last Remnant", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Japanese-Style", - "Console-style RPG" - ] - }, - { - "id": 10042, - "name": "The Mask Man", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10043, - "name": "The Punisher (2005)", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "Shooter", - "Modern", - "Action" - ] - }, - { - "id": 10044, - "name": "The Secret of Monkey Island: Special Edition", - "site": "metacritic_com", - "genres": [ - "General", - "Point-and-Click", - "Adventure" - ] - }, - { - "id": 10045, - "name": "The Slippery Slope", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10046, - "name": "The Stanley Parable", - "site": "metacritic_com", - "genres": [ - "Adventure", - "First-Person", - "3D", - "General", - "Modern", - "Action" - ] - }, - { - "id": 10047, - "name": "The Walking Dead: A New Frontier", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10048, - "name": "The Walking Dead: Michonne", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10049, - "name": "The Walking Dead: Season One", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10050, - "name": "The Walking Dead: Season Two", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10051, - "name": "The Witcher", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10052, - "name": "The Witcher 2: Assassins of Kings", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10053, - "name": "The Witcher 3: Wild Hunt", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10054, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10055, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10056, - "name": "The Wolf Among Us", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Adventure", - "General", - "Fantasy", - "Point-and-Click" - ] - }, - { - "id": 10057, - "name": "Thief", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 10058, - "name": "This Is The Police", - "site": "metacritic_com", - "genres": [ - "Simulation", - "Career", - "Virtual" - ] - }, - { - "id": 10059, - "name": "TimeShift", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Sci-Fi", - "Action" - ] - }, - { - "id": 10060, - "name": "Titan Quest", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10061, - "name": "Titan Quest Anniversary Edition", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10062, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10063, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10064, - "name": "Titan Souls", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 10065, - "name": "Titanfall 2", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 10066, - "name": "To The Moon", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10067, - "name": "Tomb Raider", - "site": "metacritic_com", - "genres": [ - "General", - "Linear", - "Modern", - "Action Adventure" - ] - }, - { - "id": 10068, - "name": "Torchlight", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10069, - "name": "Torchlight II", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10070, - "name": "Toren", - "site": "metacritic_com", - "genres": [ - "General", - "Fantasy", - "Action Adventure" - ] - }, - { - "id": 10071, - "name": "Tormentum: Dark Sorrow", - "site": "metacritic_com", - "genres": [ - "Adventure", - "Point-and-Click", - "Horror", - "Action Adventure" - ] - }, - { - "id": 10072, - "name": "Total Overdose", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10073, - "name": "Transistor", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10074, - "name": "Trine", - "site": "metacritic_com", - "genres": [ - "Platformer", - "2D", - "Action Adventure", - "General", - "Fantasy", - "Action" - ] - }, - { - "id": 10075, - "name": "Trine 2", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 10076, - "name": "Trine 3: The Artifacts of Power", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 10077, - "name": "Troll Tale", - "site": "metacritic_com", - "genres": [ - "General", - "Puzzle" - ] - }, - { - "id": 10078, - "name": "Trollface Quest", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10079, - "name": "Trollface Quest 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10080, - "name": "Trollface Quest 3", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10081, - "name": "Trollface Quest 4", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10082, - "name": "Trollface Quest 5", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10083, - "name": "Trollface Quest 6", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10084, - "name": "Trollface Quest 7", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10085, - "name": "Trollface Quest: Video Memes", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10086, - "name": "Twin Sector", - "site": "metacritic_com", - "genres": [ - "General", - "Linear", - "Modern", - "Action Adventure" - ] - }, - { - "id": 10087, - "name": "Two Worlds", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "General", - "Role-Playing" - ] - }, - { - "id": 10088, - "name": "Typical Nightmare", - "site": "metacritic_com", - "genres": [ - "First-Person", - "3D", - "Adventure" - ] - }, - { - "id": 10089, - "name": "Ultimate Epic Battle Simulator", - "site": "metacritic_com", - "genres": [ - "General", - "Strategy" - ] - }, - { - "id": 10090, - "name": "Ultra Street Fighter IV", - "site": "metacritic_com", - "genres": [ - "2D", - "Action", - "Fighting" - ] - }, - { - "id": 10091, - "name": "Undertale", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Japanese-Style" - ] - }, - { - "id": 10092, - "name": "Unfair Mario", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10093, - "name": "Unmechanical", - "site": "metacritic_com", - "genres": [ - "Platformer", - "2D", - "Puzzle", - "Miscellaneous", - "General", - "Action" - ] - }, - { - "id": 10094, - "name": "Unmechanical: Extended (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10095, - "name": "Until the last", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 10096, - "name": "Valiant Hearts: The Great War", - "site": "metacritic_com", - "genres": [ - "2D", - "Platformer", - "Action" - ] - }, - { - "id": 10097, - "name": "Vanish", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 10098, - "name": "Victor Vran", - "site": "metacritic_com", - "genres": [ - "General", - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10099, - "name": "Vikings: Wolves of Midgard", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10100, - "name": "Visage", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 10101, - "name": "Wanted: Weapons of Fate", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Shooter", - "Third-Person", - "Modern", - "Action" - ] - }, - { - "id": 10102, - "name": "Warcraft III: Reign of Chaos", - "site": "metacritic_com", - "genres": [ - "General", - "Real-Time", - "Fantasy", - "Strategy" - ] - }, - { - "id": 10103, - "name": "Warcraft III: The Frozen Throne", - "site": "metacritic_com", - "genres": [ - "General", - "Real-Time", - "Fantasy", - "Strategy" - ] - }, - { - "id": 10104, - "name": "Warhammer 40.000: Space Marine", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Role-Playing", - "Shooter", - "Action RPG", - "Sci-Fi", - "Third-Person", - "Action" - ] - }, - { - "id": 10105, - "name": "Warhammer: Chaosbane", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10106, - "name": "We Happy Few", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 10107, - "name": "Whack The Thief", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10108, - "name": "Whack Your Boss", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10109, - "name": "Whack Your Ex", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10110, - "name": "Whack Your Neighbour", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10111, - "name": "Whack Your Teacher", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10112, - "name": "What Remains of Edith Finch", - "site": "metacritic_com", - "genres": [ - "First-Person", - "General", - "3D", - "Adventure" - ] - }, - { - "id": 10113, - "name": "Will Rock", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Fantasy", - "Action" - ] - }, - { - "id": 10114, - "name": "Willy-Nilly Knight", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Tactics", - "Turn-Based", - "Action RPG", - "Strategy" - ] - }, - { - "id": 10115, - "name": "Wizard of Legend", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10116, - "name": "Wolfenstein", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10117, - "name": "Wolfenstein: The New Order", - "site": "metacritic_com", - "genres": [ - "Historic", - "Arcade", - "First-Person", - "Shooter", - "General", - "Action" - ] - }, - { - "id": 10118, - "name": "Wolfenstein: The Old Blood", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Shooter", - "Arcade", - "Action" - ] - }, - { - "id": 10119, - "name": "Woolfe - The Red Hood Diaries", - "site": "metacritic_com", - "genres": [ - "Survival", - "Action Adventure" - ] - }, - { - "id": 10120, - "name": "Worms 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10121, - "name": "X-Blades / Ониблэйд", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10122, - "name": "X-Morph: Defense", - "site": "metacritic_com", - "genres": [ - "Real-Time", - "Defense", - "Strategy" - ] - }, - { - "id": 10123, - "name": "Yandere School", - "site": "metacritic_com", - "genres": [ - "Third-Person", - "3D", - "Adventure" - ] - }, - { - "id": 10124, - "name": "Year Walk", - "site": "metacritic_com", - "genres": [ - "Adventure", - "Miscellaneous", - "First-Person", - "3D", - "General", - "Modern" - ] - }, - { - "id": 10125, - "name": "ZOMBI", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Survival", - "First-Person", - "Shooter", - "Action Adventure", - "Action" - ] - }, - { - "id": 10126, - "name": "Zero Escape: Zero Time Dilemma", - "site": "metacritic_com", - "genres": [ - "Visual Novel", - "Adventure" - ] - }, - { - "id": 10127, - "name": "Ziggurat", - "site": "metacritic_com", - "genres": [ - "Arcade", - "First-Person", - "Shooter", - "Fantasy", - "Action" - ] - }, - { - "id": 10128, - "name": "Zombie Driver HD", - "site": "metacritic_com", - "genres": [ - "General", - "Action" - ] - }, - { - "id": 10129, - "name": "Zombie Party", - "site": "metacritic_com", - "genres": [ - "Shooter", - "Top-Down", - "Shoot-'Em-Up", - "Action" - ] - }, - { - "id": 10130, - "name": "Zombie Vikings", - "site": "metacritic_com", - "genres": [ - "Beat-'Em-Up", - "2D", - "Action" - ] - }, - { - "id": 10131, - "name": "Zuma Deluxe", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "General", - "Matching", - "Puzzle" - ] - }, - { - "id": 10132, - "name": "Zuma's Revenge!", - "site": "metacritic_com", - "genres": [ - "Miscellaneous", - "Action", - "Puzzle" - ] - }, - { - "id": 10133, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10134, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10135, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10136, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10137, - "name": "Невероятные Приключения Трояна", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10138, - "name": "Невероятные Приключения Трояна 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10139, - "name": "Невероятные Приключения Трояна 3", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10140, - "name": "Предатор", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10141, - "name": "Стоп-Гоп", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10142, - "name": "Стоп-Гоп 2", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10143, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10144, - "name": "Darksiders Genesis", - "site": "gamebomb_ru", - "genres": [ - "Ролевые игры", - "Боевик/Экшн" - ] - }, - { - "id": 10145, - "name": "Darksiders Genesis", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Ролевая игра" - ] - }, - { - "id": 10146, - "name": "Darksiders Genesis", - "site": "gameguru_ru", - "genres": [ - "Слэшер", - "Экшен", - "Шутер" - ] - }, - { - "id": 10147, - "name": "Darksiders Genesis", - "site": "iwantgames_ru", - "genres": [ - "Экшен", - "Приключения", - "РПГ" - ] - }, - { - "id": 10148, - "name": "Darksiders Genesis", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Приключения", - "Экшены" - ] - }, - { - "id": 10149, - "name": "Darksiders Genesis", - "site": "gamer_info_com", - "genres": [ - "hack & slash" - ] - }, - { - "id": 10150, - "name": "Darksiders Genesis", - "site": "playground_ru", - "genres": [ - "Экшен", - "Вид сверху" - ] - }, - { - "id": 10151, - "name": "Darksiders Genesis", - "site": "stopgame_ru", - "genres": [ - "rpg" - ] - }, - { - "id": 10152, - "name": "Darksiders Genesis", - "site": "gamespot_com", - "genres": [ - "Role-Playing", - "Action" - ] - }, - { - "id": 10153, - "name": "Darksiders Genesis", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10154, - "name": "Darksiders Genesis", - "site": "metacritic_com", - "genres": [ - "Role-Playing", - "Action RPG" - ] - }, - { - "id": 10155, - "name": "Darksiders Genesis", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10156, - "name": "Darksiders Genesis", - "site": "spong_com", - "genres": [] - }, - { - "id": 10157, - "name": "Wolfenstein II: The New Colossus", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10158, - "name": "Wolfenstein II: The New Colossus", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10159, - "name": "Wolfenstein II: The New Colossus", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10160, - "name": "Wolfenstein II: The New Colossus", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 10161, - "name": "Wolfenstein II: The New Colossus", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн", - "Шутер от первого лица" - ] - }, - { - "id": 10162, - "name": "Wolfenstein II: The New Colossus", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10163, - "name": "Wolfenstein II: The New Colossus", - "site": "playground_ru", - "genres": [] - }, - { - "id": 10164, - "name": "Wolfenstein II: The New Colossus", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10165, - "name": "Wolfenstein II: The New Colossus", - "site": "gamespot_com", - "genres": [ - "3D", - "Action", - "First-Person", - "Shooter" - ] - }, - { - "id": 10166, - "name": "Wolfenstein II: The New Colossus", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 10167, - "name": "Wolfenstein II: The New Colossus", - "site": "spong_com", - "genres": [] - }, - { - "id": 10168, - "name": "Wolfenstein II: The New Colossus", - "site": "metacritic_com", - "genres": [ - "Arcade", - "Action", - "First-Person", - "Shooter" - ] - }, - { - "id": 10169, - "name": "Wolfenstein II: The New Colossus", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10170, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10171, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10172, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10173, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10174, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10175, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10176, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10177, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10178, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10179, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 10180, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 10181, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 10182, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 10183, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10184, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10185, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10186, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10187, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10188, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10189, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10190, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 10191, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10192, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10193, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 10194, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10195, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10196, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10197, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 10198, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10199, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10200, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10201, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10202, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10203, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 10204, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10205, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 10206, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 10207, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 10208, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 10209, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10210, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10211, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10212, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10213, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 10214, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 10215, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10216, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10217, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10218, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "playground_ru", - "genres": [] - }, - { - "id": 10219, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10220, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10221, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 10222, - "name": "Witch trainer", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10223, - "name": "Witch trainer", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10224, - "name": "Witch trainer", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 10225, - "name": "Witch trainer", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10226, - "name": "Witch trainer", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10227, - "name": "Witch trainer", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10228, - "name": "Witch trainer", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10229, - "name": "Witch trainer", - "site": "playground_ru", - "genres": [] - }, - { - "id": 10230, - "name": "Witch trainer", - "site": "spong_com", - "genres": [] - }, - { - "id": 10231, - "name": "Witch trainer", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10232, - "name": "Witch trainer", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10233, - "name": "Witch trainer", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 10234, - "name": "Witch trainer", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 10235, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10236, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 10237, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "gamebomb_ru", - "genres": [ - "Приключения", - "Боевик/Экшн" - ] - }, - { - "id": 10238, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 10239, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица", - "Боевик" - ] - }, - { - "id": 10240, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10241, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10242, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 10243, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10244, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "spong_com", - "genres": [ - "Beat 'Em Up" - ] - }, - { - "id": 10245, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10246, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10247, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Sci-Fi" - ] - }, - { - "id": 10248, - "name": "Control", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены", - "Приключения" - ] - }, - { - "id": 10249, - "name": "Crysis 3", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 10250, - "name": "Control", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 10251, - "name": "Control", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн" - ] - }, - { - "id": 10252, - "name": "Control", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 10253, - "name": "Control", - "site": "igromania_ru", - "genres": [ - "Боевик от третьего лица" - ] - }, - { - "id": 10254, - "name": "Control", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "От третьего лица", - "Экшен" - ] - }, - { - "id": 10255, - "name": "Crysis 3", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 10256, - "name": "Crysis 3", - "site": "gameguru_ru", - "genres": [ - "Шутер", - "Экшен" - ] - }, - { - "id": 10257, - "name": "Control", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10258, - "name": "Control", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10259, - "name": "Crysis 3", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 10260, - "name": "Control", - "site": "iwantgames_ru", - "genres": [ - "Приключения", - "Экшен" - ] - }, - { - "id": 10261, - "name": "Control", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10262, - "name": "Control", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10263, - "name": "Control", - "site": "metacritic_com", - "genres": [ - "General", - "Action Adventure" - ] - }, - { - "id": 10264, - "name": "Crysis 3", - "site": "playground_ru", - "genres": [ - "Открытый мир", - "Шутер", - "Экшен", - "От первого лица", - "Научная фантастика" - ] - }, - { - "id": 10265, - "name": "Control", - "site": "spong_com", - "genres": [] - }, - { - "id": 10266, - "name": "Crysis 3", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10267, - "name": "Crysis 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10268, - "name": "Crysis 3", - "site": "gamespot_com", - "genres": [ - "First-Person", - "3D", - "Action", - "Shooter" - ] - }, - { - "id": 10269, - "name": "Crysis 3", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10270, - "name": "Crysis 3", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица", - "Шутер", - "Многопользоватеский шутер", - "Боевик/Экшн" - ] - }, - { - "id": 10271, - "name": "Crysis 3", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10272, - "name": "Crysis 3", - "site": "metacritic_com", - "genres": [ - "Sci-Fi", - "Action", - "Arcade", - "First-Person", - "Shooter" - ] - }, - { - "id": 10273, - "name": "Crysis 3", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 10274, - "name": "Devil May Cry", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 10275, - "name": "Devil May Cry", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 10276, - "name": "Devil May Cry", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 10277, - "name": "Devil May Cry", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн", - "Шутер", - "Боевик-приключения" - ] - }, - { - "id": 10278, - "name": "Devil May Cry", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Боевик от третьего лица" - ] - }, - { - "id": 10279, - "name": "Devil May Cry", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10280, - "name": "Devil May Cry", - "site": "playground_ru", - "genres": [ - "Слэшер", - "Экшен", - "От третьего лица" - ] - }, - { - "id": 10281, - "name": "Devil May Cry", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10282, - "name": "Devil May Cry", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 10283, - "name": "Devil May Cry", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10284, - "name": "Devil May Cry", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 10285, - "name": "Devil May Cry", - "site": "gamespot_com", - "genres": [ - "Adventure", - "Action" - ] - }, - { - "id": 10286, - "name": "Devil May Cry", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Fantasy" - ] - }, - { - "id": 10287, - "name": "Devil May Cry 2", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10288, - "name": "Devil May Cry 2", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн", - "Шутер" - ] - }, - { - "id": 10289, - "name": "Devil May Cry 2", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Боевик от третьего лица" - ] - }, - { - "id": 10290, - "name": "Devil May Cry 2", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 10291, - "name": "Devil May Cry 2", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 10292, - "name": "Devil May Cry 2", - "site": "playground_ru", - "genres": [ - "Слэшер", - "Экшен", - "От третьего лица" - ] - }, - { - "id": 10293, - "name": "Devil May Cry 2", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10294, - "name": "Devil May Cry 2", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10295, - "name": "Devil May Cry 2", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10296, - "name": "Devil May Cry HD Collection", - "site": "gameguru_ru", - "genres": [ - "Слэшер", - "Экшен", - "Ремастер" - ] - }, - { - "id": 10297, - "name": "Devil May Cry 2", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 10298, - "name": "Devil May Cry 2", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 10299, - "name": "Devil May Cry 2", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10300, - "name": "Devil May Cry HD Collection", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Боевик от третьего лица" - ] - }, - { - "id": 10301, - "name": "Devil May Cry HD Collection", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн", - "Приключения" - ] - }, - { - "id": 10302, - "name": "Devil May Cry HD Collection", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10303, - "name": "Devil May Cry HD Collection", - "site": "playground_ru", - "genres": [ - "Слэшер", - "Экшен" - ] - }, - { - "id": 10304, - "name": "Devil May Cry HD Collection", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10305, - "name": "Devil May Cry HD Collection", - "site": "iwantgames_ru", - "genres": [ - "Аркады", - "Экшен", - "Ужасы" - ] - }, - { - "id": 10306, - "name": "Devil May Cry HD Collection", - "site": "spong_com", - "genres": [] - }, - { - "id": 10307, - "name": "Devil May Cry HD Collection", - "site": "ag_ru", - "genres": [ - "Приключения", - "Экшены" - ] - }, - { - "id": 10308, - "name": "Devil May Cry 2", - "site": "metacritic_com", - "genres": [ - "Action Adventure", - "Fantasy" - ] - }, - { - "id": 10309, - "name": "Devil May Cry HD Collection", - "site": "mobygames_com", - "genres": [ - "Compilation" - ] - }, - { - "id": 10310, - "name": "Devil May Cry HD Collection", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10311, - "name": "Devil May Cry HD Collection", - "site": "gamespot_com", - "genres": [ - "Compilation" - ] - }, - { - "id": 10312, - "name": "Devil May Cry HD Collection", - "site": "metacritic_com", - "genres": [ - "Compilation", - "Miscellaneous" - ] - }, - { - "id": 10313, - "name": "Terminator: Resistance", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 10314, - "name": "Terminator: Resistance", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн", - "Шутер от первого лица" - ] - }, - { - "id": 10315, - "name": "Terminator: Resistance", - "site": "ag_ru", - "genres": [ - "Экшены" - ] - }, - { - "id": 10316, - "name": "Terminator: Resistance", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 10317, - "name": "Terminator: Resistance", - "site": "iwantgames_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 10318, - "name": "Terminator: Resistance", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица" - ] - }, - { - "id": 10319, - "name": "Terminator: Resistance", - "site": "playground_ru", - "genres": [ - "Научная фантастика", - "Шутер", - "От первого лица" - ] - }, - { - "id": 10320, - "name": "Terminator: Resistance", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10321, - "name": "Terminator: Resistance", - "site": "spong_com", - "genres": [] - }, - { - "id": 10322, - "name": "Terminator: Resistance", - "site": "store_steampowered_com", - "genres": [ - "Action", - "RPG", - "Adventure" - ] - }, - { - "id": 10323, - "name": "Terminator: Resistance", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10324, - "name": "Terminator: Resistance", - "site": "gamespot_com", - "genres": [ - "Action", - "3D", - "First-Person", - "Shooter" - ] - }, - { - "id": 10325, - "name": "Terminator: Resistance", - "site": "metacritic_com", - "genres": [ - "Action", - "First-Person", - "Arcade", - "Shooter" - ] - }, - { - "id": 10326, - "name": "Kholat", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 10327, - "name": "Kholat", - "site": "gamer_info_com", - "genres": [ - "ужасы", - "инди" - ] - }, - { - "id": 10328, - "name": "Kholat", - "site": "gamebomb_ru", - "genres": [ - "Приключения" - ] - }, - { - "id": 10329, - "name": "Kholat", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 10330, - "name": "Kholat", - "site": "iwantgames_ru", - "genres": [ - "Ужасы", - "Приключения" - ] - }, - { - "id": 10331, - "name": "Kholat", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Ужасы" - ] - }, - { - "id": 10332, - "name": "Kholat", - "site": "playground_ru", - "genres": [ - "Адвенчура", - "Ужасы", - "Инди", - "Экшен", - "От первого лица" - ] - }, - { - "id": 10333, - "name": "Kholat", - "site": "spong_com", - "genres": [ - "Adventure: Survival Horror" - ] - }, - { - "id": 10334, - "name": "Kholat", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10335, - "name": "Kholat", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 10336, - "name": "Kholat", - "site": "mobygames_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10337, - "name": "Kholat", - "site": "gamespot_com", - "genres": [ - "3D", - "First-Person", - "Adventure" - ] - }, - { - "id": 10338, - "name": "Kholat", - "site": "metacritic_com", - "genres": [ - "3D", - "First-Person", - "Adventure" - ] - }, - { - "id": 10339, - "name": "The Old City: Leviathan", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10340, - "name": "The Old City: Leviathan", - "site": "gameguru_ru", - "genres": [ - "Квест" - ] - }, - { - "id": 10341, - "name": "The Old City: Leviathan", - "site": "ag_ru", - "genres": [ - "Инди", - "Приключения" - ] - }, - { - "id": 10342, - "name": "The Old City: Leviathan", - "site": "igromania_ru", - "genres": [ - "Приключение" - ] - }, - { - "id": 10343, - "name": "The Old City: Leviathan", - "site": "gamer_info_com", - "genres": [ - "приключения", - "инди" - ] - }, - { - "id": 10344, - "name": "The Old City: Leviathan", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10345, - "name": "The Old City: Leviathan", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 10346, - "name": "The Old City: Leviathan", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 10347, - "name": "The Old City: Leviathan", - "site": "spong_com", - "genres": [] - }, - { - "id": 10348, - "name": "The Old City: Leviathan", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Adventure" - ] - }, - { - "id": 10349, - "name": "The Old City: Leviathan", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 10350, - "name": "The Old City: Leviathan", - "site": "gamespot_com", - "genres": [ - "3D", - "First-Person", - "Adventure" - ] - }, - { - "id": 10351, - "name": "The Old City: Leviathan", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Adventure", - "Fantasy" - ] - }, - { - "id": 10352, - "name": "Nihilumbra", - "site": "gamebomb_ru", - "genres": [ - "Головоломки/Пазл", - "Платформер" - ] - }, - { - "id": 10353, - "name": "Nihilumbra", - "site": "gameguru_ru", - "genres": [ - "Платформер", - "Аркада", - "Инди" - ] - }, - { - "id": 10354, - "name": "Nihilumbra", - "site": "gamer_info_com", - "genres": [ - "логические", - "головоломки", - "платформеры", - "инди" - ] - }, - { - "id": 10355, - "name": "Nihilumbra", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10356, - "name": "Nihilumbra", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Платформер" - ] - }, - { - "id": 10357, - "name": "Nihilumbra", - "site": "ag_ru", - "genres": [ - "Инди", - "Экшены", - "Приключения", - "Головоломки", - "Казуальные" - ] - }, - { - "id": 10358, - "name": "Nihilumbra", - "site": "playground_ru", - "genres": [ - "Аркада" - ] - }, - { - "id": 10359, - "name": "Nihilumbra", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10360, - "name": "Nihilumbra", - "site": "spong_com", - "genres": [] - }, - { - "id": 10361, - "name": "Nihilumbra", - "site": "gamespot_com", - "genres": [ - "Platformer", - "Action", - "2D" - ] - }, - { - "id": 10362, - "name": "Nihilumbra", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10363, - "name": "Nihilumbra", - "site": "store_steampowered_com", - "genres": [ - "Casual", - "Indie", - "Adventure" - ] - }, - { - "id": 10364, - "name": "Nihilumbra", - "site": "metacritic_com", - "genres": [ - "Platformer", - "Action", - "2D" - ] - }, - { - "id": 10365, - "name": "Samorost 3", - "site": "gameguru_ru", - "genres": [ - "Квест", - "Point-and-click" - ] - }, - { - "id": 10366, - "name": "Samorost 3", - "site": "gamebomb_ru", - "genres": [ - "Головоломки/Пазл", - "Приключения" - ] - }, - { - "id": 10367, - "name": "Samorost 3", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10368, - "name": "Samorost 3", - "site": "gamer_info_com", - "genres": [ - "логические", - "головоломки", - "квесты" - ] - }, - { - "id": 10369, - "name": "Samorost 3", - "site": "igromania_ru", - "genres": [ - "Приключение", - "Логика" - ] - }, - { - "id": 10370, - "name": "Samorost 3", - "site": "playground_ru", - "genres": [ - "Адвенчура" - ] - }, - { - "id": 10371, - "name": "Samorost 3", - "site": "stopgame_ru", - "genres": [ - "adventure" - ] - }, - { - "id": 10372, - "name": "Samorost 3", - "site": "ag_ru", - "genres": [ - "Семейные", - "Инди", - "Приключения", - "Казуальные", - "Головоломки" - ] - }, - { - "id": 10373, - "name": "Samorost 3", - "site": "spong_com", - "genres": [] - }, - { - "id": 10374, - "name": "Samorost 3", - "site": "mobygames_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 10375, - "name": "Samorost 3", - "site": "gamespot_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 10376, - "name": "Samorost 3", - "site": "store_steampowered_com", - "genres": [ - "Indie", - "Casual", - "Adventure" - ] - }, - { - "id": 10377, - "name": "Samorost 3", - "site": "metacritic_com", - "genres": [ - "Point-and-Click", - "Adventure", - "General" - ] - }, - { - "id": 10378, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10379, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 10380, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10381, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "ag_ru", - "genres": [ - "Ролевые", - "Экшены", - "Шутеры" - ] - }, - { - "id": 10382, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10383, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "spong_com", - "genres": [] - }, - { - "id": 10384, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10385, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 10386, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10387, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "metacritic_com", - "genres": [] - }, - { - "id": 10388, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10389, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ролевая" - ] - }, - { - "id": 10390, - "name": "Unreal Tournament", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 10391, - "name": "Unreal Tournament", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 10392, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "mobygames_com", - "genres": [ - "DLC / add-on" - ] - }, - { - "id": 10393, - "name": "Unreal Tournament", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Боевик от первого лица" - ] - }, - { - "id": 10394, - "name": "Unreal Tournament", - "site": "ag_ru", - "genres": [ - "Экшены", - "Шутеры" - ] - }, - { - "id": 10395, - "name": "Unreal Tournament", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 10396, - "name": "Unreal Tournament", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 10397, - "name": "Unreal Tournament", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10398, - "name": "Unreal Tournament", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10399, - "name": "Unreal Tournament", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10400, - "name": "Unreal Tournament", - "site": "playground_ru", - "genres": [ - "Мультиплеер", - "Шутер", - "Космос", - "От первого лица", - "Научная фантастика", - "Экшен" - ] - }, - { - "id": 10401, - "name": "Unreal Tournament", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10402, - "name": "Unreal Tournament", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 10403, - "name": "Unreal Tournament", - "site": "metacritic_com", - "genres": [ - "Shooter", - "Sci-Fi", - "Action", - "Arcade", - "First-Person" - ] - }, - { - "id": 10404, - "name": "Prey", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 10405, - "name": "Prey", - "site": "gamer_info_com", - "genres": [ - "action" - ] - }, - { - "id": 10406, - "name": "Prey", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10407, - "name": "Prey", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10408, - "name": "Prey", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Боевик от первого лица" - ] - }, - { - "id": 10409, - "name": "Prey", - "site": "spong_com", - "genres": [] - }, - { - "id": 10410, - "name": "Prey", - "site": "playground_ru", - "genres": [ - "Экшен", - "Шутер", - "От первого лица" - ] - }, - { - "id": 10411, - "name": "Prey", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 10412, - "name": "Prey", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10413, - "name": "Prey", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10414, - "name": "Prey", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10415, - "name": "Prey", - "site": "gamespot_com", - "genres": [ - "3D", - "Shooter", - "First-Person", - "Action" - ] - }, - { - "id": 10416, - "name": "Prey", - "site": "metacritic_com", - "genres": [ - "General", - "Shooter", - "Action", - "Arcade", - "First-Person" - ] - }, - { - "id": 10417, - "name": "007: Quantum of Solace", - "site": "gamebomb_ru", - "genres": [] - }, - { - "id": 10418, - "name": "007: Quantum of Solace", - "site": "gameguru_ru", - "genres": [] - }, - { - "id": 10419, - "name": "007: Quantum of Solace", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10420, - "name": "007: Quantum of Solace", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10421, - "name": "007: Quantum of Solace", - "site": "igromania_ru", - "genres": [] - }, - { - "id": 10422, - "name": "007: Quantum of Solace", - "site": "playground_ru", - "genres": [] - }, - { - "id": 10423, - "name": "007: Quantum of Solace", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10424, - "name": "007: Quantum of Solace", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10425, - "name": "007: Quantum of Solace", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 10426, - "name": "007: Quantum of Solace", - "site": "spong_com", - "genres": [] - }, - { - "id": 10427, - "name": "007: Quantum of Solace", - "site": "gamespot_com", - "genres": [ - "Shooter", - "3D", - "First-Person", - "Action" - ] - }, - { - "id": 10428, - "name": "007: Quantum of Solace", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10429, - "name": "007: Quantum of Solace", - "site": "metacritic_com", - "genres": [ - "Modern", - "Arcade", - "First-Person", - "General", - "Shooter", - "Action" - ] - }, - { - "id": 10430, - "name": "Constantine", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 10431, - "name": "Constantine", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн", - "Приключения" - ] - }, - { - "id": 10432, - "name": "Constantine", - "site": "ag_ru", - "genres": [] - }, - { - "id": 10433, - "name": "Constantine", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 10434, - "name": "Constantine", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Боевик от третьего лица" - ] - }, - { - "id": 10435, - "name": "Constantine", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10436, - "name": "Constantine", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10437, - "name": "Constantine", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10438, - "name": "Constantine", - "site": "playground_ru", - "genres": [ - "Экшен", - "Ужасы", - "От третьего лица", - "Шутер" - ] - }, - { - "id": 10439, - "name": "Constantine", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10440, - "name": "Constantine", - "site": "gamespot_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10441, - "name": "Constantine", - "site": "metacritic_com", - "genres": [ - "Sci-Fi", - "Action Adventure" - ] - }, - { - "id": 10442, - "name": "Constantine", - "site": "spong_com", - "genres": [ - "Adventure" - ] - }, - { - "id": 10443, - "name": "You Are Empty", - "site": "gamebomb_ru", - "genres": [ - "Шутер", - "Боевик/Экшн" - ] - }, - { - "id": 10444, - "name": "You Are Empty", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 10445, - "name": "You Are Empty", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 10446, - "name": "You Are Empty", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 10447, - "name": "You Are Empty", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 10448, - "name": "You Are Empty", - "site": "store_steampowered_com", - "genres": [] - }, - { - "id": 10449, - "name": "You Are Empty", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 10450, - "name": "You Are Empty", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 10451, - "name": "You Are Empty", - "site": "playground_ru", - "genres": [ - "Экшен", - "Постапокалипсис", - "От первого лица" - ] - }, - { - "id": 10452, - "name": "You Are Empty", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 10453, - "name": "You Are Empty", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 10454, - "name": "You Are Empty", - "site": "gamespot_com", - "genres": [ - "First-Person", - "Action", - "3D", - "Shooter" - ] - }, - { - "id": 10455, - "name": "You Are Empty", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Historic", - "Shooter", - "Action", - "Arcade" - ] - }, - { - "id": 10456, - "name": "007: Quantum of Solace", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10457, - "name": "35MM", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Action" - ] - }, - { - "id": 10458, - "name": "60 Seconds!", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Simulation", - "Strategy" - ] - }, - { - "id": 10459, - "name": "A Bird Story", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10460, - "name": "A Plague Tale: Innocence", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10461, - "name": "A Story About My Uncle", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10462, - "name": "Adam Wolfe", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10463, - "name": "Alan Wake", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action Adventure" - ] - }, - { - "id": 10464, - "name": "Alan Wake's American Nightmare", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "TPS", - "Action" - ] - }, - { - "id": 10465, - "name": "Alice: Madness Returns", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Horror", - "Beat 'em up" - ] - }, - { - "id": 10466, - "name": "Alien Isolation: Crew Expendable (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10467, - "name": "Alien Isolation: Last Survivor (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10468, - "name": "Alien Rage - Unlimited", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10469, - "name": "Alien: Isolation", - "site": "squarefaction_ru", - "genres": [ - "Shooter", - "Survival Horror" - ] - }, - { - "id": 10470, - "name": "Aliens vs. Predator (2010)", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10471, - "name": "Aliens: Colonial Marines", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10472, - "name": "Aliens: Colonial Marines - Stasis Interrupted (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10473, - "name": "Alone in the Dark (2008)", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action Adventure" - ] - }, - { - "id": 10474, - "name": "Amnesia: A Machine for Pigs", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Action Adventure" - ] - }, - { - "id": 10475, - "name": "Amnesia: The Dark Descent", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Action Adventure" - ] - }, - { - "id": 10476, - "name": "Among The Sleep", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "First-Person" - ] - }, - { - "id": 10477, - "name": "Angry Birds", - "site": "squarefaction_ru", - "genres": [ - "Puzzle" - ] - }, - { - "id": 10478, - "name": "Angry Birds Rio", - "site": "squarefaction_ru", - "genres": [ - "Puzzle" - ] - }, - { - "id": 10479, - "name": "Angry Birds Seasons", - "site": "squarefaction_ru", - "genres": [ - "Puzzle" - ] - }, - { - "id": 10480, - "name": "Angry Birds Space", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Arcade" - ] - }, - { - "id": 10481, - "name": "Angry Birds Star Wars", - "site": "squarefaction_ru", - "genres": [ - "Puzzle" - ] - }, - { - "id": 10482, - "name": "Angry Birds Star Wars II", - "site": "squarefaction_ru", - "genres": [ - "Puzzle" - ] - }, - { - "id": 10483, - "name": "Armored Kitten", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10484, - "name": "Ashes", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10485, - "name": "Attack on Titan / A.O.T. Wings of Freedom", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10486, - "name": "BLACK ROSE", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10487, - "name": "Bad Dream: Bridge", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10488, - "name": "Bad Dream: Butcher", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10489, - "name": "Bad Dream: Coma", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10490, - "name": "Bad Dream: Cyclops", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10491, - "name": "Bad Dream: Graveyard", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10492, - "name": "Bad Dream: Hospital", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10493, - "name": "Bad Dream: Memories", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10494, - "name": "Baldur's Gate", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10495, - "name": "Bastion", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Action Adventure" - ] - }, - { - "id": 10496, - "name": "Batman: The Telltale Series", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie" - ] - }, - { - "id": 10497, - "name": "Battlefield 1", - "site": "squarefaction_ru", - "genres": [ - "Tactical FPS", - "FPS" - ] - }, - { - "id": 10498, - "name": "Battlefield 1942", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10499, - "name": "Battlefield 4", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10500, - "name": "Battlefield Hardline", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10501, - "name": "Battlefield V", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10502, - "name": "Battlefield Vietnam", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10503, - "name": "Bayonetta", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action" - ] - }, - { - "id": 10504, - "name": "Beholder", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Strategy" - ] - }, - { - "id": 10505, - "name": "Beholder (Beta)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10506, - "name": "Beholder: Blissful Sleep (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10507, - "name": "Ben and Ed", - "site": "squarefaction_ru", - "genres": [ - "Action" - ] - }, - { - "id": 10508, - "name": "Ben and Ed: Blood Party", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Platform Game", - "Action" - ] - }, - { - "id": 10509, - "name": "Besiege", - "site": "squarefaction_ru", - "genres": [ - "Puzzle" - ] - }, - { - "id": 10510, - "name": "Beyond Good and Evil", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10511, - "name": "Binary Domain", - "site": "squarefaction_ru", - "genres": [ - "Action" - ] - }, - { - "id": 10512, - "name": "BioShock", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10513, - "name": "BioShock 2", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10514, - "name": "BioShock Infinite", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10515, - "name": "BioShock Infinite: Burial at Sea - Episode One (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10516, - "name": "BioShock Infinite: Burial at Sea - Episode Two (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10517, - "name": "Bionic Commando", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Action" - ] - }, - { - "id": 10518, - "name": "Black Mesa", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10519, - "name": "BlackSite: Area 51", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10520, - "name": "Blades of Time", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Beat 'em up" - ] - }, - { - "id": 10521, - "name": "Blades of Time: Dismal Swamp", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10522, - "name": "Blood Omen 2: Legacy of Kain", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Action" - ] - }, - { - "id": 10523, - "name": "BloodRayne", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action", - "Third-Person" - ] - }, - { - "id": 10524, - "name": "BloodRayne 2", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action", - "Third-Person" - ] - }, - { - "id": 10525, - "name": "BloodRayne: Betrayal", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Beat 'em up" - ] - }, - { - "id": 10526, - "name": "Bloodbath kavkaz", - "site": "squarefaction_ru", - "genres": [ - "Shooter" - ] - }, - { - "id": 10527, - "name": "Bloodline: Линия крови", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10528, - "name": "Blue Estate", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10529, - "name": "Blues and Bullets: Episode 1", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10530, - "name": "Blues and Bullets: Episode 2 (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10531, - "name": "Boogeyman", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10532, - "name": "Borderlands", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "FPS" - ] - }, - { - "id": 10533, - "name": "Borderlands 2", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "FPS" - ] - }, - { - "id": 10534, - "name": "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10535, - "name": "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10536, - "name": "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10537, - "name": "Borderlands 2: Headhunter 3: Mercenary Day (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10538, - "name": "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10539, - "name": "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10540, - "name": "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10541, - "name": "Borderlands: Claptrap's New Robot Revolution (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10542, - "name": "Borderlands: Mad Moxxi's Underdome Riot (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10543, - "name": "Borderlands: The Pre-Sequel!", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10544, - "name": "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10545, - "name": "Borderlands: The Secret Armory of General Knoxx (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10546, - "name": "Borderlands: The Zombie Island of Dr. Ned (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10547, - "name": "Brink", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10548, - "name": "Broforce", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Action" - ] - }, - { - "id": 10549, - "name": "Broken Age", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Third-Person" - ] - }, - { - "id": 10550, - "name": "Brothers: A Tale Of Two Sons", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Third-Person" - ] - }, - { - "id": 10551, - "name": "Brutal Legend", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "RTS" - ] - }, - { - "id": 10552, - "name": "Buff Knight Advanced", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10553, - "name": "Bulb Boy", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10554, - "name": "Bulletstorm", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10555, - "name": "Call of Cthulhu (2018)", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Horror" - ] - }, - { - "id": 10556, - "name": "Call of Cthulhu: Dark Corners of the Earth", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10557, - "name": "Call of Duty", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10558, - "name": "Call of Duty 2", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10559, - "name": "Call of Duty 4: Modern Warfare", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10560, - "name": "Call of Duty: Advanced Warfare", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10561, - "name": "Call of Duty: Black Ops", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10562, - "name": "Call of Duty: Black Ops II", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10563, - "name": "Call of Duty: Black Ops III", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10564, - "name": "Call of Duty: Ghosts", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10565, - "name": "Call of Duty: Infinite Warfare", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10566, - "name": "Call of Duty: Modern Warfare 2", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10567, - "name": "Call of Duty: Modern Warfare 3", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10568, - "name": "Call of Duty: United Offensive", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10569, - "name": "Call of Duty: WWII", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10570, - "name": "Call of Duty: World at War", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10571, - "name": "Canabalt", - "site": "squarefaction_ru", - "genres": [ - "Action" - ] - }, - { - "id": 10572, - "name": "Castlevania: Lords of Shadow", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Slasher" - ] - }, - { - "id": 10573, - "name": "Castlevania: Lords of Shadow - Resurrection (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10574, - "name": "Castlevania: Lords of Shadow - Reverie (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10575, - "name": "Castlevania: Lords of Shadow 2", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10576, - "name": "Cat Mario", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10577, - "name": "Cat Quest", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "RPG" - ] - }, - { - "id": 10578, - "name": "Child Of Light", - "site": "squarefaction_ru", - "genres": [ - "RPG", - "Platform Game" - ] - }, - { - "id": 10579, - "name": "Children of Morta", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10580, - "name": "Choice Chamber", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10581, - "name": "Claire", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Action" - ] - }, - { - "id": 10582, - "name": "Clustertruck", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10583, - "name": "Cold Fear", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "TPS" - ] - }, - { - "id": 10584, - "name": "Constantine", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 10585, - "name": "Control", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10586, - "name": "Costume Quest", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 10587, - "name": "Costume Quest 2", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 10588, - "name": "Costume Quest: Grubbins on Ice Part", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10589, - "name": "Cross of the Dutchman", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10590, - "name": "Cry Of Fear", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10591, - "name": "Crysis", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10592, - "name": "Crysis 2", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action" - ] - }, - { - "id": 10593, - "name": "Crysis 3", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10594, - "name": "Crysis Warhead", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10595, - "name": "Cube Escape: Arles", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10596, - "name": "Cube Escape: Case 23", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10597, - "name": "Cube Escape: Harvey's Box", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10598, - "name": "Cube Escape: Seasons", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10599, - "name": "Cube Escape: The Lake", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10600, - "name": "Cube Escape: The Mill", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10601, - "name": "DOOM (2016)", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10602, - "name": "Dark Deception", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10603, - "name": "Dark Messiah of Might and Magic", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10604, - "name": "Dark Sector", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10605, - "name": "Dark Souls II", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10606, - "name": "Dark Souls II: Scholar of the First Sin", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10607, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10608, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10609, - "name": "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10610, - "name": "Dark Souls III", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10611, - "name": "Dark Souls III: Ashes of Ariandel (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10612, - "name": "Dark Souls III: The Ringed City (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10613, - "name": "Dark Souls: Prepare to Die Edition", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10614, - "name": "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10615, - "name": "Dark Souls: Remastered", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10616, - "name": "Dark Souls: Remastered - Artorias of the Abyss (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10617, - "name": "Darksiders", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10618, - "name": "Darksiders Genesis", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10619, - "name": "Darksiders II", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Action Adventure" - ] - }, - { - "id": 10620, - "name": "Darksiders III", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Slasher" - ] - }, - { - "id": 10621, - "name": "Daylight", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Horror" - ] - }, - { - "id": 10622, - "name": "Dead Island", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "FPS" - ] - }, - { - "id": 10623, - "name": "Dead Island: Riptide", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Action", - "Beat 'em up" - ] - }, - { - "id": 10624, - "name": "Dead Island: Ryder White", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10625, - "name": "Dead Rising", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Third-Person" - ] - }, - { - "id": 10626, - "name": "Dead Space", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "TPS", - "Action" - ] - }, - { - "id": 10627, - "name": "Dead Space 2", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "TPS" - ] - }, - { - "id": 10628, - "name": "Dead Space 3", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "TPS" - ] - }, - { - "id": 10629, - "name": "Dead Space 3: Awakened (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10630, - "name": "Dead by Daylight", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action" - ] - }, - { - "id": 10631, - "name": "Deadlight", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Platform Game", - "Action", - "Puzzle" - ] - }, - { - "id": 10632, - "name": "Deadpool", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 10633, - "name": "DeathSpank", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10634, - "name": "DeathSpank: Thongs of Virtue", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10635, - "name": "Deathtrap", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10636, - "name": "Demigod", - "site": "squarefaction_ru", - "genres": [ - "RTS", - "Action" - ] - }, - { - "id": 10637, - "name": "Descenders", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10638, - "name": "Detention", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Horror" - ] - }, - { - "id": 10639, - "name": "Devil May Cry", - "site": "squarefaction_ru", - "genres": [ - "Slasher" - ] - }, - { - "id": 10640, - "name": "Devil May Cry 2", - "site": "squarefaction_ru", - "genres": [ - "Slasher" - ] - }, - { - "id": 10641, - "name": "Devil May Cry 3: Dante’s Awakening", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action" - ] - }, - { - "id": 10642, - "name": "Devil May Cry 4", - "site": "squarefaction_ru", - "genres": [ - "Slasher" - ] - }, - { - "id": 10643, - "name": "Devil May Cry 5", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action" - ] - }, - { - "id": 10644, - "name": "Devil May Cry HD Collection", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10645, - "name": "Diablo II", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10646, - "name": "Diablo II: Lord of Destruction (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10647, - "name": "Diablo III", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10648, - "name": "Diablo III: Reaper of Souls (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10649, - "name": "Dig or Die", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10650, - "name": "Disciples II: Dark Prophecy", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 10651, - "name": "Disciples III: Renaissance", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 10652, - "name": "Dishonored", - "site": "squarefaction_ru", - "genres": [ - "Stealth", - "Action Adventure" - ] - }, - { - "id": 10653, - "name": "Dishonored: The Brigmore Witches (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10654, - "name": "Dishonored: The Knife of Dunwall (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10655, - "name": "Distraint", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "Horror" - ] - }, - { - "id": 10656, - "name": "Disturbed", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10657, - "name": "DmC: Devil May Cry", - "site": "squarefaction_ru", - "genres": [ - "Slasher" - ] - }, - { - "id": 10658, - "name": "DmC: Devil May Cry - Vergil's Downfall (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10659, - "name": "Doki Doki Literature Club", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Visual Novel" - ] - }, - { - "id": 10660, - "name": "Don't Knock Twice", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10661, - "name": "Doom 3", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10662, - "name": "Double Dragon Neon", - "site": "squarefaction_ru", - "genres": [ - "Beat 'em up", - "Action" - ] - }, - { - "id": 10663, - "name": "Down Stairs", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10664, - "name": "Downfall", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10665, - "name": "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10666, - "name": "Dragon Age II", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10667, - "name": "Dragon Age: Inquisition", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10668, - "name": "Dragon Age: Inquisition - Jaws of Hakkon (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10669, - "name": "Dragon Age: Inquisition - The Descent (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10670, - "name": "Dragon Age: Inquisition - Trespasser (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10671, - "name": "Dragon Age: Origins", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10672, - "name": "Dragon Age: Origins - Awakening", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10673, - "name": "Dragon Age: Origins - Leliana's Song (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10674, - "name": "Dragon Age: Origins - The Darkspawn Chronicles (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10675, - "name": "Dragon Age: Origins - The Golems of Amgarrak (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10676, - "name": "Dragon Age: Origins - Witch Hunt (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10677, - "name": "Dragon's Dogma: Dark Arisen", - "site": "squarefaction_ru", - "genres": [ - "Action", - "RPG" - ] - }, - { - "id": 10678, - "name": "Draw a Stickman: Episode 1", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10679, - "name": "Draw a Stickman: Episode 2", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10680, - "name": "DreadOut", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10681, - "name": "Dropsy", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10682, - "name": "Dude, Stop", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10683, - "name": "Duke Nukem Forever", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10684, - "name": "Dungeon Nightmares", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10685, - "name": "Dying Light", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action" - ] - }, - { - "id": 10686, - "name": "Dying Light: The Following (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10687, - "name": "Emily Wants To Play", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Adventure" - ] - }, - { - "id": 10688, - "name": "Escape Dead Island", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Third-Person" - ] - }, - { - "id": 10689, - "name": "Evil Defenders", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10690, - "name": "Evoland", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10691, - "name": "Evoland 2", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10692, - "name": "Eyes The Horror Game", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10693, - "name": "F.E.A.R.", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Horror" - ] - }, - { - "id": 10694, - "name": "F.E.A.R. 2: Project Origin", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10695, - "name": "F.E.A.R. 2: Reborn", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10696, - "name": "F.E.A.R. 3", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10697, - "name": "F.E.A.R. Extraction Point", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10698, - "name": "F.E.A.R. Perseus Mandate", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10699, - "name": "Fable (2004)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10700, - "name": "Fable III", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10701, - "name": "Fahrenheit", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 10702, - "name": "Failman", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10703, - "name": "Fallout 3", - "site": "squarefaction_ru", - "genres": [ - "First-Person", - "RPG" - ] - }, - { - "id": 10704, - "name": "Fallout 3 - Broken Steel (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10705, - "name": "Fallout 3 - Mothership Zeta (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10706, - "name": "Fallout 3 - Operation: Anchorage (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10707, - "name": "Fallout 3 - Point Lookout (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10708, - "name": "Fallout 3 - The Pitt (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10709, - "name": "Fallout 4", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10710, - "name": "Fallout: New Vegas", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10711, - "name": "Family Trouble", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10712, - "name": "Far Cry", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10713, - "name": "Far Cry 2", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10714, - "name": "Far Cry 3: Blood Dragon", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "FPS" - ] - }, - { - "id": 10715, - "name": "FarSky", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10716, - "name": "Final Fantasy III (Remake)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10717, - "name": "Final Fantasy IV (Remake)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10718, - "name": "Final Fantasy IV: The After Years (Remake)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10719, - "name": "Final Fantasy X HD Remaster", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10720, - "name": "Final Fantasy X HD Remaster: Eternal Calm (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10721, - "name": "Final Fantasy XIII", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 10722, - "name": "Final Fantasy XIII-2", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 10723, - "name": "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10724, - "name": "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10725, - "name": "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10726, - "name": "Final Fantasy XV", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10727, - "name": "Final Fantasy XV: Ardyn (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10728, - "name": "Final Fantasy XV: Comrades (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10729, - "name": "Final Fantasy XV: Episode Gladiolus (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10730, - "name": "Final Fantasy XV: Episode Ignis (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10731, - "name": "Final Fantasy XV: Episode Prompto (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10732, - "name": "Firewatch", - "site": "squarefaction_ru", - "genres": [ - "First-Person", - "Adventure" - ] - }, - { - "id": 10733, - "name": "First Winter", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10734, - "name": "Five Minutes", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10735, - "name": "Five Nights At Freddy's", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10736, - "name": "Five Nights At Freddy's 2", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10737, - "name": "Five Nights At Freddy's 3", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10738, - "name": "Five Nights At Freddy's 4", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10739, - "name": "Five Nights at Freddy's 3D", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10740, - "name": "Five Nights in Anime v3", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10741, - "name": "Fran Bow", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10742, - "name": "Front Mission Evolved", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 10743, - "name": "GUN", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10744, - "name": "Gemini Rue", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10745, - "name": "Geometry Dash", - "site": "squarefaction_ru", - "genres": [ - "Music", - "Platform Game", - "Action" - ] - }, - { - "id": 10746, - "name": "Ghost of a Tale", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Stealth", - "RPG" - ] - }, - { - "id": 10747, - "name": "Goat Simulator", - "site": "squarefaction_ru", - "genres": [ - "Simulation" - ] - }, - { - "id": 10748, - "name": "Grand Theft Auto III", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Sandbox", - "TPS" - ] - }, - { - "id": 10749, - "name": "Grand Theft Auto: San Andreas", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10750, - "name": "Grand Theft Auto: Vice City", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10751, - "name": "Grandpa", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10752, - "name": "Grey", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10753, - "name": "Grim Fandango Remastered", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10754, - "name": "Grow Home", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Sandbox" - ] - }, - { - "id": 10755, - "name": "Grow Up", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10756, - "name": "Guns, Gore & Cannoli", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Arcade" - ] - }, - { - "id": 10757, - "name": "Guns, Gore & Cannoli 2", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Arcade" - ] - }, - { - "id": 10758, - "name": "Half-Life", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10759, - "name": "Half-Life 2", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10760, - "name": "Half-Life 2: Episode One", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10761, - "name": "Half-Life 2: Episode Two", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10762, - "name": "Half-Life: Blue Shift", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10763, - "name": "Half-Life: Opposing Force", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10764, - "name": "Half-Life: Paranoia", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10765, - "name": "Half-Life: The Xeno Project", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10766, - "name": "Halloween Stories: Invitation (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10767, - "name": "Halo: Combat Evolved", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10768, - "name": "Hand of Fate", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10769, - "name": "Hand of Fate 2", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10770, - "name": "Happy Room", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10771, - "name": "Happy Wheels", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10772, - "name": "Hard Reset", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10773, - "name": "Hard Reset: Exile (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10774, - "name": "Harts", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10775, - "name": "Hatred", - "site": "squarefaction_ru", - "genres": [ - "Shooter", - "Action" - ] - }, - { - "id": 10776, - "name": "Hellblade: Senua's Sacrifice", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action Adventure", - "Third-Person" - ] - }, - { - "id": 10777, - "name": "Hellgate: London", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10778, - "name": "Her Story", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 10779, - "name": "Hero Academy", - "site": "squarefaction_ru", - "genres": [ - "Tactical RPG" - ] - }, - { - "id": 10780, - "name": "Heroes of Might and Magic III", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 10781, - "name": "Heroes of Might and Magic IV", - "site": "squarefaction_ru", - "genres": [ - "TBS", - "Tactical RPG" - ] - }, - { - "id": 10782, - "name": "Heroes of Might and Magic V", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 10783, - "name": "Heroes of Might and Magic V: Hammers of Fate", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 10784, - "name": "Heroes of Might and Magic V: Tribes of the East", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 10785, - "name": "Homefront", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10786, - "name": "Hotel Remorse", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10787, - "name": "Hotline Miami", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Stealth" - ] - }, - { - "id": 10788, - "name": "Hotline Miami 2: Wrong Number", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Stealth" - ] - }, - { - "id": 10789, - "name": "House Flipper", - "site": "squarefaction_ru", - "genres": [ - "Simulation" - ] - }, - { - "id": 10790, - "name": "I Am Alive", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action Adventure" - ] - }, - { - "id": 10791, - "name": "I am Setsuna", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 10792, - "name": "I hate this game", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10793, - "name": "INSIDE", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10794, - "name": "Indivisible", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10795, - "name": "Injustice: Gods Among Us", - "site": "squarefaction_ru", - "genres": [ - "Fighting" - ] - }, - { - "id": 10796, - "name": "Into the Breach", - "site": "squarefaction_ru", - "genres": [ - "Strategy", - "TBS", - "Simulation" - ] - }, - { - "id": 10797, - "name": "Jade Empire", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10798, - "name": "Jotun", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10799, - "name": "Just Cause", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Racing", - "Arcade" - ] - }, - { - "id": 10800, - "name": "Just Cause 2", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Driving", - "TPS" - ] - }, - { - "id": 10801, - "name": "Kane & Lynch 2: Dog Days", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 10802, - "name": "Kholat", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "First-Person", - "Adventure" - ] - }, - { - "id": 10803, - "name": "Killer is Dead: Nightmare Edition", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10804, - "name": "King's Bounty: Armored Princess", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10805, - "name": "King's Bounty: Crossworlds", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10806, - "name": "King's Bounty: Dark Side", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10807, - "name": "King's Bounty: The Legend", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10808, - "name": "King's Bounty: Warriors of the North", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10809, - "name": "King's Bounty: Warriors of the North - Ice and Fire (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10810, - "name": "Kingdom Rush", - "site": "squarefaction_ru", - "genres": [ - "Strategy", - "Tower Defense" - ] - }, - { - "id": 10811, - "name": "Kingdom: Classic", - "site": "squarefaction_ru", - "genres": [ - "Simulation", - "Strategy", - "Adventure", - "City Building", - "Tower Defense", - "Action" - ] - }, - { - "id": 10812, - "name": "Kingdoms of Amalur: Reckoning", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10813, - "name": "Knock-Knock", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10814, - "name": "Land of the Dead: Road to Fiddler's Green", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10815, - "name": "Lara Croft and the Guardian of Light", - "site": "squarefaction_ru", - "genres": [ - "Arcade" - ] - }, - { - "id": 10816, - "name": "Lara Croft and the Temple of Osiris", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Action" - ] - }, - { - "id": 10817, - "name": "Late Shift", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 10818, - "name": "Layers of Fear", - "site": "squarefaction_ru", - "genres": [ - "Horror" - ] - }, - { - "id": 10819, - "name": "Layers of Fear: Inheritance (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10820, - "name": "Left 4 Dead", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "FPS" - ] - }, - { - "id": 10821, - "name": "Left 4 Dead 2", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action", - "FPS" - ] - }, - { - "id": 10822, - "name": "Legend Hand of God", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10823, - "name": "Legend of Grimrock", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Adventure", - "RPG" - ] - }, - { - "id": 10824, - "name": "Legendary", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10825, - "name": "Life After Us: Shipwrecked", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10826, - "name": "Life Goes On: Done to Death", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10827, - "name": "Life Is Strange", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 10828, - "name": "Limbo", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Platform Game" - ] - }, - { - "id": 10829, - "name": "Little Nightmares", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10830, - "name": "Little Nightmares - The Depths (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10831, - "name": "Little Nightmares - The Hideaway (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10832, - "name": "Loki: Heroes of Mythology", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10833, - "name": "Lost Planet: Colonies", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10834, - "name": "Lost: Via Domus", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10835, - "name": "Love Is Strange (DEMO)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10836, - "name": "METAL SLUG", - "site": "squarefaction_ru", - "genres": [ - "Scrolling Shooter" - ] - }, - { - "id": 10837, - "name": "METAL SLUG 3", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Scrolling Shooter" - ] - }, - { - "id": 10838, - "name": "MINERVA: Metastasis", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10839, - "name": "MOTHERGUNSHIP", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10840, - "name": "Machinarium", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10841, - "name": "Magibot", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10842, - "name": "Magicka", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10843, - "name": "Magicka 2", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10844, - "name": "Manual Samuel", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10845, - "name": "Mario.EXE", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10846, - "name": "Marvel vs. Capcom: Infinite", - "site": "squarefaction_ru", - "genres": [ - "Fighting" - ] - }, - { - "id": 10847, - "name": "Masochisia", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10848, - "name": "Masters of Anima", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Strategy", - "RPG" - ] - }, - { - "id": 10849, - "name": "Max Payne", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 10850, - "name": "Max Payne 2: The Fall of Max Payne", - "site": "squarefaction_ru", - "genres": [ - "Action", - "TPS" - ] - }, - { - "id": 10851, - "name": "Max Payne 3", - "site": "squarefaction_ru", - "genres": [ - "Action", - "TPS" - ] - }, - { - "id": 10852, - "name": "Max: The Curse Of Brotherhood", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Platform Game" - ] - }, - { - "id": 10853, - "name": "McPixel", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Puzzle" - ] - }, - { - "id": 10854, - "name": "Metal Gear Rising: Revengeance", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10855, - "name": "Metal Wolf Chaos XD", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10856, - "name": "Metro 2033", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10857, - "name": "Metro 2033 Redux", - "site": "squarefaction_ru", - "genres": [ - "Shooter", - "Action" - ] - }, - { - "id": 10858, - "name": "Metro Exodus", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "FPS" - ] - }, - { - "id": 10859, - "name": "Metro Last Light", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10860, - "name": "Metro Last Light Redux", - "site": "squarefaction_ru", - "genres": [ - "Shooter", - "Action" - ] - }, - { - "id": 10861, - "name": "Metro Last Light Redux: Chronicles Pack (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10862, - "name": "Metro Last Light Redux: Developer Pack (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10863, - "name": "Metro Last Light Redux: Faction Pack (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10864, - "name": "Metro Last Light Redux: Tower Pack (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10865, - "name": "Middle-earth: Shadow of Mordor", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10866, - "name": "Might & Magic Heroes VI", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 10867, - "name": "Minecraft Story Mode", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10868, - "name": "Mini Ninjas", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Arcade", - "Adventure" - ] - }, - { - "id": 10869, - "name": "Mirror's Edge", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10870, - "name": "Monst", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10871, - "name": "Mortal Kombat IX", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10872, - "name": "Mortal Kombat X", - "site": "squarefaction_ru", - "genres": [ - "Fighting" - ] - }, - { - "id": 10873, - "name": "Mother Russia Bleeds", - "site": "squarefaction_ru", - "genres": [ - "Beat 'em up" - ] - }, - { - "id": 10874, - "name": "Mount Your Friends", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10875, - "name": "Mr. Robot", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10876, - "name": "Mr.President!", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10877, - "name": "Murdered: Soul Suspect", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10878, - "name": "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10879, - "name": "Naruto Shippuden: Ultimate Ninja Storm 4", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Fighting" - ] - }, - { - "id": 10880, - "name": "Naruto Shippuden: Ultimate Ninja Storm Revolution", - "site": "squarefaction_ru", - "genres": [ - "Fighting" - ] - }, - { - "id": 10881, - "name": "Need for Speed: Underground", - "site": "squarefaction_ru", - "genres": [ - "Racing" - ] - }, - { - "id": 10882, - "name": "Never Alone", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10883, - "name": "Neverending Nightmares", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10884, - "name": "Nevermind", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 10885, - "name": "Nevertales 8: The Abomination (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10886, - "name": "Neverwinter Nights", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10887, - "name": "Neverwinter Nights 2", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10888, - "name": "NieR: Automata", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10889, - "name": "Nightmare House 2", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "FPS" - ] - }, - { - "id": 10890, - "name": "Nihilumbra", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Platform Game", - "Adventure" - ] - }, - { - "id": 10891, - "name": "Nosferatu: The Wrath of Malachi", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "FPS" - ] - }, - { - "id": 10892, - "name": "Nox Timore", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10893, - "name": "Oceanhorn Monster of Uncharted Seas", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Puzzle" - ] - }, - { - "id": 10894, - "name": "Omensight", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10895, - "name": "One Hand Clapping (DEMO)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10896, - "name": "One Night at Flumpty's", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10897, - "name": "One Night at Flumpty's 2", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10898, - "name": "One Piece: Burning Blood", - "site": "squarefaction_ru", - "genres": [ - "Fighting" - ] - }, - { - "id": 10899, - "name": "Orcs Must Die!", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Action", - "Tower Defense" - ] - }, - { - "id": 10900, - "name": "Orcs Must Die! - Lost Adventures (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10901, - "name": "Ori and the Blind Forest", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10902, - "name": "Outlast", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10903, - "name": "Outlast: Whistleblower", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10904, - "name": "Overlord", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Action", - "TBS", - "Adventure" - ] - }, - { - "id": 10905, - "name": "Overlord: Raising Hell", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10906, - "name": "Oxenfree", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Adventure" - ] - }, - { - "id": 10907, - "name": "Painkiller", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10908, - "name": "Painkiller: Battle Out of Hell", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10909, - "name": "Painkiller: Overdose", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10910, - "name": "Paint the Town Red", - "site": "squarefaction_ru", - "genres": [ - "Action" - ] - }, - { - "id": 10911, - "name": "Panty Party", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10912, - "name": "Papers, Please", - "site": "squarefaction_ru", - "genres": [ - "Management", - "Text Game", - "Adventure" - ] - }, - { - "id": 10913, - "name": "Peace, Death!", - "site": "squarefaction_ru", - "genres": [ - "Simulation", - "Management" - ] - }, - { - "id": 10914, - "name": "Pillars of Eternity", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 10915, - "name": "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10916, - "name": "Pillars of Eternity: The White March Part I (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10917, - "name": "Pillars of Eternity: The White March Part II (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10918, - "name": "Pinstripe", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Adventure" - ] - }, - { - "id": 10919, - "name": "Pirates of the Caribbean: At World’s End", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action Adventure", - "Third-Person" - ] - }, - { - "id": 10920, - "name": "Pizza Delivery 2", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10921, - "name": "Plague Inc: Evolved", - "site": "squarefaction_ru", - "genres": [ - "Simulation" - ] - }, - { - "id": 10922, - "name": "Plants vs. Zombies", - "site": "squarefaction_ru", - "genres": [ - "Strategy", - "Tower Defense" - ] - }, - { - "id": 10923, - "name": "Plants vs. Zombies: Garden Warfare 2", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 10924, - "name": "Pony Island", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Horror" - ] - }, - { - "id": 10925, - "name": "Portal", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "FPS" - ] - }, - { - "id": 10926, - "name": "Portal 2", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "FPS" - ] - }, - { - "id": 10927, - "name": "Prey", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10928, - "name": "Prey (2006)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10929, - "name": "Prince of Persia (2008)", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10930, - "name": "Prince of Persia: The Two Thrones", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10931, - "name": "Prospekt", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10932, - "name": "Prototype", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10933, - "name": "Prototype 2", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10934, - "name": "Psychonauts", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Adventure" - ] - }, - { - "id": 10935, - "name": "PuniTy", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10936, - "name": "P·O·L·L·E·N", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 10937, - "name": "Quake 4", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10938, - "name": "Quantum Break", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "TPS" - ] - }, - { - "id": 10939, - "name": "Raft", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10940, - "name": "Rage", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 10941, - "name": "Rampage Knights", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10942, - "name": "Rayman Legends", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10943, - "name": "Rayman Origins", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 10944, - "name": "Rayman Raving Rabbids", - "site": "squarefaction_ru", - "genres": [ - "Party", - "Platform Game" - ] - }, - { - "id": 10945, - "name": "Rayman Raving Rabbids 2", - "site": "squarefaction_ru", - "genres": [ - "Party" - ] - }, - { - "id": 10946, - "name": "Real Horror Stories", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10947, - "name": "Redeemer", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 10948, - "name": "Reign Of Kings", - "site": "squarefaction_ru", - "genres": [ - "Sandbox", - "RPG" - ] - }, - { - "id": 10949, - "name": "Reigns", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10950, - "name": "Reigns: Her Majesty", - "site": "squarefaction_ru", - "genres": [ - "Card Game", - "Simulation" - ] - }, - { - "id": 10951, - "name": "Resident Evil", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10952, - "name": "Resident Evil / Biohazard HD REMASTER", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10953, - "name": "Resident Evil 0 / Biohazard 0 HD REMASTER", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10954, - "name": "Resident Evil 4", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action", - "TPS" - ] - }, - { - "id": 10955, - "name": "Resident Evil 5", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "TPS" - ] - }, - { - "id": 10956, - "name": "Resident Evil 5: Desperate Escape (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10957, - "name": "Resident Evil 5: Lost in Nightmares (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10958, - "name": "Resident Evil 6", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Action Adventure" - ] - }, - { - "id": 10959, - "name": "Resident Evil 7: Biohazard", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 10960, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10961, - "name": "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10962, - "name": "Resident Evil 7: Biohazard - End of Zoe (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10963, - "name": "Resident Evil 7: Biohazard - Not a Hero (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10964, - "name": "Resident Evil: Operation Raccoon City", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 10965, - "name": "Resident Evil: Revelations", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "TPS" - ] - }, - { - "id": 10966, - "name": "Resident Evil: Revelations 2", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Horror" - ] - }, - { - "id": 10967, - "name": "Reus", - "site": "squarefaction_ru", - "genres": [ - "Strategy", - "Simulation" - ] - }, - { - "id": 10968, - "name": "Rezrog", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10969, - "name": "Rise of the Tomb Raider", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10970, - "name": "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10971, - "name": "Rise of the Tomb Raider - Blood Ties (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10972, - "name": "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10973, - "name": "Rise of the Tomb Raider - Endurance Mode (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10974, - "name": "Risen", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10975, - "name": "Rock of Ages", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Arcade", - "TBS" - ] - }, - { - "id": 10976, - "name": "Rock of Ages 2: Bigger & Boulder", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10977, - "name": "Root Of Evil: The Tailor", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10978, - "name": "Runic Rampage", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10979, - "name": "Rust", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10980, - "name": "Ryse: Son of Rome", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 10981, - "name": "S.T.A.L.K.E.R.: Shadow of Chernobyl", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10982, - "name": "SCP-087", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Adventure" - ] - }, - { - "id": 10983, - "name": "SCP-087-B", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "Adventure" - ] - }, - { - "id": 10984, - "name": "SINNER: Sacrifice for Redemption", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10985, - "name": "SWAT 4", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 10986, - "name": "Sable Maze: Forbidden Garden (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10987, - "name": "Sable Maze: Nightmare Shadows (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10988, - "name": "Sable Maze: Norwich Caves (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10989, - "name": "Sable Maze: Sinister Knowledge (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10990, - "name": "Sable Maze: Soul Catcher (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10991, - "name": "Sable Maze: Sullivan River (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10992, - "name": "Sable Maze: Twelve Fears (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10993, - "name": "Sacred", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10994, - "name": "Sacred 3", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 10995, - "name": "Sacred Citadel", - "site": "squarefaction_ru", - "genres": [ - "Beat 'em up", - "Platform Game", - "Arcade", - "Action" - ] - }, - { - "id": 10996, - "name": "Saga of the Nine Worlds: The Four Stags (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10997, - "name": "Saga of the Nine Worlds: The Gathering (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10998, - "name": "Saga of the Nine Worlds: The Hunt (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 10999, - "name": "Saints Row IV", - "site": "squarefaction_ru", - "genres": [ - "Sandbox", - "TPS" - ] - }, - { - "id": 11000, - "name": "Saints Row: Gat out of Hell", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 11001, - "name": "Salt and Sanctuary", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11002, - "name": "Samorost 3", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 11003, - "name": "Samsara Room", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11004, - "name": "Saw: The Video Game", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 11005, - "name": "Scratches", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 11006, - "name": "Seasons after Fall", - "site": "squarefaction_ru", - "genres": [ - "Arcade", - "Puzzle", - "Platform Game" - ] - }, - { - "id": 11007, - "name": "Serena", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 11008, - "name": "Serious Sam 2", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11009, - "name": "Serious Sam 3: BFE", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11010, - "name": "Serious Sam 3: Jewel of the Nile (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11011, - "name": "Serious Sam Classic: The First Encounter", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11012, - "name": "Serious Sam Classic: The Second Encounter", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11013, - "name": "Shadow Warrior", - "site": "squarefaction_ru", - "genres": [ - "Shooter", - "First-Person" - ] - }, - { - "id": 11014, - "name": "Shadow of the Tomb Raider", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 11015, - "name": "Shadow of the Tomb Raider - The Forge (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11016, - "name": "Shantae: Half-Genie Hero", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 11017, - "name": "Shoot Your Nightmare Halloween Special", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11018, - "name": "Shovel Knight: Shovel of Hope", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 11019, - "name": "Shrek 2: The Game", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11020, - "name": "Silence on The Line", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11021, - "name": "Silent Hill: Alchemilla", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11022, - "name": "Silverfall", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11023, - "name": "Silverfall: Earth Awakening", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11024, - "name": "Singularity", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11025, - "name": "Slap-Man", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11026, - "name": "Slay the Spire", - "site": "squarefaction_ru", - "genres": [ - "Card Game", - "Strategy", - "Arcade", - "TBS" - ] - }, - { - "id": 11027, - "name": "Sleeping Dogs", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 11028, - "name": "Slender: The Arrival", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 11029, - "name": "Slendytubbies", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Horror" - ] - }, - { - "id": 11030, - "name": "Slime Rancher", - "site": "squarefaction_ru", - "genres": [ - "Action", - "First-Person", - "Sandbox" - ] - }, - { - "id": 11031, - "name": "Sniper Elite", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 11032, - "name": "Sniper Elite 3", - "site": "squarefaction_ru", - "genres": [ - "Action", - "TPS" - ] - }, - { - "id": 11033, - "name": "Sniper Elite V2", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 11034, - "name": "Sophie's Curse", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11035, - "name": "South Park: The Fractured But Whole", - "site": "squarefaction_ru", - "genres": [ - "Tactical RPG" - ] - }, - { - "id": 11036, - "name": "South Park: The Fractured But Whole - Bring The Crunch (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11037, - "name": "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11038, - "name": "South Park: The Stick of Truth", - "site": "squarefaction_ru", - "genres": [ - "RPG" - ] - }, - { - "id": 11039, - "name": "Spirits of Mystery: Amber Maiden (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11040, - "name": "Spirits of Mystery: Chains of Promise (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11041, - "name": "Spirits of Mystery: Family Lies (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11042, - "name": "Spirits of Mystery: Illusions (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11043, - "name": "Spirits of Mystery: Song of the Phoenix (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11044, - "name": "Spirits of Mystery: The Dark Minotaur (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11045, - "name": "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11046, - "name": "Spirits of Mystery: The Last Fire Queen (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11047, - "name": "Spirits of Mystery: The Lost Queen (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11048, - "name": "Spirits of Mystery: The Moon Crystal (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11049, - "name": "Spirits of Mystery: The Silver Arrow (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11050, - "name": "Spirits of Mystery: Whisper of the Past (Collector's Edition)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11051, - "name": "Spooky's House of Jump Scares", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11052, - "name": "Star Wars Jedi Knight: Jedi Academy", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS", - "TPS" - ] - }, - { - "id": 11053, - "name": "Star Wars: The Force Unleashed", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action" - ] - }, - { - "id": 11054, - "name": "Star Wars: The Force Unleashed II", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Action" - ] - }, - { - "id": 11055, - "name": "SteamWorld Dig", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Arcade" - ] - }, - { - "id": 11056, - "name": "SteamWorld Dig 2", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Arcade" - ] - }, - { - "id": 11057, - "name": "SteamWorld Heist", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 11058, - "name": "Steampunk Tower 2", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11059, - "name": "Steel Rats", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Racing", - "Arcade", - "Action" - ] - }, - { - "id": 11060, - "name": "Stories: The Path of Destinies", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11061, - "name": "Street Fighter X Tekken", - "site": "squarefaction_ru", - "genres": [ - "Fighting" - ] - }, - { - "id": 11062, - "name": "Stubbs the Zombie in Rebel Without a Pulse", - "site": "squarefaction_ru", - "genres": [ - "Action" - ] - }, - { - "id": 11063, - "name": "Stupidella", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11064, - "name": "Sudeki", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11065, - "name": "Superhot", - "site": "squarefaction_ru", - "genres": [ - "Tactical FPS" - ] - }, - { - "id": 11066, - "name": "Surgeon Simulator", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11067, - "name": "Tales from the Borderlands", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 11068, - "name": "Tales of Berseria", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 11069, - "name": "Tales of Zestiria", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 11070, - "name": "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11071, - "name": "Tasty Planet: Back for Seconds", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11072, - "name": "Tekken 7", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Fighting" - ] - }, - { - "id": 11073, - "name": "Terminator: Resistance", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11074, - "name": "Tesla Effect: A Tex Murphy Adventure", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11075, - "name": "Tesla vs Lovecraft", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11076, - "name": "Teslagrad", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Platform Game" - ] - }, - { - "id": 11077, - "name": "The Banner Saga", - "site": "squarefaction_ru", - "genres": [ - "Tactical RPG" - ] - }, - { - "id": 11078, - "name": "The Banner Saga 2", - "site": "squarefaction_ru", - "genres": [ - "Tactical RPG" - ] - }, - { - "id": 11079, - "name": "The Bard’s Tale (2004)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11080, - "name": "The Beginner's Guide", - "site": "squarefaction_ru", - "genres": [ - "First-Person", - "Adventure" - ] - }, - { - "id": 11081, - "name": "The Binding of Isaac: Afterbirth (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11082, - "name": "The Binding of Isaac: Rebirth", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Arcade" - ] - }, - { - "id": 11083, - "name": "The Bunker", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 11084, - "name": "The Cat Lady", - "site": "squarefaction_ru", - "genres": [ - "Horror", - "Adventure" - ] - }, - { - "id": 11085, - "name": "The Council", - "site": "squarefaction_ru", - "genres": [ - "Adventure", - "RPG" - ] - }, - { - "id": 11086, - "name": "The Curse of Blackwater", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11087, - "name": "The Dark Pictures Anthology: Man of Medan", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11088, - "name": "The Darkness II", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 11089, - "name": "The Elder Scrolls IV: Oblivion", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Sandbox" - ] - }, - { - "id": 11090, - "name": "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11091, - "name": "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11092, - "name": "The Elder Scrolls V: Skyrim", - "site": "squarefaction_ru", - "genres": [ - "Action RPG", - "Sandbox" - ] - }, - { - "id": 11093, - "name": "The Elder Scrolls V: Skyrim - Dawnguard (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11094, - "name": "The Elder Scrolls V: Skyrim - Dragonborn (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11095, - "name": "The Elder Scrolls V: Skyrim - Falskaar (MOD)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11096, - "name": "The Elder Scrolls V: Skyrim - Hearthfire (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11097, - "name": "The Evil Within", - "site": "squarefaction_ru", - "genres": [ - "TPS", - "First-Person", - "Stealth", - "Slasher", - "Survival Horror" - ] - }, - { - "id": 11098, - "name": "The Evil Within 2", - "site": "squarefaction_ru", - "genres": [ - "TPS", - "First-Person", - "Stealth", - "Slasher", - "Survival Horror" - ] - }, - { - "id": 11099, - "name": "The Evil Within: The Assignment (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11100, - "name": "The Evil Within: The Consequence (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11101, - "name": "The Evil Within: The Executioner (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11102, - "name": "The Expendabros", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11103, - "name": "The Final Station", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure" - ] - }, - { - "id": 11104, - "name": "The First Tree", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11105, - "name": "The Forest", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror" - ] - }, - { - "id": 11106, - "name": "The Hat Man Shadow Ward", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11107, - "name": "The Incredible Adventures of Van Helsing", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11108, - "name": "The Incredible Adventures of Van Helsing II", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11109, - "name": "The Incredible Adventures of Van Helsing III", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11110, - "name": "The Last Remnant", - "site": "squarefaction_ru", - "genres": [ - "JRPG" - ] - }, - { - "id": 11111, - "name": "The Mask Man", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11112, - "name": "The Old City: Leviathan", - "site": "squarefaction_ru", - "genres": [ - "First-Person", - "Adventure" - ] - }, - { - "id": 11113, - "name": "The Punisher (2005)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11114, - "name": "The Secret of Monkey Island: Special Edition", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11115, - "name": "The Slippery Slope", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11116, - "name": "The Stanley Parable", - "site": "squarefaction_ru", - "genres": [ - "First-Person", - "Adventure" - ] - }, - { - "id": 11117, - "name": "The Walking Dead: A New Frontier", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 11118, - "name": "The Walking Dead: Michonne", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 11119, - "name": "The Walking Dead: Season One", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11120, - "name": "The Walking Dead: Season Two", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Adventure" - ] - }, - { - "id": 11121, - "name": "The Witcher", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11122, - "name": "The Witcher 2: Assassins of Kings", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11123, - "name": "The Witcher 3: Wild Hunt", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11124, - "name": "The Witcher 3: Wild Hunt - Blood and Wine (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11125, - "name": "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11126, - "name": "The Wolf Among Us", - "site": "squarefaction_ru", - "genres": [ - "Interactive Movie", - "Action Adventure" - ] - }, - { - "id": 11127, - "name": "Thief", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Stealth" - ] - }, - { - "id": 11128, - "name": "This Is The Police", - "site": "squarefaction_ru", - "genres": [ - "Management", - "Simulation", - "Adventure" - ] - }, - { - "id": 11129, - "name": "TimeShift", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11130, - "name": "Titan Quest", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11131, - "name": "Titan Quest Anniversary Edition", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11132, - "name": "Titan Quest: Ragnarök (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11133, - "name": "Titan Quest: The Immortal Throne (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11134, - "name": "Titan Souls", - "site": "squarefaction_ru", - "genres": [ - "Action" - ] - }, - { - "id": 11135, - "name": "Titanfall 2", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11136, - "name": "To The Moon", - "site": "squarefaction_ru", - "genres": [ - "Adventure" - ] - }, - { - "id": 11137, - "name": "Tomb Raider", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Platform Game", - "Third-Person" - ] - }, - { - "id": 11138, - "name": "Torchlight", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11139, - "name": "Torchlight II", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11140, - "name": "Toren", - "site": "squarefaction_ru", - "genres": [ - "Action Adventure", - "Third-Person" - ] - }, - { - "id": 11141, - "name": "Tormentum: Dark Sorrow", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Adventure" - ] - }, - { - "id": 11142, - "name": "Total Overdose", - "site": "squarefaction_ru", - "genres": [ - "Action", - "TPS" - ] - }, - { - "id": 11143, - "name": "Transistor", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11144, - "name": "Trine", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 11145, - "name": "Trine 2", - "site": "squarefaction_ru", - "genres": [ - "Platform Game", - "Action" - ] - }, - { - "id": 11146, - "name": "Trine 3: The Artifacts of Power", - "site": "squarefaction_ru", - "genres": [ - "Platform Game" - ] - }, - { - "id": 11147, - "name": "Troll Tale", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11148, - "name": "Trollface Quest", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11149, - "name": "Trollface Quest 2", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11150, - "name": "Trollface Quest 3", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11151, - "name": "Trollface Quest 4", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11152, - "name": "Trollface Quest 5", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11153, - "name": "Trollface Quest 6", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11154, - "name": "Trollface Quest 7", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11155, - "name": "Trollface Quest: Video Memes", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Point&Click" - ] - }, - { - "id": 11156, - "name": "Twin Sector", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11157, - "name": "Two Worlds", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11158, - "name": "Typical Nightmare", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11159, - "name": "Ultimate Epic Battle Simulator", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11160, - "name": "Ultra Street Fighter IV", - "site": "squarefaction_ru", - "genres": [ - "Fighting" - ] - }, - { - "id": 11161, - "name": "Undertale", - "site": "squarefaction_ru", - "genres": [ - "JRPG", - "Adventure" - ] - }, - { - "id": 11162, - "name": "Unfair Mario", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11163, - "name": "Unmechanical", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Arcade" - ] - }, - { - "id": 11164, - "name": "Unmechanical: Extended (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11165, - "name": "Unreal Tournament", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11166, - "name": "Until the last", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11167, - "name": "Valiant Hearts: The Great War", - "site": "squarefaction_ru", - "genres": [ - "Puzzle", - "Arcade", - "Adventure" - ] - }, - { - "id": 11168, - "name": "Vanish", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11169, - "name": "Victor Vran", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11170, - "name": "Vikings: Wolves of Midgard", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11171, - "name": "Visage", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "First-Person" - ] - }, - { - "id": 11172, - "name": "Wanted: Weapons of Fate", - "site": "squarefaction_ru", - "genres": [ - "Action", - "TPS" - ] - }, - { - "id": 11173, - "name": "Warcraft III: Reign of Chaos", - "site": "squarefaction_ru", - "genres": [ - "RTS" - ] - }, - { - "id": 11174, - "name": "Warcraft III: The Frozen Throne", - "site": "squarefaction_ru", - "genres": [ - "RTS" - ] - }, - { - "id": 11175, - "name": "Warhammer 40.000: Space Marine", - "site": "squarefaction_ru", - "genres": [ - "TPS" - ] - }, - { - "id": 11176, - "name": "Warhammer: Chaosbane", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11177, - "name": "We Happy Few", - "site": "squarefaction_ru", - "genres": [ - "Action", - "First-Person" - ] - }, - { - "id": 11178, - "name": "Whack The Thief", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11179, - "name": "Whack Your Boss", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11180, - "name": "Whack Your Ex", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11181, - "name": "Whack Your Neighbour", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11182, - "name": "Whack Your Teacher", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11183, - "name": "What Remains of Edith Finch", - "site": "squarefaction_ru", - "genres": [ - "First-Person", - "Adventure" - ] - }, - { - "id": 11184, - "name": "Will Rock", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 11185, - "name": "Willy-Nilly Knight", - "site": "squarefaction_ru", - "genres": [ - "RPG", - "Tactical RPG" - ] - }, - { - "id": 11186, - "name": "Witch trainer", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11187, - "name": "Wizard of Legend", - "site": "squarefaction_ru", - "genres": [ - "Action RPG" - ] - }, - { - "id": 11188, - "name": "Wolfenstein", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11189, - "name": "Wolfenstein II: The New Colossus", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11190, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11191, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11192, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11193, - "name": "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11194, - "name": "Wolfenstein: The New Order", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11195, - "name": "Wolfenstein: The Old Blood", - "site": "squarefaction_ru", - "genres": [ - "Action", - "FPS" - ] - }, - { - "id": 11196, - "name": "Woolfe - The Red Hood Diaries", - "site": "squarefaction_ru", - "genres": [ - "Slasher", - "Platform Game" - ] - }, - { - "id": 11197, - "name": "Worms 2", - "site": "squarefaction_ru", - "genres": [ - "TBS" - ] - }, - { - "id": 11198, - "name": "X-Blades / Ониблэйд", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11199, - "name": "X-Morph: Defense", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11200, - "name": "Yandere School", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11201, - "name": "Year Walk", - "site": "squarefaction_ru", - "genres": [ - "First-Person", - "Adventure" - ] - }, - { - "id": 11202, - "name": "You Are Empty", - "site": "squarefaction_ru", - "genres": [ - "Survival Horror", - "FPS" - ] - }, - { - "id": 11203, - "name": "ZOMBI", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11204, - "name": "Zero Escape: Zero Time Dilemma", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11205, - "name": "Ziggurat", - "site": "squarefaction_ru", - "genres": [ - "FPS" - ] - }, - { - "id": 11206, - "name": "Zombie Driver HD", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11207, - "name": "Zombie Party", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11208, - "name": "Zombie Vikings", - "site": "squarefaction_ru", - "genres": [ - "Beat 'em up", - "Arcade" - ] - }, - { - "id": 11209, - "name": "Zuma Deluxe", - "site": "squarefaction_ru", - "genres": [ - "Puzzle" - ] - }, - { - "id": 11210, - "name": "Zuma's Revenge!", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11211, - "name": "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11212, - "name": "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11213, - "name": "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11214, - "name": "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11215, - "name": "Невероятные Приключения Трояна", - "site": "squarefaction_ru", - "genres": [ - "Visual Novel" - ] - }, - { - "id": 11216, - "name": "Невероятные Приключения Трояна 2", - "site": "squarefaction_ru", - "genres": [ - "Visual Novel" - ] - }, - { - "id": 11217, - "name": "Невероятные Приключения Трояна 3", - "site": "squarefaction_ru", - "genres": [ - "Visual Novel" - ] - }, - { - "id": 11218, - "name": "Предатор", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11219, - "name": "Стоп-Гоп", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11220, - "name": "Стоп-Гоп 2", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11221, - "name": "ШХД: ЗИМА / IT'S WINTER", - "site": "squarefaction_ru", - "genres": [] - }, - { - "id": 11222, - "name": "Rogue Warrior", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 11223, - "name": "Rogue Warrior", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 11224, - "name": "Rogue Warrior", - "site": "gamebomb_ru", - "genres": [ - "Боевик/Экшн", - "Шутер от первого лица" - ] - }, - { - "id": 11225, - "name": "Rogue Warrior", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 11226, - "name": "Rogue Warrior", - "site": "gamer_info_com", - "genres": [ - "FPS" - ] - }, - { - "id": 11227, - "name": "Rogue Warrior", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 11228, - "name": "Rogue Warrior", - "site": "squarefaction_ru", - "genres": [ - "FPS", - "Stealth" - ] - }, - { - "id": 11229, - "name": "Rogue Warrior", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 11230, - "name": "Rogue Warrior", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 11231, - "name": "Rogue Warrior", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up" - ] - }, - { - "id": 11232, - "name": "Rogue Warrior", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 11233, - "name": "Rogue Warrior", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 11234, - "name": "Rogue Warrior", - "site": "gamespot_com", - "genres": [] - }, - { - "id": 11235, - "name": "Rogue Warrior", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Arcade", - "Tactical", - "Shooter", - "Action", - "Modern" - ] - }, - { - "id": 11236, - "name": "Counter-Strike", - "site": "gameguru_ru", - "genres": [ - "Экшен", - "Шутер" - ] - }, - { - "id": 11237, - "name": "Counter-Strike", - "site": "gamebomb_ru", - "genres": [ - "Шутер от первого лица" - ] - }, - { - "id": 11238, - "name": "Counter-Strike", - "site": "ag_ru", - "genres": [ - "Шутеры", - "Экшены" - ] - }, - { - "id": 11239, - "name": "Counter-Strike", - "site": "gamer_info_com", - "genres": [] - }, - { - "id": 11240, - "name": "Counter-Strike", - "site": "squarefaction_ru", - "genres": [ - "Tactical FPS" - ] - }, - { - "id": 11241, - "name": "Counter-Strike", - "site": "igromania_ru", - "genres": [ - "Боевик от первого лица", - "Боевик" - ] - }, - { - "id": 11242, - "name": "Counter-Strike", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Strategy: Combat" - ] - }, - { - "id": 11243, - "name": "Counter-Strike", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 11244, - "name": "Counter-Strike", - "site": "mobygames_com", - "genres": [] - }, - { - "id": 11245, - "name": "Counter-Strike", - "site": "playground_ru", - "genres": [ - "Экшен", - "От первого лица", - "Мультиплеер", - "Тактика", - "Шутер" - ] - }, - { - "id": 11246, - "name": "Counter-Strike", - "site": "stopgame_ru", - "genres": [] - }, - { - "id": 11247, - "name": "Counter-Strike", - "site": "store_steampowered_com", - "genres": [ - "Action" - ] - }, - { - "id": 11248, - "name": "Counter-Strike", - "site": "gamespot_com", - "genres": [ - "Tactical", - "Shooter", - "Action", - "First-Person" - ] - }, - { - "id": 11249, - "name": "Counter-Strike", - "site": "metacritic_com", - "genres": [ - "First-Person", - "Tactical", - "Shooter", - "Action", - "Modern" - ] - }, - { - "id": 11250, - "name": "Dark Void", - "site": "gameguru_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 11251, - "name": "Dark Void", - "site": "ag_ru", - "genres": [ - "Приключения", - "Шутеры", - "Экшены" - ] - }, - { - "id": 11252, - "name": "Dark Void", - "site": "igromania_ru", - "genres": [ - "Боевик", - "Боевик от третьего лица" - ] - }, - { - "id": 11253, - "name": "Dark Void", - "site": "gamebomb_ru", - "genres": [ - "Боевик-приключения", - "Шутер" - ] - }, - { - "id": 11254, - "name": "Dark Void", - "site": "gamer_info_com", - "genres": [ - "action", - "шутеры от 3-го лица" - ] - }, - { - "id": 11255, - "name": "Dark Void", - "site": "iwantgames_ru", - "genres": [] - }, - { - "id": 11256, - "name": "Dark Void", - "site": "spong_com", - "genres": [ - "Shoot 'Em Up", - "Combat Game: Flying" - ] - }, - { - "id": 11257, - "name": "Dark Void", - "site": "squarefaction_ru", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 11258, - "name": "Dark Void", - "site": "stopgame_ru", - "genres": [ - "action" - ] - }, - { - "id": 11259, - "name": "Dark Void", - "site": "playground_ru", - "genres": [ - "Экшен" - ] - }, - { - "id": 11260, - "name": "Dark Void", - "site": "store_steampowered_com", - "genres": [ - "Action", - "Adventure" - ] - }, - { - "id": 11261, - "name": "Dark Void", - "site": "mobygames_com", - "genres": [ - "Action" - ] - }, - { - "id": 11262, - "name": "Dark Void", - "site": "gamespot_com", - "genres": [ - "Action" - ] - }, - { - "id": 11263, - "name": "Dark Void", - "site": "metacritic_com", - "genres": [ - "Action", - "General" - ] - } -] \ No newline at end of file diff --git a/html_parsing/get_game_genres/export_import/export.py b/html_parsing/get_game_genres/export_import/export.py deleted file mode 100644 index 0739d386f..000000000 --- a/html_parsing/get_game_genres/export_import/export.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import json -from pathlib import Path - -import sys -sys.path.append('..') - -from db import Dump - -from playhouse.shortcuts import model_to_dict - - -DIR = Path(__file__).parent.resolve() / 'data' -DIR.mkdir(parents=True, exist_ok=True) - -FILE_NAME_EXPORT_JSON = DIR / 'games.json' - - -if __name__ == '__main__': - items = [model_to_dict(dump) for dump in Dump.select()] - print(len(items)) - - json.dump( - items, - open(FILE_NAME_EXPORT_JSON, 'w', encoding='utf-8'), - ensure_ascii=False, - indent=4 - ) diff --git a/html_parsing/get_game_genres/export_import/import.py b/html_parsing/get_game_genres/export_import/import.py deleted file mode 100644 index f576e0e19..000000000 --- a/html_parsing/get_game_genres/export_import/import.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import json - -import sys -sys.path.append('..') - -from db import Dump - -import peewee -from playhouse.shortcuts import dict_to_model - -from export import FILE_NAME_EXPORT_JSON - - -items = json.load( - open(FILE_NAME_EXPORT_JSON, encoding='utf-8') -) -print('items:', len(items)) -print('Dump count before import:', Dump.select().count()) - -for x in items: - try: - dump = dict_to_model(Dump, x) - dump.save(force_insert=True) - print(f'Import {x}') - - except peewee.IntegrityError as e: - # Ignore error "UNIQUE constraint failed: dump.id" - pass - -print('Current dump count:', Dump.select().count()) diff --git a/html_parsing/get_game_genres/generate_games/game_by_genres.json b/html_parsing/get_game_genres/generate_games/game_by_genres.json deleted file mode 100644 index c42d6db4d..000000000 --- a/html_parsing/get_game_genres/generate_games/game_by_genres.json +++ /dev/null @@ -1,5094 +0,0 @@ -{ - "35MM": [ - "Action-adventure", - "FPS", - "Post apocalypse", - "Quest", - "Survival horror" - ], - "60 Seconds!": [ - "Action-adventure", - "Arcade", - "Casual", - "Post apocalypse", - "Quest", - "Simulation", - "Strategy", - "Survival" - ], - "A Bird Story": [ - "Action-adventure", - "Action/RPG", - "Retro" - ], - "A Plague Tale: Innocence": [ - "Action-adventure", - "Action/RPG", - "Stealth", - "Third-person" - ], - "A Story About My Uncle": [ - "Action-adventure", - "Arcade", - "Casual", - "Platformer", - "Puzzle" - ], - "Adam Wolfe": [ - "Adventure", - "Casual", - "Hidden Object", - "Puzzle" - ], - "Alan Wake": [ - "Action-adventure", - "Shooter", - "Survival horror", - "Third-person" - ], - "Alan Wake's American Nightmare": [ - "Action-adventure", - "Horror", - "Shooter", - "Survival horror", - "Third-person" - ], - "Alice: Madness Returns": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Survival horror" - ], - "Alien Isolation: Crew Expendable (DLC)": [ - "DLC" - ], - "Alien Isolation: Last Survivor (DLC)": [ - "DLC" - ], - "Alien Rage - Unlimited": [ - "Action", - "Shooter" - ], - "Alien: Isolation": [ - "Action-adventure", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Survival horror" - ], - "Aliens vs. Predator (2010)": [ - "Action", - "FPS" - ], - "Aliens: Colonial Marines": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Tactical" - ], - "Aliens: Colonial Marines - Stasis Interrupted (DLC)": [ - "DLC" - ], - "Alone in the Dark (2008)": [ - "Action-adventure", - "First-person", - "Survival horror", - "Third-person" - ], - "Amnesia: A Machine for Pigs": [ - "Action-adventure", - "First-person", - "Puzzle", - "Quest", - "Survival horror" - ], - "Amnesia: The Dark Descent": [ - "Action-adventure", - "First-person", - "Quest", - "Survival horror" - ], - "Among The Sleep": [ - "Action-adventure", - "First-person", - "Horror", - "Quest" - ], - "Angry Birds": [ - "Action", - "Arcade", - "Logic", - "Puzzle", - "Strategy" - ], - "Angry Birds Rio": [ - "Action", - "Arcade", - "Casual", - "Logic", - "Puzzle", - "Strategy", - "Tactic" - ], - "Angry Birds Seasons": [ - "Action", - "Arcade", - "Casual", - "Logic", - "Puzzle", - "Strategy", - "Tactic" - ], - "Angry Birds Space": [ - "Action", - "Arcade", - "Casual", - "Logic", - "Puzzle", - "Strategy", - "Tactic" - ], - "Angry Birds Star Wars": [ - "Action", - "Arcade", - "Casual", - "Logic", - "Puzzle", - "Strategy", - "Tactic" - ], - "Angry Birds Star Wars II": [ - "Action", - "Arcade", - "Casual", - "Logic", - "Puzzle", - "Strategy" - ], - "Armored Kitten": [ - "2D", - "Action/RPG", - "Arcade", - "Shoot 'em up", - "Shooter" - ], - "Ashes": [ - "Action-adventure", - "Arcade", - "Casual" - ], - "Attack on Titan / A.O.T. Wings of Freedom": [ - "Action" - ], - "BLACK ROSE": [ - "Adventure", - "First-person" - ], - "Bad Dream: Bridge": [], - "Bad Dream: Butcher": [ - "Adventure" - ], - "Bad Dream: Coma": [ - "Adventure", - "Arcade", - "Horror", - "Point-and-click" - ], - "Bad Dream: Cyclops": [], - "Bad Dream: Graveyard": [], - "Bad Dream: Hospital": [], - "Bad Dream: Memories": [], - "Baldur's Gate": [ - "Fantasy", - "RPG", - "Strategy" - ], - "Bastion": [ - "Action/RPG", - "Arcade", - "Logic", - "Platformer" - ], - "Batman: The Telltale Series": [ - "Adventure", - "Point-and-click" - ], - "Battlefield 1": [ - "Action", - "FPS", - "MMOFPS", - "Tactic" - ], - "Battlefield 1942": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up", - "Wargame" - ], - "Battlefield 4": [ - "Action", - "Arcade", - "FPS", - "MMOFPS", - "Shoot 'em up" - ], - "Battlefield Hardline": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up", - "Tactic" - ], - "Battlefield V": [ - "Action-adventure", - "FPS", - "MMO", - "MMOFPS", - "Tactic" - ], - "Battlefield Vietnam": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up", - "Wargame" - ], - "Bayonetta": [ - "Action-adventure", - "Shooter", - "Slasher", - "Third-person", - "Wargame" - ], - "Beholder": [ - "Adventure", - "Life simulation", - "Quest", - "RPG", - "Simulation", - "Strategy", - "Tactic" - ], - "Beholder (Beta)": [], - "Beholder: Blissful Sleep (DLC)": [ - "Add-on", - "Adventure", - "DLC", - "Quest" - ], - "Ben and Ed": [ - "Action-adventure", - "Arcade", - "Platformer" - ], - "Ben and Ed: Blood Party": [ - "Action", - "Arcade", - "Platformer" - ], - "Besiege": [ - "Action", - "Logic", - "RTS", - "Simulation", - "Strategy", - "Wargame" - ], - "Beyond Good and Evil": [ - "Action-adventure" - ], - "Binary Domain": [ - "Action", - "Action-adventure", - "Cyberpunk", - "Sci-fi", - "Shoot 'em up", - "Shooter", - "Tactic", - "Tactical", - "Third-person" - ], - "BioShock": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Steampunk" - ], - "BioShock 2": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Steampunk" - ], - "BioShock Infinite": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi", - "Steampunk" - ], - "BioShock Infinite: Burial at Sea - Episode One (DLC)": [ - "Action", - "Add-on", - "DLC", - "Shooter" - ], - "BioShock Infinite: Burial at Sea - Episode Two (DLC)": [ - "Action", - "DLC", - "Shooter" - ], - "Bionic Commando": [ - "2D", - "Action-adventure", - "Platformer", - "Quest", - "Sci-fi", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Black Mesa": [ - "Action-adventure", - "Arcade", - "FPS", - "Sci-fi" - ], - "BlackSite: Area 51": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Blades of Time": [ - "Action-adventure", - "Beat 'em up", - "Hack and slash", - "Slasher", - "Third-person" - ], - "Blades of Time: Dismal Swamp": [ - "Action", - "DLC" - ], - "Blood Omen 2: Legacy of Kain": [ - "Action-adventure", - "Action/RPG", - "Third-person" - ], - "BloodRayne": [ - "Action-adventure", - "Arcade", - "Fantasy", - "Horror", - "Shooter", - "Third-person" - ], - "BloodRayne 2": [ - "Action-adventure", - "Arcade", - "Fantasy", - "Shooter", - "Third-person" - ], - "BloodRayne: Betrayal": [ - "2D", - "Action-adventure", - "Arcade", - "Beat 'em up", - "Fantasy", - "Platformer", - "Shooter", - "Slasher", - "Third-person" - ], - "Bloodbath kavkaz": [ - "2D", - "Action-adventure", - "Shoot 'em up", - "Shooter" - ], - "Bloodline: Линия крови": [ - "Action", - "Shooter" - ], - "Blue Estate": [ - "Action", - "Arcade", - "FPS" - ], - "Blues and Bullets: Episode 1": [], - "Blues and Bullets: Episode 2 (DLC)": [], - "Boogeyman": [ - "Action-adventure", - "First-person", - "Strategy", - "Survival" - ], - "Borderlands": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Borderlands 2": [ - "Action/RPG", - "Arcade", - "FPS", - "Open-World", - "Racing", - "Sci-fi" - ], - "Borderlands 2: Captain Scarlett and her Pirate's Booty (DLC)": [ - "Action/RPG", - "DLC", - "Shooter" - ], - "Borderlands 2: Headhunter 1: Bloody Harvest (DLC)": [], - "Borderlands 2: Headhunter 2: Wattle Gobbler (DLC)": [], - "Borderlands 2: Headhunter 3: Mercenary Day (DLC)": [], - "Borderlands 2: Mr. Torgue’s Campaign of Carnage (DLC)": [ - "Action/RPG", - "DLC", - "Shooter" - ], - "Borderlands 2: Sir Hammerlock’s Big Game Hunt (DLC)": [ - "Action/RPG", - "DLC", - "Shooter" - ], - "Borderlands: Claptrap's New Robot Revolution (DLC)": [ - "Action/RPG", - "Add-on", - "DLC", - "Shooter" - ], - "Borderlands: Mad Moxxi's Underdome Riot (DLC)": [ - "Action", - "Add-on", - "DLC", - "Shooter" - ], - "Borderlands: The Pre-Sequel!": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Borderlands: The Pre-Sequel! - Claptastic Voyage and Ultimate Vault Hunter Upgrade Pack 2 (DLC)": [ - "DLC" - ], - "Borderlands: The Secret Armory of General Knoxx (DLC)": [ - "Action", - "Add-on", - "DLC", - "Shooter" - ], - "Borderlands: The Zombie Island of Dr. Ned (DLC)": [ - "Action", - "Add-on", - "DLC", - "Shooter" - ], - "Brink": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Broforce": [ - "2D", - "Action-adventure", - "Arcade", - "Casual", - "Platformer", - "Retro", - "Shooter" - ], - "Broken Age": [ - "Adventure", - "Casual", - "Point-and-click", - "Puzzle", - "Quest", - "Third-person" - ], - "Brothers: A Tale Of Two Sons": [ - "Action-adventure", - "Fantasy", - "Puzzle" - ], - "Brutal Legend": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Open-World", - "RTS", - "Slasher", - "Strategy", - "Tactic" - ], - "Buff Knight Advanced": [ - "Action/RPG", - "Arcade", - "Casual", - "Runner" - ], - "Bulb Boy": [ - "Adventure", - "Horror", - "Point-and-click", - "Puzzle", - "Quest" - ], - "Bulletstorm": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Call of Cthulhu (2018)": [], - "Call of Cthulhu: Dark Corners of the Earth": [ - "Action-adventure", - "FPS", - "Quest", - "Survival horror" - ], - "Call of Duty": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up", - "Simulation" - ], - "Call of Duty 2": [ - "Action", - "Arcade", - "FPS" - ], - "Call of Duty 4: Modern Warfare": [ - "Action", - "Arcade", - "FPS" - ], - "Call of Duty: Advanced Warfare": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Tactic" - ], - "Call of Duty: Black Ops": [ - "Action", - "Arcade", - "FPS", - "Robot War", - "Zombie" - ], - "Call of Duty: Black Ops II": [ - "Action", - "Arcade", - "FPS" - ], - "Call of Duty: Black Ops III": [ - "Action-adventure", - "Arcade", - "FPS", - "MMO" - ], - "Call of Duty: Ghosts": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up" - ], - "Call of Duty: Infinite Warfare": [ - "Action", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Tactic" - ], - "Call of Duty: Modern Warfare 2": [ - "Action", - "Arcade", - "FPS" - ], - "Call of Duty: Modern Warfare 3": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up" - ], - "Call of Duty: United Offensive": [ - "Action", - "Arcade", - "DLC", - "FPS", - "Shoot 'em up", - "Tactic", - "Third-person", - "Wargame" - ], - "Call of Duty: WWII": [ - "Action", - "Arcade", - "FPS", - "MMOFPS" - ], - "Call of Duty: World at War": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up" - ], - "Canabalt": [ - "2D", - "Action", - "Arcade", - "Casual", - "Platformer", - "Runner" - ], - "Castlevania: Lords of Shadow": [ - "Action-adventure", - "Fantasy", - "Quest", - "Slasher", - "Third-person" - ], - "Castlevania: Lords of Shadow - Resurrection (DLC)": [ - "Action", - "Quest" - ], - "Castlevania: Lords of Shadow - Reverie (DLC)": [ - "Action" - ], - "Castlevania: Lords of Shadow 2": [ - "Action-adventure", - "Fantasy", - "Quest", - "Slasher", - "Third-person" - ], - "Cat Mario": [], - "Cat Quest": [ - "Action-adventure", - "Action/RPG", - "Arcade" - ], - "Child Of Light": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "JRPG", - "Platformer" - ], - "Children of Morta": [ - "Action-adventure", - "Action/RPG", - "Retro" - ], - "Choice Chamber": [ - "Action", - "Arcade", - "MMO" - ], - "Claire": [ - "Action-adventure" - ], - "Clustertruck": [ - "Action", - "Arcade", - "First-person", - "Platformer", - "Racing", - "Runner" - ], - "Cold Fear": [ - "Action-adventure", - "Shooter", - "Survival horror", - "Third-person" - ], - "Costume Quest": [ - "Action-adventure", - "Action/RPG", - "Casual", - "Fantasy", - "Humor" - ], - "Costume Quest 2": [ - "Action-adventure", - "Action/RPG", - "Casual" - ], - "Costume Quest: Grubbins on Ice Part": [], - "Cross of the Dutchman": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Third-person" - ], - "Cry Of Fear": [ - "Action-adventure", - "FPS", - "Survival horror" - ], - "Crysis": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Crysis 2": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Crysis Warhead": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Cube Escape: Arles": [ - "Adventure", - "Logic", - "Puzzle" - ], - "Cube Escape: Case 23": [ - "Adventure", - "Logic", - "Puzzle" - ], - "Cube Escape: Harvey's Box": [ - "Adventure", - "Logic", - "Puzzle" - ], - "Cube Escape: Seasons": [ - "Adventure", - "Logic", - "Puzzle" - ], - "Cube Escape: The Lake": [ - "Adventure", - "Logic", - "Puzzle" - ], - "Cube Escape: The Mill": [ - "Adventure", - "Logic", - "Puzzle" - ], - "DOOM (2016)": [ - "Action", - "FPS", - "Horror" - ], - "Dark Deception": [ - "Action-adventure", - "Action/RPG" - ], - "Dark Messiah of Might and Magic": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Fantasy", - "Strategy" - ], - "Dark Sector": [ - "Action", - "Arcade", - "Cyberpunk", - "Sci-fi", - "Shoot 'em up", - "Shooter", - "TPS", - "Third-person" - ], - "Dark Souls II": [ - "Action/RPG" - ], - "Dark Souls II: Scholar of the First Sin": [ - "Action-adventure", - "Action/RPG" - ], - "Dark Souls II: Scholar of the First Sin - Crown of the Ivory King (DLC)": [], - "Dark Souls II: Scholar of the First Sin - Crown of the Old Iron King (DLC)": [], - "Dark Souls II: Scholar of the First Sin - Crown of the Sunken King (DLC)": [], - "Dark Souls III": [ - "Action/RPG" - ], - "Dark Souls III: Ashes of Ariandel (DLC)": [ - "Action/RPG", - "DLC" - ], - "Dark Souls III: The Ringed City (DLC)": [ - "Action/RPG", - "DLC" - ], - "Dark Souls: Prepare to Die Edition": [ - "Action-adventure", - "Action/RPG" - ], - "Dark Souls: Prepare to Die Edition - Artorias of the Abyss (DLC)": [], - "Dark Souls: Remastered": [ - "Action/RPG", - "Horror" - ], - "Dark Souls: Remastered - Artorias of the Abyss (DLC)": [], - "Darksiders": [ - "Action-adventure", - "Fantasy", - "Open-World", - "Slasher", - "Third-person" - ], - "Darksiders Genesis": [ - "Action-adventure", - "Action/RPG", - "Hack and slash", - "Shooter", - "Slasher" - ], - "Darksiders II": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Open-World" - ], - "Darksiders III": [ - "Action-adventure", - "Action/RPG", - "Open-World" - ], - "Daylight": [ - "Action-adventure", - "Casual", - "Quest", - "Survival horror" - ], - "Dead Island": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Open-World", - "Survival horror" - ], - "Dead Island: Riptide": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Racing", - "Survival horror" - ], - "Dead Island: Ryder White": [ - "Action", - "Action-adventure", - "DLC", - "Survival horror" - ], - "Dead Rising": [ - "Action-adventure", - "Open-World", - "Sandbox", - "Survival horror", - "Third-person", - "Zombie" - ], - "Dead Space": [ - "Action-adventure", - "Arcade", - "Horror", - "Sci-fi", - "Shooter", - "Space", - "Survival horror", - "Third-person" - ], - "Dead Space 2": [ - "Action-adventure", - "Arcade", - "Horror", - "Sci-fi", - "Shooter", - "Space", - "Survival horror", - "Third-person" - ], - "Dead Space 3": [ - "Action-adventure", - "Horror", - "Sci-fi", - "Shooter", - "Space", - "Survival horror", - "Third-person" - ], - "Dead Space 3: Awakened (DLC)": [ - "Action", - "DLC" - ], - "Dead by Daylight": [ - "Action-adventure", - "First-person", - "Survival horror", - "Third-person" - ], - "Deadlight": [ - "2D", - "Action-adventure", - "Arcade", - "Platformer", - "Survival horror", - "Zombie" - ], - "Deadpool": [ - "Action-adventure", - "Arcade", - "Beat 'em up", - "Humor", - "Shooter", - "Third-person" - ], - "DeathSpank": [ - "Action-adventure", - "Action/RPG", - "Fighting", - "Third-person" - ], - "DeathSpank: Thongs of Virtue": [ - "Action-adventure", - "Action/RPG", - "Puzzle", - "Racing", - "Third-person" - ], - "Deathtrap": [ - "Action-adventure", - "Action/RPG", - "RTS", - "Strategy", - "Tactic" - ], - "Demigod": [ - "Action-adventure", - "Action/RPG", - "Beat 'em up", - "Fantasy", - "MOBA", - "RTS", - "Strategy", - "Tactic" - ], - "Descenders": [ - "Action", - "Arcade", - "Cycle", - "Motorcycle", - "Racing", - "Simulation", - "Sport" - ], - "Detention": [ - "Adventure", - "Point-and-click", - "Puzzle", - "Quest", - "Survival horror" - ], - "Devil May Cry 3: Dante’s Awakening": [ - "Action-adventure", - "Shooter", - "Slasher", - "Third-person" - ], - "Devil May Cry 4": [ - "Action-adventure", - "Beat 'em up", - "Fantasy", - "Slasher", - "Third-person" - ], - "Devil May Cry 5": [ - "Action-adventure", - "Slasher", - "Third-person" - ], - "Diablo II": [ - "Action-adventure", - "Action/RPG", - "Hack and slash" - ], - "Diablo II: Lord of Destruction (DLC)": [ - "Adventure", - "DLC", - "RPG" - ], - "Diablo III": [ - "Action-adventure", - "Action/RPG", - "Hack and slash" - ], - "Diablo III: Reaper of Souls (DLC)": [ - "Action-adventure", - "Action/RPG", - "DLC" - ], - "Dig or Die": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Platformer", - "Sandbox", - "Strategy" - ], - "Disciples II: Dark Prophecy": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "Disciples III: Renaissance": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic", - "Wargame" - ], - "Dishonored": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Stealth", - "Steampunk" - ], - "Dishonored: The Brigmore Witches (DLC)": [ - "Action", - "DLC" - ], - "Dishonored: The Knife of Dunwall (DLC)": [ - "Action", - "DLC" - ], - "Distraint": [ - "Adventure", - "Arcade", - "Casual", - "Horror" - ], - "Disturbed": [ - "Adventure", - "Arcade", - "Casual", - "Horror", - "Point-and-click", - "Simulation", - "Visual Novel" - ], - "DmC: Devil May Cry": [ - "Action-adventure", - "Beat 'em up", - "Fantasy", - "Slasher", - "Third-person" - ], - "DmC: Devil May Cry - Vergil's Downfall (DLC)": [ - "Action", - "Add-on" - ], - "Doki Doki Literature Club": [ - "Adventure", - "Casual", - "Visual Novel" - ], - "Don't Knock Twice": [ - "Action-adventure", - "First-person", - "Horror" - ], - "Doom 3": [ - "Action", - "Arcade", - "FPS", - "Horror", - "Sci-fi" - ], - "Double Dragon Neon": [ - "2D", - "Action", - "Arcade", - "Beat 'em up", - "Fighting" - ], - "Down Stairs": [], - "Downfall": [ - "Adventure", - "Arcade", - "Logic", - "Point-and-click", - "Puzzle", - "Quest", - "Strategy", - "Tactic" - ], - "Dr Langeskov, The Tiger, and The Terribly Cursed Emerald: A Whirlwind Heist": [ - "Adventure", - "First-person", - "Quest" - ], - "Dragon Age II": [ - "Action-adventure", - "Action/RPG" - ], - "Dragon Age: Inquisition": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Third-person" - ], - "Dragon Age: Inquisition - Jaws of Hakkon (DLC)": [ - "Add-on", - "DLC", - "RPG" - ], - "Dragon Age: Inquisition - The Descent (DLC)": [ - "DLC" - ], - "Dragon Age: Inquisition - Trespasser (DLC)": [ - "Action/RPG", - "Add-on", - "DLC" - ], - "Dragon Age: Origins": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Tactic", - "Third-person", - "Wargame" - ], - "Dragon Age: Origins - Awakening": [ - "Adventure", - "DLC", - "RPG" - ], - "Dragon Age: Origins - Leliana's Song (DLC)": [ - "DLC" - ], - "Dragon Age: Origins - The Darkspawn Chronicles (DLC)": [ - "DLC" - ], - "Dragon Age: Origins - The Golems of Amgarrak (DLC)": [ - "DLC" - ], - "Dragon Age: Origins - Witch Hunt (DLC)": [ - "DLC" - ], - "Dragon's Dogma: Dark Arisen": [ - "Action-adventure", - "Action/RPG" - ], - "Draw a Stickman: Episode 1": [], - "Draw a Stickman: Episode 2": [ - "Adventure" - ], - "DreadOut": [ - "Action-adventure", - "Survival horror", - "Third-person" - ], - "Dropsy": [ - "Adventure", - "Arcade", - "Point-and-click", - "Puzzle", - "Quest" - ], - "Dude, Stop": [ - "Action-adventure", - "Arcade", - "Casual", - "Logic" - ], - "Duke Nukem Forever": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Third-person" - ], - "Dungeon Nightmares": [ - "Action-adventure", - "First-person" - ], - "Dying Light": [ - "Action-adventure", - "FPS", - "Open-World", - "Survival horror", - "Zombie" - ], - "Dying Light: The Following (DLC)": [ - "Action", - "Add-on", - "Open-World" - ], - "Emily Wants To Play": [ - "Action-adventure", - "Casual", - "First-person", - "Puzzle", - "Quest", - "Simulation", - "Strategy", - "Survival horror" - ], - "Escape Dead Island": [ - "Action-adventure", - "Survival horror", - "Third-person" - ], - "Evil Defenders": [ - "Adventure", - "Arcade", - "Casual", - "RTS", - "Strategy", - "Tactic", - "Tower Defence", - "Tower defense" - ], - "Evoland": [ - "Action-adventure", - "Action/RPG", - "JRPG", - "Retro" - ], - "Evoland 2": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Fighting", - "RTS" - ], - "Eyes The Horror Game": [ - "Adventure" - ], - "F.E.A.R.": [ - "Action", - "Arcade", - "FPS", - "Horror", - "Shoot 'em up", - "Wargame" - ], - "F.E.A.R. 2: Project Origin": [ - "Action-adventure", - "Arcade", - "FPS", - "Horror", - "Shoot 'em up", - "Survival horror" - ], - "F.E.A.R. 2: Reborn": [ - "Action", - "Arcade", - "DLC", - "FPS", - "Horror" - ], - "F.E.A.R. 3": [ - "Action", - "Arcade", - "FPS", - "Horror", - "Sci-fi" - ], - "F.E.A.R. Extraction Point": [ - "Action", - "Arcade", - "DLC", - "FPS", - "Horror", - "Shoot 'em up", - "Wargame" - ], - "F.E.A.R. Perseus Mandate": [ - "Action-adventure", - "Arcade", - "FPS", - "Horror", - "Survival horror" - ], - "Fable (2004)": [], - "Fable III": [ - "Action-adventure", - "Action/RPG" - ], - "Fahrenheit": [ - "Action-adventure", - "Interactive movie", - "Quest" - ], - "Failman": [], - "Fallout 3": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Open-World", - "Post apocalypse", - "Third-person" - ], - "Fallout 3 - Broken Steel (DLC)": [ - "Action/RPG", - "Add-on", - "DLC" - ], - "Fallout 3 - Mothership Zeta (DLC)": [ - "Action/RPG", - "DLC" - ], - "Fallout 3 - Operation: Anchorage (DLC)": [ - "Action/RPG", - "Add-on", - "DLC", - "Shooter" - ], - "Fallout 3 - Point Lookout (DLC)": [ - "Action/RPG", - "Add-on", - "DLC" - ], - "Fallout 3 - The Pitt (DLC)": [ - "Action/RPG", - "Add-on", - "DLC" - ], - "Fallout 4": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Open-World", - "Post apocalypse", - "Third-person" - ], - "Fallout: New Vegas": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Open-World", - "Post apocalypse", - "Third-person" - ], - "Family Trouble": [ - "Adventure" - ], - "Far Cry": [ - "Action-adventure", - "FPS", - "Open-World", - "Robot War", - "Sandbox", - "Shoot 'em up" - ], - "Far Cry 2": [ - "Action", - "Action-adventure", - "Arcade", - "FPS", - "Open-World", - "Sandbox", - "Shoot 'em up" - ], - "Far Cry 3: Blood Dragon": [ - "Action-adventure", - "Add-on", - "Arcade", - "FPS", - "Open-World", - "Sandbox", - "Shoot 'em up" - ], - "FarSky": [ - "Action-adventure", - "Survival" - ], - "Final Fantasy III (Remake)": [], - "Final Fantasy IV (Remake)": [], - "Final Fantasy IV: The After Years (Remake)": [], - "Final Fantasy X HD Remaster": [ - "JRPG", - "RPG" - ], - "Final Fantasy X HD Remaster: Eternal Calm (DLC)": [], - "Final Fantasy XIII": [ - "Adventure", - "JRPG", - "RPG" - ], - "Final Fantasy XIII-2": [ - "Adventure", - "JRPG", - "RPG" - ], - "Final Fantasy XIII-2 - Lightning's Story: Requiem of the Goddess (DLC)": [], - "Final Fantasy XIII-2 - Sazh's Story: Heads or Tails? (DLC)": [], - "Final Fantasy XIII-2 - Snow's Story: Perpetual Battlefield (DLC)": [], - "Final Fantasy XV": [ - "Action-adventure", - "Action/RPG", - "JRPG" - ], - "Final Fantasy XV: Ardyn (DLC)": [], - "Final Fantasy XV: Comrades (DLC)": [], - "Final Fantasy XV: Episode Gladiolus (DLC)": [ - "Action-adventure", - "Action/RPG" - ], - "Final Fantasy XV: Episode Ignis (DLC)": [ - "Action-adventure", - "Action/RPG" - ], - "Final Fantasy XV: Episode Prompto (DLC)": [ - "Action-adventure", - "Action/RPG", - "Shooter" - ], - "Firewatch": [ - "Adventure", - "First-person", - "Logic", - "Puzzle", - "Quest", - "Simulation" - ], - "First Winter": [ - "Adventure", - "First-person" - ], - "Five Minutes": [ - "Puzzle" - ], - "Five Nights At Freddy's": [ - "Action-adventure", - "First-person", - "Quest", - "Simulation", - "Sport", - "Strategy", - "Survival horror" - ], - "Five Nights At Freddy's 2": [ - "Action-adventure", - "Arcade", - "First-person", - "Quest", - "Simulation", - "Strategy", - "Survival horror" - ], - "Five Nights At Freddy's 3": [ - "Action-adventure", - "Arcade", - "First-person", - "Horror", - "Quest", - "Simulation" - ], - "Five Nights At Freddy's 4": [ - "Action-adventure", - "First-person", - "Quest", - "Strategy", - "Survival horror" - ], - "Five Nights at Freddy's 3D": [], - "Five Nights in Anime v3": [], - "Fran Bow": [ - "Adventure", - "Horror", - "Platformer", - "Point-and-click", - "Quest" - ], - "Front Mission Evolved": [ - "Action", - "Action-adventure", - "Arcade", - "Robot War", - "Sci-fi", - "Shoot 'em up", - "Shooter", - "Simulation", - "Third-person" - ], - "GUN": [ - "Action-adventure", - "FPS", - "Quest", - "Racing", - "Third-person", - "Western" - ], - "Gemini Rue": [ - "Adventure", - "Point-and-click", - "Quest", - "Sci-fi", - "Third-person" - ], - "Geometry Dash": [ - "Action", - "Arcade", - "Casual", - "Music", - "Music/Rhythm" - ], - "Ghost of a Tale": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Third-person" - ], - "Goat Simulator": [ - "Action-adventure", - "Arcade", - "Casual", - "Humor", - "Open-World", - "Sandbox", - "Simulation" - ], - "Grand Theft Auto III": [ - "Action-adventure", - "Racing", - "Shooter", - "Simulation", - "Third-person" - ], - "Grand Theft Auto: San Andreas": [ - "Action-adventure", - "Arcade", - "Open-World", - "Racing", - "Sandbox", - "Shooter", - "Simulation", - "TPS", - "Third-person", - "Wargame" - ], - "Grand Theft Auto: Vice City": [ - "Action-adventure", - "Arcade", - "Open-World", - "Racing", - "Sandbox", - "Shooter", - "Simulation", - "TPS", - "Third-person", - "Wargame" - ], - "Grandpa": [ - "Action-adventure", - "Survival" - ], - "Grey": [ - "Action" - ], - "Grim Fandango Remastered": [ - "Action-adventure", - "Fantasy", - "Puzzle", - "Quest", - "Third-person" - ], - "Grow Home": [ - "Action-adventure", - "Arcade", - "Casual", - "Open-World", - "Platformer", - "Quest", - "Sandbox" - ], - "Grow Up": [ - "Action-adventure", - "Arcade", - "Casual", - "Logic", - "Open-World", - "Platformer", - "Quest", - "Third-person" - ], - "Guns, Gore & Cannoli": [ - "Action", - "Arcade", - "Platformer", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Guns, Gore & Cannoli 2": [ - "Action", - "Arcade", - "Platformer", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Half-Life": [ - "Action", - "Action-adventure", - "Arcade", - "FPS", - "Puzzle", - "Sci-fi", - "Shoot 'em up" - ], - "Half-Life 2": [ - "Action-adventure", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Half-Life 2: Episode One": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Half-Life 2: Episode Two": [ - "Action", - "Arcade", - "FPS", - "Logic", - "Sci-fi", - "Shoot 'em up" - ], - "Half-Life: Blue Shift": [ - "Action", - "Arcade", - "FPS", - "Puzzle", - "Sci-fi", - "Shoot 'em up" - ], - "Half-Life: Opposing Force": [ - "Action", - "Arcade", - "DLC", - "FPS", - "Sci-fi" - ], - "Half-Life: Paranoia": [], - "Half-Life: The Xeno Project": [], - "Halloween Stories: Invitation (Collector's Edition)": [], - "Halo: Combat Evolved": [ - "Action-adventure", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Hand of Fate": [ - "Action-adventure", - "Action/RPG", - "Board game", - "Cardgame", - "Logic", - "Strategy" - ], - "Hand of Fate 2": [ - "Action-adventure", - "Action/RPG", - "Board game", - "Cardgame", - "Logic", - "Strategy", - "Wargame" - ], - "Happy Room": [ - "Action", - "Arcade", - "Simulation" - ], - "Happy Wheels": [ - "2D", - "Action", - "Arcade", - "Platformer", - "Racing" - ], - "Hard Reset": [ - "Action", - "Arcade", - "Cyberpunk", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Hard Reset: Exile (DLC)": [ - "DLC" - ], - "Harts": [ - "Adventure", - "First-person" - ], - "Hatred": [ - "2D", - "Action", - "Arcade", - "Open-World", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Hellblade: Senua's Sacrifice": [ - "Action-adventure", - "Fantasy", - "Open-World", - "Slasher", - "Third-person" - ], - "Hellgate: London": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Fantasy", - "MMORPG", - "Survival horror", - "Third-person" - ], - "Her Story": [ - "Adventure", - "Logic", - "Puzzle", - "Quest", - "Simulation", - "Visual Novel" - ], - "Hero Academy": [ - "Board game", - "Fantasy", - "Strategy", - "TBS", - "Tactic" - ], - "Heroes of Might and Magic III": [ - "Adventure", - "Strategy", - "Wargame" - ], - "Heroes of Might and Magic IV": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "Heroes of Might and Magic V": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "Heroes of Might and Magic V: Hammers of Fate": [ - "Adventure", - "DLC", - "Fantasy", - "RPG", - "Strategy", - "TBS" - ], - "Heroes of Might and Magic V: Tribes of the East": [ - "Fantasy", - "God game", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "Homefront": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up" - ], - "Hotel Remorse": [ - "Action-adventure", - "First-person" - ], - "Hotline Miami": [ - "Action-adventure", - "Arcade", - "Retro", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Hotline Miami 2: Wrong Number": [ - "Action-adventure", - "Arcade", - "Retro", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "House Flipper": [ - "Adventure", - "First-person", - "Simulation", - "Strategy" - ], - "I Am Alive": [ - "Action-adventure", - "Open-World", - "Platformer", - "Post apocalypse", - "Quest", - "Shooter", - "Survival horror", - "Third-person" - ], - "I am Setsuna": [ - "Adventure", - "JRPG", - "RPG" - ], - "I hate this game": [ - "Action", - "Arcade" - ], - "INSIDE": [ - "2D", - "Action-adventure", - "Action/RPG", - "Arcade", - "Horror", - "Logic", - "Platformer", - "Puzzle" - ], - "Indivisible": [ - "Action/RPG", - "Platformer" - ], - "Injustice: Gods Among Us": [ - "2D", - "Action", - "Arcade", - "Beat 'em up", - "Fighting" - ], - "Into the Breach": [ - "RPG", - "Sci-fi", - "Simulation", - "Strategy", - "TBS", - "Tactic" - ], - "Jade Empire": [ - "Action-adventure", - "Action/RPG", - "Fighting" - ], - "Jotun": [ - "Action-adventure", - "Arcade" - ], - "Just Cause": [ - "Action-adventure", - "Arcade", - "Open-World", - "Quest", - "Racing", - "Robot War", - "Sandbox", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Just Cause 2": [ - "Action-adventure", - "Open-World", - "Quest", - "Racing", - "Robot War", - "Sandbox", - "Shooter", - "Third-person" - ], - "Kane & Lynch 2: Dog Days": [ - "Action", - "Arcade", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Killer is Dead: Nightmare Edition": [ - "Action-adventure", - "First-person", - "Sci-fi" - ], - "King's Bounty: Armored Princess": [ - "Adventure", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "King's Bounty: Crossworlds": [ - "Adventure", - "DLC", - "Fantasy", - "RPG", - "Strategy", - "TBS" - ], - "King's Bounty: Dark Side": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic", - "Wargame" - ], - "King's Bounty: The Legend": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "King's Bounty: Warriors of the North": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "King's Bounty: Warriors of the North - Ice and Fire (DLC)": [ - "DLC" - ], - "Kingdom Rush": [ - "Action", - "Arcade", - "RTS", - "Strategy", - "Tactic", - "Tower Defence", - "Tower defense" - ], - "Kingdom: Classic": [ - "Action-adventure", - "Simulation", - "Strategy" - ], - "Kingdoms of Amalur: Reckoning": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Open-World", - "Third-person" - ], - "Knock-Knock": [ - "Action-adventure", - "Arcade" - ], - "Land of the Dead: Road to Fiddler's Green": [ - "Action", - "Arcade", - "FPS" - ], - "Lara Croft and the Guardian of Light": [ - "Action-adventure", - "Arcade", - "Platformer", - "Puzzle", - "Shoot 'Em Up", - "Shoot 'em up" - ], - "Lara Croft and the Temple of Osiris": [ - "2D", - "Action-adventure", - "Arcade", - "Logic", - "Platformer", - "Puzzle", - "Shoot 'em up" - ], - "Late Shift": [ - "Adventure", - "Interactive movie", - "Quest" - ], - "Layers of Fear": [ - "Action-adventure", - "First-person", - "Horror", - "Quest", - "Simulation" - ], - "Layers of Fear: Inheritance (DLC)": [ - "Add-on", - "Adventure", - "DLC", - "Quest" - ], - "Left 4 Dead": [ - "Action", - "Arcade", - "FPS", - "Horror", - "Sci-fi", - "Zombie" - ], - "Left 4 Dead 2": [ - "Action", - "FPS", - "Horror", - "Sci-fi", - "TPS", - "Tactic", - "Zombie" - ], - "Legend Hand of God": [ - "Action/RPG", - "Fantasy" - ], - "Legend of Grimrock": [ - "Adventure", - "RPG", - "Wargame" - ], - "Legendary": [ - "Action", - "FPS", - "Shoot 'em up" - ], - "Life After Us: Shipwrecked": [], - "Life Goes On: Done to Death": [ - "2D", - "Action-adventure", - "Arcade", - "Casual", - "Logic", - "Platformer", - "Puzzle" - ], - "Life Is Strange": [ - "Action-adventure", - "Puzzle", - "Quest", - "Sci-fi", - "Third-person" - ], - "Limbo": [ - "2D", - "Action-adventure", - "Arcade", - "Logic", - "Platformer", - "Puzzle" - ], - "Little Nightmares": [ - "Action-adventure", - "Arcade", - "Horror", - "Platformer", - "Puzzle" - ], - "Little Nightmares - The Depths (DLC)": [ - "Action-adventure", - "Add-on", - "Arcade", - "DLC", - "Platformer" - ], - "Little Nightmares - The Hideaway (DLC)": [ - "Action-adventure", - "Add-on", - "Arcade", - "DLC", - "Platformer" - ], - "Loki: Heroes of Mythology": [ - "Action/RPG", - "Hack and slash" - ], - "Lost Planet: Colonies": [ - "Action", - "Shooter" - ], - "Lost: Via Domus": [ - "Action-adventure", - "Arcade", - "Puzzle", - "Quest" - ], - "Love Is Strange (DEMO)": [], - "METAL SLUG": [ - "2D", - "Action", - "Arcade", - "Casual", - "Fighting", - "Platformer", - "Shoot 'em up", - "Shooter" - ], - "METAL SLUG 3": [ - "2D", - "Action", - "Arcade", - "Casual", - "Fighting", - "Platformer", - "Shoot 'em up", - "Shooter" - ], - "MINERVA: Metastasis": [ - "Shooter" - ], - "MOTHERGUNSHIP": [ - "Action", - "FPS", - "Sci-fi" - ], - "Machinarium": [ - "Adventure", - "Logic", - "Point-and-click", - "Puzzle", - "Quest", - "Sci-fi", - "Steampunk", - "Third-person" - ], - "Magibot": [ - "2D", - "Action-adventure", - "Arcade", - "Fantasy", - "Logic", - "Platformer", - "Puzzle", - "Strategy" - ], - "Magicka": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Fantasy", - "Strategy" - ], - "Magicka 2": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Fantasy" - ], - "Manual Samuel": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Humor", - "Quest" - ], - "Mario.EXE": [], - "Marvel vs. Capcom: Infinite": [ - "2D", - "Action", - "Arcade", - "Fighting" - ], - "Masochisia": [ - "Adventure", - "Point-and-click", - "Quest" - ], - "Masters of Anima": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Fantasy", - "MOBA", - "RTS", - "Strategy", - "Tactic" - ], - "Max Payne": [ - "Action-adventure", - "Arcade", - "Shoot 'em up", - "Shooter", - "TPS", - "Third-person" - ], - "Max Payne 2: The Fall of Max Payne": [ - "Action-adventure", - "Arcade", - "Shoot 'em up", - "Shooter", - "TPS", - "Third-person" - ], - "Max Payne 3": [ - "Action-adventure", - "Arcade", - "Shoot 'em up", - "Shooter", - "TPS", - "Third-person" - ], - "Max: The Curse Of Brotherhood": [ - "Action-adventure", - "Arcade", - "Logic", - "Platformer", - "Puzzle" - ], - "McPixel": [ - "Action-adventure", - "Arcade", - "Logic", - "Puzzle" - ], - "Metal Gear Rising: Revengeance": [ - "Action-adventure", - "Sci-fi", - "Shooter", - "Slasher", - "Tactic", - "Third-person" - ], - "Metal Wolf Chaos XD": [ - "Action", - "Robots", - "Sci-fi", - "Shooter", - "Simulation", - "Wargame" - ], - "Metro 2033": [ - "Action-adventure", - "Arcade", - "FPS", - "Post apocalypse", - "Sci-fi", - "Stealth", - "Survival horror" - ], - "Metro 2033 Redux": [ - "Action", - "Arcade", - "FPS", - "Sci-fi" - ], - "Metro Exodus": [ - "Action-adventure", - "Arcade", - "FPS", - "Open-World", - "Post apocalypse", - "Stealth", - "Survival horror" - ], - "Metro Last Light": [ - "Action", - "Arcade", - "FPS", - "Post apocalypse", - "Sci-fi", - "Stealth" - ], - "Metro Last Light Redux": [ - "Action", - "Arcade", - "FPS", - "Sci-fi" - ], - "Metro Last Light Redux: Chronicles Pack (DLC)": [], - "Metro Last Light Redux: Developer Pack (DLC)": [], - "Metro Last Light Redux: Faction Pack (DLC)": [], - "Metro Last Light Redux: Tower Pack (DLC)": [], - "Middle-earth: Shadow of Mordor": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Open-World", - "Third-person" - ], - "Might & Magic Heroes VI": [ - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic", - "Wargame" - ], - "Minecraft Story Mode": [ - "Adventure", - "Fantasy", - "Third-person" - ], - "Mini Ninjas": [ - "Action-adventure", - "Arcade", - "Fantasy", - "Quest", - "Third-person" - ], - "Mirror's Edge": [ - "Action-adventure", - "FPS", - "Platformer", - "Simulation" - ], - "Monst": [], - "Mortal Kombat IX": [], - "Mortal Kombat X": [ - "2D", - "Action", - "Arcade", - "Fighting" - ], - "Mother Russia Bleeds": [ - "2D", - "Action", - "Arcade", - "Beat 'em up", - "Fighting", - "Platformer", - "Retro" - ], - "Mount Your Friends": [ - "Action", - "Arcade", - "Simulation", - "Sport" - ], - "Mr. Robot": [ - "Action-adventure", - "Action/RPG", - "Logic", - "Platformer", - "Puzzle", - "Sci-fi", - "Strategy", - "Tactic" - ], - "Mr.President!": [ - "Action-adventure", - "Simulation" - ], - "Murdered: Soul Suspect": [ - "Action-adventure", - "Action/RPG", - "Quest", - "Survival horror", - "Third-person" - ], - "Naruto Shippuden: Ultimate Ninja Storm 3 Full Burst": [ - "Action-adventure", - "Beat 'em up", - "Fighting" - ], - "Naruto Shippuden: Ultimate Ninja Storm 4": [ - "Action-adventure", - "Arcade", - "Fighting" - ], - "Naruto Shippuden: Ultimate Ninja Storm Revolution": [ - "Action-adventure", - "Arcade", - "Beat 'em up", - "Fighting" - ], - "Need for Speed: Underground": [ - "Arcade", - "Racing", - "Simulation" - ], - "Never Alone": [ - "2D", - "Action", - "Arcade", - "Platformer", - "Puzzle" - ], - "Neverending Nightmares": [ - "Action-adventure", - "Quest", - "Survival horror" - ], - "Nevermind": [ - "Action-adventure", - "Adventure", - "First-person", - "Horror", - "Platformer" - ], - "Nevertales 8: The Abomination (Collector's Edition)": [], - "Neverwinter Nights": [ - "Adventure", - "Fantasy", - "RPG" - ], - "Neverwinter Nights 2": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Third-person" - ], - "NieR: Automata": [ - "Action/RPG", - "Post apocalypse", - "Robots", - "Sci-fi", - "Slasher" - ], - "Nightmare House 2": [], - "Nosferatu: The Wrath of Malachi": [ - "Action-adventure", - "Arcade", - "FPS", - "Fantasy", - "Horror" - ], - "Nox Timore": [], - "Oceanhorn Monster of Uncharted Seas": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "JRPG", - "Open-World", - "Quest" - ], - "Omensight": [ - "Action-adventure", - "Action/RPG", - "First-person" - ], - "One Hand Clapping (DEMO)": [], - "One Night at Flumpty's": [ - "Adventure", - "Quest" - ], - "One Night at Flumpty's 2": [], - "One Piece: Burning Blood": [ - "Action", - "Arcade", - "Beat 'em up", - "Fighting" - ], - "Orcs Must Die!": [ - "Action-adventure", - "RTS", - "Strategy", - "Tactic", - "Third-person", - "Tower Defence", - "Wargame" - ], - "Orcs Must Die! - Lost Adventures (DLC)": [ - "DLC" - ], - "Ori and the Blind Forest": [ - "2D", - "Action", - "Arcade", - "Platformer" - ], - "Outlast": [ - "Action-adventure", - "First-person", - "Survival horror" - ], - "Outlast: Whistleblower": [ - "Action-adventure", - "Add-on", - "DLC", - "First-person", - "Survival horror" - ], - "Overlord": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Quest", - "Strategy" - ], - "Overlord: Raising Hell": [ - "Action-adventure", - "Action/RPG", - "DLC", - "Fantasy", - "Strategy" - ], - "Oxenfree": [ - "Action-adventure", - "Adventure", - "Horror", - "Platformer", - "Quest", - "RPG" - ], - "Painkiller": [ - "Action-adventure", - "Arcade", - "FPS", - "Fantasy", - "Shoot 'em up", - "Survival horror" - ], - "Painkiller: Battle Out of Hell": [ - "Action", - "Arcade", - "DLC", - "FPS", - "Fantasy", - "Shoot 'em up" - ], - "Painkiller: Overdose": [ - "Action", - "Arcade", - "FPS", - "Fantasy", - "Shoot 'em up" - ], - "Paint the Town Red": [ - "Action", - "First-person", - "Survival" - ], - "Panty Party": [ - "Action-adventure", - "Arcade", - "Casual", - "Humor", - "Puzzle", - "Shooter", - "Simulation", - "Third-person" - ], - "Papers, Please": [ - "Action-adventure", - "Adventure", - "Logic", - "Puzzle", - "RPG", - "Retro", - "Simulation" - ], - "Peace, Death!": [ - "Arcade", - "Casual", - "Humor", - "Retro", - "Simulation" - ], - "Pillars of Eternity": [ - "Adventure", - "Fantasy", - "RPG", - "Third-person" - ], - "Pillars of Eternity: Champion Edition Upgrade Pack (DLC)": [ - "DLC" - ], - "Pillars of Eternity: The White March Part I (DLC)": [ - "Adventure", - "DLC", - "RPG" - ], - "Pillars of Eternity: The White March Part II (DLC)": [ - "Adventure", - "DLC", - "RPG" - ], - "Pinstripe": [ - "Action-adventure", - "Casual", - "Platformer", - "Puzzle", - "Quest" - ], - "Pirates of the Caribbean: At World’s End": [ - "Action-adventure", - "Quest" - ], - "Pizza Delivery 2": [], - "Plague Inc: Evolved": [ - "Casual", - "RTS", - "Sci-fi", - "Simulation", - "Strategy", - "TBS", - "Tactic" - ], - "Plants vs. Zombies": [ - "Action", - "Arcade", - "Fantasy", - "Puzzle", - "RTS", - "Strategy", - "Tactic", - "Tower Defence", - "Tower defense", - "Wargame", - "Zombie" - ], - "Plants vs. Zombies: Garden Warfare 2": [ - "Action", - "MMOFPS", - "Shoot 'em up", - "Shooter", - "TPS", - "Tactic", - "Third-person" - ], - "Pony Island": [ - "Action-adventure", - "Arcade", - "Logic", - "Puzzle" - ], - "Portal": [ - "Action-adventure", - "Arcade", - "FPS", - "Logic", - "Platformer", - "Puzzle", - "Sci-fi" - ], - "Portal 2": [ - "Action-adventure", - "Arcade", - "FPS", - "Logic", - "Puzzle", - "Sci-fi" - ], - "Prey (2006)": [ - "Action-adventure", - "Arcade", - "FPS", - "Sci-fi" - ], - "Prince of Persia (2008)": [ - "Action-adventure", - "Arcade", - "Fantasy", - "Open-World", - "Platformer", - "Third-person" - ], - "Prince of Persia: The Two Thrones": [ - "Action-adventure", - "Fantasy", - "Platformer", - "Third-person" - ], - "Prospekt": [ - "Action", - "Arcade", - "FPS" - ], - "Prototype": [ - "Action-adventure", - "Action/RPG", - "Open-World", - "Sci-fi" - ], - "Prototype 2": [ - "Action-adventure", - "First-person", - "Open-World", - "Sci-fi" - ], - "Psychonauts": [ - "Action-adventure", - "Arcade", - "Humor", - "Platformer", - "Third-person" - ], - "PuniTy": [], - "P·O·L·L·E·N": [ - "Action-adventure", - "First-person", - "Quest", - "Space" - ], - "Quake 4": [ - "Action", - "Arcade", - "FPS", - "Sci-fi" - ], - "Quantum Break": [ - "Action-adventure", - "Shooter", - "Third-person" - ], - "Raft": [ - "Action-adventure", - "First-person", - "Open-World", - "Simulation", - "Survival" - ], - "Rage": [ - "Action", - "Arcade", - "Beat 'em up", - "FPS", - "Racing" - ], - "Rampage Knights": [ - "2D", - "Action-adventure", - "Action/RPG", - "Arcade", - "Beat 'em up" - ], - "Rayman Legends": [ - "2D", - "Action-adventure", - "Arcade", - "Humor", - "Platformer" - ], - "Rayman Origins": [ - "2D", - "Action-adventure", - "Arcade", - "Fantasy", - "Humor", - "Platformer" - ], - "Rayman Raving Rabbids": [ - "Action-adventure", - "Arcade", - "Humor", - "Platformer" - ], - "Rayman Raving Rabbids 2": [ - "Action", - "Arcade", - "Music/Rhythm", - "Platformer" - ], - "Real Horror Stories": [ - "Action-adventure", - "Point-and-click" - ], - "Redeemer": [ - "Action-adventure", - "Fighting" - ], - "Reign Of Kings": [ - "Action-adventure", - "Action/RPG", - "First-person", - "Open-World", - "Simulation", - "Survival" - ], - "Reigns": [ - "Adventure", - "Arcade", - "Cardgame", - "Humor", - "Logic", - "Quest", - "RPG", - "RTS", - "Simulation", - "Strategy" - ], - "Reigns: Her Majesty": [ - "Adventure", - "Arcade", - "Cardgame", - "Casual", - "Humor", - "Quest", - "RPG", - "RTS", - "Simulation", - "Strategy" - ], - "Resident Evil": [ - "Action-adventure", - "Survival horror", - "Third-person", - "Zombie" - ], - "Resident Evil / Biohazard HD REMASTER": [ - "Action-adventure" - ], - "Resident Evil 0 / Biohazard 0 HD REMASTER": [ - "Action-adventure" - ], - "Resident Evil 4": [ - "Action-adventure", - "Shooter", - "Survival horror", - "TPS", - "Third-person" - ], - "Resident Evil 5": [ - "Action-adventure", - "Shooter", - "Survival horror", - "TPS", - "Third-person", - "Zombie" - ], - "Resident Evil 5: Desperate Escape (DLC)": [], - "Resident Evil 5: Lost in Nightmares (DLC)": [ - "Action", - "Shooter" - ], - "Resident Evil 6": [ - "Action-adventure", - "Shooter", - "Survival horror", - "TPS", - "Third-person" - ], - "Resident Evil 7: Biohazard": [ - "Action-adventure", - "Shooter", - "Survival horror" - ], - "Resident Evil 7: Biohazard - Banned Footage Vol.1 (DLC)": [ - "Action", - "Add-on", - "DLC" - ], - "Resident Evil 7: Biohazard - Banned Footage Vol.2 (DLC)": [ - "Action", - "Add-on", - "DLC" - ], - "Resident Evil 7: Biohazard - End of Zoe (DLC)": [ - "Action", - "Add-on", - "DLC" - ], - "Resident Evil 7: Biohazard - Not a Hero (DLC)": [ - "Action", - "Add-on", - "DLC" - ], - "Resident Evil: Operation Raccoon City": [ - "Action-adventure", - "Arcade", - "Horror", - "Sci-fi", - "Shoot 'em up", - "Shooter", - "Survival horror", - "Tactical", - "Third-person", - "Zombie" - ], - "Resident Evil: Revelations": [ - "Action-adventure", - "Survival horror", - "Third-person" - ], - "Resident Evil: Revelations 2": [ - "Action-adventure", - "Shooter", - "Survival horror", - "Third-person" - ], - "Reus": [ - "Arcade", - "God game", - "RTS", - "Sandbox", - "Simulation", - "Strategy", - "Tactic" - ], - "Rezrog": [ - "Adventure", - "Logic", - "RPG", - "Roguelike", - "Strategy", - "TBS", - "Tactic" - ], - "Rise of the Tomb Raider": [ - "Action-adventure", - "Open-World", - "Platformer", - "Third-person" - ], - "Rise of the Tomb Raider - Baba Yaga: The Temple of the Witch (DLC)": [ - "Action", - "DLC" - ], - "Rise of the Tomb Raider - Blood Ties (DLC)": [ - "Action" - ], - "Rise of the Tomb Raider - Cold Darkness Awakened (DLC)": [ - "Action", - "DLC" - ], - "Rise of the Tomb Raider - Endurance Mode (DLC)": [ - "DLC" - ], - "Risen": [ - "Action-adventure", - "Action/RPG", - "Open-World", - "Third-person" - ], - "Rock of Ages": [ - "Action", - "Arcade", - "Platformer", - "Racing", - "Strategy", - "TBS" - ], - "Rock of Ages 2: Bigger & Boulder": [ - "Action", - "Arcade", - "Humor", - "Racing", - "Strategy", - "Tower Defence" - ], - "Root Of Evil: The Tailor": [ - "Action-adventure", - "Casual", - "First-person", - "Horror", - "Simulation" - ], - "Runic Rampage": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Hack and slash" - ], - "Rust": [ - "Action-adventure", - "Action/RPG", - "MMO", - "MMOFPS", - "Open-World", - "Shooter", - "Survival" - ], - "Ryse: Son of Rome": [ - "Action-adventure", - "Fantasy", - "First-person", - "Slasher", - "Third-person", - "Wargame" - ], - "S.T.A.L.K.E.R.: Shadow of Chernobyl": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Open-World", - "Post apocalypse", - "Sci-fi", - "Shoot 'em up", - "Survival horror" - ], - "SCP-087": [ - "Action-adventure" - ], - "SCP-087-B": [ - "Action" - ], - "SINNER: Sacrifice for Redemption": [ - "Action-adventure", - "Action/RPG" - ], - "SWAT 4": [ - "Action", - "Arcade", - "FPS", - "Simulation", - "Strategy", - "Tactic", - "Wargame" - ], - "Sable Maze: Forbidden Garden (Collector's Edition)": [ - "Adventure", - "Casual", - "Puzzle" - ], - "Sable Maze: Nightmare Shadows (Collector's Edition)": [], - "Sable Maze: Norwich Caves (Collector's Edition)": [ - "Adventure", - "Casual" - ], - "Sable Maze: Sinister Knowledge (Collector's Edition)": [], - "Sable Maze: Soul Catcher (Collector's Edition)": [], - "Sable Maze: Sullivan River (Collector's Edition)": [ - "Adventure", - "Casual" - ], - "Sable Maze: Twelve Fears (Collector's Edition)": [ - "Adventure", - "Casual" - ], - "Sacred": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Open-World" - ], - "Sacred 3": [ - "Action-adventure", - "Action/RPG", - "Hack and slash" - ], - "Sacred Citadel": [ - "2D", - "Action-adventure", - "Arcade", - "Beat 'em up", - "Platformer", - "Shoot 'Em Up" - ], - "Saga of the Nine Worlds: The Four Stags (Collector's Edition)": [], - "Saga of the Nine Worlds: The Gathering (Collector's Edition)": [], - "Saga of the Nine Worlds: The Hunt (Collector's Edition)": [], - "Saints Row IV": [ - "Action-adventure", - "Fighting", - "Open-World", - "Racing", - "Shooter" - ], - "Saints Row: Gat out of Hell": [ - "Action-adventure", - "Add-on", - "DLC", - "Open-World", - "Racing", - "Shooter", - "Third-person" - ], - "Salt and Sanctuary": [ - "2D", - "Action-adventure", - "Action/RPG", - "Arcade", - "Platformer", - "TBS" - ], - "Samsara Room": [ - "Adventure", - "Logic", - "Puzzle" - ], - "Saw: The Video Game": [ - "Action", - "Horror", - "Third-person" - ], - "Scratches": [ - "Adventure", - "Horror", - "Point-and-click", - "Quest" - ], - "Seasons after Fall": [ - "2D", - "Action-adventure", - "Arcade", - "Logic", - "Platformer", - "Puzzle" - ], - "Serena": [ - "Adventure", - "First-person", - "Horror", - "Quest" - ], - "Serious Sam 2": [ - "Action", - "First-person" - ], - "Serious Sam 3: BFE": [ - "Action", - "Arcade", - "FPS", - "Sci-fi" - ], - "Serious Sam 3: Jewel of the Nile (DLC)": [ - "Action", - "DLC", - "Shooter" - ], - "Serious Sam Classic: The First Encounter": [ - "Action" - ], - "Serious Sam Classic: The Second Encounter": [ - "Action" - ], - "Shadow Warrior": [ - "Action-adventure", - "Arcade", - "FPS", - "Fantasy", - "Shoot 'em up" - ], - "Shadow of the Tomb Raider": [ - "Action-adventure", - "Open-World", - "Third-person" - ], - "Shadow of the Tomb Raider - The Forge (DLC)": [ - "Action", - "Add-on", - "DLC", - "Open-World" - ], - "Shantae: Half-Genie Hero": [ - "2D", - "Action-adventure", - "Arcade", - "Platformer" - ], - "Shoot Your Nightmare Halloween Special": [ - "Action", - "Arcade", - "FPS" - ], - "Shovel Knight: Shovel of Hope": [ - "Action-adventure" - ], - "Shrek 2: The Game": [ - "Arcade", - "Third-person" - ], - "Silence on The Line": [], - "Silent Hill: Alchemilla": [], - "Silverfall": [ - "Action-adventure", - "Action/RPG" - ], - "Silverfall: Earth Awakening": [ - "Action/RPG" - ], - "Singularity": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Slap-Man": [], - "Slay the Spire": [ - "Cardgame", - "Logic", - "Strategy", - "TBS", - "Tactic" - ], - "Sleeping Dogs": [ - "Action-adventure", - "Open-World", - "Racing", - "Shooter", - "Third-person" - ], - "Slender: The Arrival": [ - "Action-adventure", - "First-person", - "Horror", - "Quest" - ], - "Slendytubbies": [], - "Slime Rancher": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Logic", - "Open-World", - "Quest", - "Simulation" - ], - "Sniper Elite": [ - "Action", - "FPS", - "Shoot 'em up", - "Shooter", - "Simulation", - "Stealth", - "TPS", - "Tactic", - "Third-person" - ], - "Sniper Elite 3": [ - "Action-adventure", - "FPS", - "Stealth", - "TPS", - "Tactic", - "Third-person" - ], - "Sniper Elite V2": [ - "Action", - "FPS", - "Shooter", - "Stealth", - "TPS", - "Tactic", - "Third-person" - ], - "Sophie's Curse": [ - "Adventure", - "Casual", - "First-person", - "Horror", - "Simulation" - ], - "South Park: The Fractured But Whole": [ - "Adventure", - "Humor", - "RPG" - ], - "South Park: The Fractured But Whole - Bring The Crunch (DLC)": [ - "Add-on", - "DLC", - "RPG" - ], - "South Park: The Fractured But Whole - From Dusk Till Casa Bonita (DLC)": [ - "Add-on", - "DLC", - "RPG" - ], - "South Park: The Stick of Truth": [ - "Action-adventure", - "Action/RPG", - "Humor" - ], - "Spirits of Mystery: Amber Maiden (Collector's Edition)": [ - "Adventure", - "Casual", - "Hidden Object", - "Puzzle" - ], - "Spirits of Mystery: Chains of Promise (Collector's Edition)": [ - "Adventure", - "Casual" - ], - "Spirits of Mystery: Family Lies (Collector's Edition)": [], - "Spirits of Mystery: Illusions (Collector's Edition)": [], - "Spirits of Mystery: Song of the Phoenix (Collector's Edition)": [ - "Adventure", - "Casual" - ], - "Spirits of Mystery: The Dark Minotaur (Collector's Edition)": [ - "Adventure", - "Casual", - "Puzzle" - ], - "Spirits of Mystery: The Fifth Kingdom (Collector's Edition)": [], - "Spirits of Mystery: The Last Fire Queen (Collector's Edition)": [], - "Spirits of Mystery: The Lost Queen (Collector's Edition)": [], - "Spirits of Mystery: The Moon Crystal (Collector's Edition)": [], - "Spirits of Mystery: The Silver Arrow (Collector's Edition)": [ - "Adventure", - "Casual", - "Puzzle" - ], - "Spirits of Mystery: Whisper of the Past (Collector's Edition)": [], - "Spooky's House of Jump Scares": [ - "Action-adventure", - "Horror" - ], - "Star Wars Jedi Knight: Jedi Academy": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Slasher", - "Third-person" - ], - "Star Wars: The Force Unleashed": [ - "Action-adventure", - "Arcade", - "Quest", - "Shooter", - "Third-person" - ], - "Star Wars: The Force Unleashed II": [ - "Action-adventure", - "Beat 'em up" - ], - "SteamWorld Dig": [ - "2D", - "Action-adventure", - "Arcade", - "Platformer", - "Post apocalypse", - "Puzzle", - "Steampunk", - "Western" - ], - "SteamWorld Dig 2": [ - "2D", - "Action-adventure", - "Arcade", - "Platformer", - "Steampunk", - "Western" - ], - "SteamWorld Heist": [ - "Action-adventure", - "Action/RPG", - "Platformer", - "Sci-fi", - "Strategy", - "TBS", - "Tactic" - ], - "Steampunk Tower 2": [ - "Action", - "RTS", - "Strategy", - "Tower Defence" - ], - "Steel Rats": [ - "Action", - "Arcade", - "Motorcycle", - "Platformer", - "Racing", - "Simulation", - "Steampunk", - "Wargame" - ], - "Stories: The Path of Destinies": [ - "Action-adventure", - "Action/RPG", - "Platformer" - ], - "Street Fighter X Tekken": [ - "2D", - "Action", - "Arcade", - "Beat 'em up", - "Fighting" - ], - "Stubbs the Zombie in Rebel Without a Pulse": [ - "Action-adventure", - "Beat 'em up", - "Sci-fi", - "Shooter", - "Third-person" - ], - "Stupidella": [ - "Arcade", - "Quest" - ], - "Sudeki": [ - "Action-adventure", - "Action/RPG", - "Third-person" - ], - "Superhot": [ - "Action", - "Arcade", - "FPS", - "Logic", - "Puzzle", - "Tactic" - ], - "Surgeon Simulator": [ - "Action/RPG", - "Simulation" - ], - "Tales from the Borderlands": [ - "Adventure" - ], - "Tales of Berseria": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "JRPG", - "Third-person" - ], - "Tales of Zestiria": [ - "Action-adventure", - "Action/RPG", - "JRPG" - ], - "Tales of Zestiria - Additional Chapter: Alisha's Story (DLC)": [ - "DLC" - ], - "Tasty Planet: Back for Seconds": [ - "Action-adventure", - "Arcade", - "Casual", - "Puzzle" - ], - "Tekken 7": [ - "Action", - "Arcade", - "Beat 'em up", - "Fighting", - "Sport" - ], - "Tesla Effect: A Tex Murphy Adventure": [ - "Adventure", - "Quest" - ], - "Tesla vs Lovecraft": [ - "2D", - "Action/RPG", - "Arcade", - "Shoot 'em up", - "Shooter" - ], - "Teslagrad": [ - "2D", - "Action-adventure", - "Arcade", - "Logic", - "Platformer", - "Puzzle" - ], - "The Banner Saga": [ - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "The Banner Saga 2": [ - "Adventure", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "The Bard’s Tale (2004)": [ - "Action/RPG" - ], - "The Beginner's Guide": [ - "Adventure", - "Humor" - ], - "The Binding of Isaac: Afterbirth (DLC)": [ - "Action/RPG", - "Add-on", - "Arcade", - "DLC" - ], - "The Binding of Isaac: Rebirth": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Roguelike", - "Shoot 'em up", - "Shooter", - "Survival horror" - ], - "The Bunker": [ - "Action-adventure", - "Point-and-click", - "Quest", - "Retro", - "Simulation" - ], - "The Cat Lady": [ - "Adventure", - "Horror", - "Quest" - ], - "The Council": [ - "Action-adventure", - "Action/RPG", - "Third-person" - ], - "The Curse of Blackwater": [ - "Action-adventure", - "Survival" - ], - "The Dark Pictures Anthology: Man of Medan": [ - "Adventure" - ], - "The Darkness II": [ - "Action-adventure", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Survival horror" - ], - "The Elder Scrolls IV: Oblivion": [ - "Action-adventure", - "Adventure", - "First-person", - "RPG" - ], - "The Elder Scrolls IV: Oblivion - Knights of the Nine (DLC)": [], - "The Elder Scrolls IV: Oblivion - Shivering Isles (DLC)": [], - "The Elder Scrolls V: Skyrim": [ - "Action-adventure", - "Action/RPG" - ], - "The Elder Scrolls V: Skyrim - Dawnguard (DLC)": [ - "DLC", - "RPG" - ], - "The Elder Scrolls V: Skyrim - Dragonborn (DLC)": [ - "Action-adventure", - "Action/RPG", - "DLC" - ], - "The Elder Scrolls V: Skyrim - Falskaar (MOD)": [], - "The Elder Scrolls V: Skyrim - Hearthfire (DLC)": [ - "Adventure", - "DLC", - "RPG" - ], - "The Evil Within": [ - "Action-adventure", - "Shooter", - "Survival horror", - "Third-person" - ], - "The Evil Within 2": [ - "Action-adventure", - "Open-World", - "Shooter", - "Survival horror", - "Third-person" - ], - "The Evil Within: The Assignment (DLC)": [ - "Action", - "Add-on", - "DLC" - ], - "The Evil Within: The Consequence (DLC)": [ - "Action", - "Add-on", - "DLC" - ], - "The Evil Within: The Executioner (DLC)": [ - "Action", - "Add-on", - "DLC" - ], - "The Expendabros": [ - "Action-adventure", - "Arcade", - "Platformer" - ], - "The Final Station": [ - "Action-adventure", - "Platformer", - "Post apocalypse", - "Quest", - "Retro", - "Survival", - "Zombie" - ], - "The First Tree": [ - "Adventure", - "Arcade", - "Quest", - "Simulation", - "Third-person" - ], - "The Forest": [ - "Action-adventure", - "First-person", - "Open-World", - "Platformer", - "Simulation", - "Strategy", - "Survival horror" - ], - "The Hat Man Shadow Ward": [ - "Action-adventure", - "Action/RPG", - "Casual", - "Horror", - "Racing", - "Simulation", - "Sport", - "Strategy" - ], - "The Incredible Adventures of Van Helsing": [ - "Action-adventure", - "Action/RPG", - "Steampunk", - "Third-person" - ], - "The Incredible Adventures of Van Helsing II": [ - "Action-adventure", - "Action/RPG" - ], - "The Incredible Adventures of Van Helsing III": [ - "Action-adventure", - "Action/RPG", - "Hack and slash", - "Third-person" - ], - "The Last Remnant": [ - "Action-adventure", - "Action/RPG", - "JRPG", - "Strategy" - ], - "The Mask Man": [], - "The Punisher (2005)": [ - "Action", - "Shooter", - "Third-person" - ], - "The Secret of Monkey Island: Special Edition": [ - "Adventure", - "Humor", - "Point-and-click", - "Quest" - ], - "The Slippery Slope": [], - "The Stanley Parable": [ - "Action-adventure", - "First-person", - "Humor", - "Quest", - "Visual Novel" - ], - "The Walking Dead: A New Frontier": [ - "Adventure", - "Zombie" - ], - "The Walking Dead: Michonne": [ - "Adventure" - ], - "The Walking Dead: Season One": [], - "The Walking Dead: Season Two": [ - "Adventure", - "Point-and-click" - ], - "The Witcher": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Open-World", - "Third-person" - ], - "The Witcher 2: Assassins of Kings": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Third-person" - ], - "The Witcher 3: Wild Hunt": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Open-World", - "Third-person" - ], - "The Witcher 3: Wild Hunt - Blood and Wine (DLC)": [ - "Action/RPG", - "Add-on", - "DLC", - "Open-World", - "RPG" - ], - "The Witcher 3: Wild Hunt - Hearts of Stone (DLC)": [ - "Action/RPG", - "Add-on", - "DLC", - "Open-World", - "RPG" - ], - "The Wolf Among Us": [ - "Action-adventure", - "Adventure", - "Fantasy", - "Point-and-click", - "Puzzle" - ], - "Thief": [ - "Action-adventure", - "Fantasy", - "Stealth" - ], - "This Is The Police": [ - "Adventure", - "Quest", - "RTS", - "Simulation", - "Strategy", - "TBS", - "Tactic", - "Time management" - ], - "TimeShift": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "Titan Quest": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Hack and slash" - ], - "Titan Quest Anniversary Edition": [ - "Action/RPG" - ], - "Titan Quest: Ragnarök (DLC)": [], - "Titan Quest: The Immortal Throne (DLC)": [ - "Action/RPG", - "DLC", - "Wargame" - ], - "Titan Souls": [ - "Action-adventure", - "Action/RPG", - "Arcade" - ], - "Titanfall 2": [ - "Action", - "Arcade", - "FPS", - "MMO", - "MMOFPS", - "Robot War", - "Robots", - "Shoot 'em up" - ], - "To The Moon": [ - "Adventure", - "Point-and-click", - "Quest", - "RPG", - "Visual Novel" - ], - "Tomb Raider": [ - "Action-adventure", - "Platformer", - "Quest" - ], - "Torchlight": [ - "Action/RPG", - "Beat 'em up", - "Fantasy", - "Hack and slash" - ], - "Torchlight II": [ - "Action-adventure", - "Action/RPG", - "Hack and slash" - ], - "Toren": [ - "Action-adventure", - "Arcade", - "Fantasy", - "First-person", - "Platformer", - "Puzzle", - "Quest" - ], - "Tormentum: Dark Sorrow": [ - "Action-adventure", - "Adventure", - "Casual", - "Fantasy", - "Horror", - "Logic", - "Point-and-click", - "Puzzle", - "Quest" - ], - "Total Overdose": [ - "Action-adventure", - "Open-World", - "Shooter", - "Third-person" - ], - "Transistor": [ - "Action-adventure", - "Action/RPG", - "Sci-fi", - "Strategy", - "TBS", - "Third-person" - ], - "Trine": [ - "2D", - "Action-adventure", - "Arcade", - "Fantasy", - "Logic", - "Platformer", - "Puzzle", - "Quest" - ], - "Trine 2": [ - "2D", - "Action", - "Arcade", - "Logic", - "Platformer", - "Puzzle" - ], - "Trine 3: The Artifacts of Power": [ - "2D", - "Action-adventure", - "Arcade", - "Logic", - "Platformer", - "Puzzle" - ], - "Troll Tale": [ - "Puzzle" - ], - "Trollface Quest": [], - "Trollface Quest 2": [], - "Trollface Quest 3": [], - "Trollface Quest 4": [], - "Trollface Quest 5": [], - "Trollface Quest 6": [], - "Trollface Quest 7": [], - "Trollface Quest: Video Memes": [], - "Twin Sector": [ - "Action-adventure", - "FPS", - "First-person", - "Logic", - "Puzzle" - ], - "Two Worlds": [ - "Action-adventure", - "Adventure", - "RPG", - "Third-person" - ], - "Typical Nightmare": [ - "Action-adventure", - "First-person", - "Simulation" - ], - "Ultimate Epic Battle Simulator": [ - "Action-adventure", - "RTS", - "Simulation", - "Strategy", - "Tactic" - ], - "Ultra Street Fighter IV": [ - "2D", - "Action", - "Arcade", - "Beat 'em up", - "Fighting" - ], - "Undertale": [ - "Action-adventure", - "Action/RPG", - "Arcade" - ], - "Unfair Mario": [ - "2D", - "Action", - "Platformer" - ], - "Unmechanical": [ - "2D", - "Action-adventure", - "Arcade", - "Casual", - "Logic", - "Platformer", - "Puzzle", - "Quest" - ], - "Unmechanical: Extended (DLC)": [ - "Action-adventure", - "Puzzle" - ], - "Until the last": [ - "Action-adventure", - "Survival" - ], - "Valiant Hearts: The Great War": [ - "2D", - "Action-adventure", - "Action/RPG", - "Arcade", - "Logic", - "Platformer", - "Puzzle", - "Quest" - ], - "Vanish": [ - "Action-adventure" - ], - "Victor Vran": [ - "Action-adventure", - "Action/RPG" - ], - "Vikings: Wolves of Midgard": [ - "Action-adventure", - "Action/RPG", - "Hack and slash" - ], - "Visage": [ - "Action-adventure", - "First-person", - "Quest", - "Simulation", - "Survival horror" - ], - "Wanted: Weapons of Fate": [ - "Action-adventure", - "Arcade", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Warcraft III: Reign of Chaos": [ - "Fantasy", - "RTS", - "Strategy", - "Tactic", - "Wargame" - ], - "Warcraft III: The Frozen Throne": [ - "Adventure", - "DLC", - "Fantasy", - "RPG", - "RTS", - "Strategy", - "Wargame" - ], - "Warhammer 40.000: Space Marine": [ - "Action/RPG", - "Arcade", - "Sci-fi", - "Shoot 'em up", - "Shooter", - "Third-person" - ], - "Warhammer: Chaosbane": [ - "Action-adventure", - "Action/RPG", - "Fantasy", - "Hack and slash" - ], - "We Happy Few": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Horror" - ], - "Whack The Thief": [], - "Whack Your Boss": [], - "Whack Your Ex": [ - "Action" - ], - "Whack Your Neighbour": [], - "Whack Your Teacher": [], - "What Remains of Edith Finch": [ - "Adventure", - "First-person", - "Quest" - ], - "Will Rock": [ - "Action", - "Arcade", - "FPS", - "Fantasy", - "Shoot 'em up" - ], - "Willy-Nilly Knight": [ - "Action/RPG", - "Adventure", - "Fantasy", - "RPG", - "Strategy", - "TBS", - "Tactic" - ], - "Witch trainer": [ - "Adventure" - ], - "Wizard of Legend": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "Dungeon", - "Hack and slash", - "Slasher" - ], - "Wolfenstein": [ - "Action", - "FPS", - "Shoot 'em up" - ], - "Wolfenstein II: The New Colossus": [ - "Action", - "Arcade", - "FPS" - ], - "Wolfenstein II: The New Colossus - The Freedom Chronicles: Episode Zero (DLC)": [], - "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Adventures of Gunslinger Joe (DLC)": [], - "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Deeds of Captain Wilkins (DLC)": [], - "Wolfenstein II: The New Colossus - The Freedom Chronicles: The Diaries of Agent Silent Death (DLC)": [], - "Wolfenstein: The New Order": [ - "Action", - "Action-adventure", - "Arcade", - "FPS", - "Shoot 'em up" - ], - "Wolfenstein: The Old Blood": [ - "Action", - "Add-on", - "Arcade", - "FPS", - "Shoot 'em up" - ], - "Woolfe - The Red Hood Diaries": [ - "Action-adventure", - "Arcade", - "Platformer", - "Slasher", - "Survival" - ], - "Worms 2": [ - "Action", - "Arcade", - "Fighting", - "Logic", - "Shoot 'em up", - "Strategy", - "TBS", - "Tactic", - "Wargame" - ], - "X-Blades / Ониблэйд": [], - "X-Morph: Defense": [ - "Action", - "Arcade", - "RTS", - "Shoot 'em up", - "Shooter", - "Strategy", - "Tactic", - "Tower Defence", - "Tower defense" - ], - "Yandere School": [ - "Action-adventure", - "Simulation", - "Third-person" - ], - "Year Walk": [ - "Adventure", - "First-person", - "Logic", - "Puzzle", - "Quest", - "RPG" - ], - "ZOMBI": [ - "Action-adventure", - "Arcade", - "FPS", - "Point-and-click", - "Survival horror" - ], - "Zero Escape: Zero Time Dilemma": [ - "Adventure", - "Logic", - "Puzzle", - "Quest", - "Visual Novel" - ], - "Ziggurat": [ - "Action/RPG", - "Arcade", - "FPS", - "Fantasy", - "Shoot 'em up" - ], - "Zombie Driver HD": [ - "Action", - "Arcade", - "Racing", - "Shooter" - ], - "Zombie Party": [ - "2D", - "Action-adventure", - "Action/RPG", - "Arcade", - "Shoot 'em up", - "Shooter" - ], - "Zombie Vikings": [ - "2D", - "Action-adventure", - "Arcade", - "Beat 'em up", - "Hack and slash" - ], - "Zuma Deluxe": [ - "Arcade", - "Casual", - "Logic", - "Puzzle" - ], - "Zuma's Revenge!": [ - "Action", - "Arcade", - "Casual", - "Logic", - "Puzzle" - ], - "Как достать соседа 2: Адские каникулы / Neighbours from Hell 2: On Vacation": [], - "Как достать соседа 3: В офисе (2006) / Pranksterz: Off Your Boss": [], - "Как достать соседа: Сладкая месть / Neighbours from Hell: Revenge Is a Sweet": [], - "Как достать соседку: Полный гламур (2009) / Neighbours from Hell: Full Glamor": [], - "Невероятные Приключения Трояна": [], - "Невероятные Приключения Трояна 2": [], - "Невероятные Приключения Трояна 3": [], - "Предатор": [], - "Стоп-Гоп": [], - "Стоп-Гоп 2": [], - "ШХД: ЗИМА / IT'S WINTER": [ - "Casual", - "Simulation" - ], - "Control": [ - "Action-adventure", - "Shooter", - "Third-person" - ], - "Crysis 3": [ - "Action", - "Arcade", - "FPS", - "MMOFPS", - "Open-World", - "Sci-fi", - "Shoot 'em up" - ], - "007: Quantum of Solace": [ - "Action", - "Arcade", - "FPS" - ], - "Borderlands 2: Tiny Tina's Assault on Dragon Keep (DLC)": [ - "Action/RPG", - "DLC", - "Shooter" - ], - "Constantine": [ - "Action-adventure", - "Horror", - "Sci-fi", - "Shooter", - "Third-person" - ], - "Devil May Cry": [ - "Action-adventure", - "Fantasy", - "Shooter", - "Slasher", - "Third-person" - ], - "Devil May Cry 2": [ - "Action-adventure", - "Fantasy", - "Shooter", - "Slasher", - "Third-person" - ], - "Devil May Cry HD Collection": [ - "Action-adventure", - "Arcade", - "Horror", - "Slasher", - "Third-person" - ], - "Kholat": [ - "Action-adventure", - "First-person", - "Horror", - "Quest", - "Survival horror" - ], - "Nihilumbra": [ - "2D", - "Action-adventure", - "Arcade", - "Casual", - "Logic", - "Platformer", - "Puzzle" - ], - "Prey": [ - "Action", - "Arcade", - "FPS" - ], - "Samorost 3": [ - "Adventure", - "Casual", - "Logic", - "Point-and-click", - "Puzzle", - "Quest" - ], - "Terminator: Resistance": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi" - ], - "The Old City: Leviathan": [ - "Adventure", - "Fantasy", - "First-person", - "Quest" - ], - "Unreal Tournament": [ - "Action", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Space" - ], - "You Are Empty": [ - "Action", - "Arcade", - "FPS", - "Post apocalypse", - "Shoot 'em up" - ], - "Rogue Warrior": [ - "Action", - "Arcade", - "FPS", - "Shoot 'em up", - "Stealth", - "Tactic" - ], - "Counter-Strike": [ - "Action", - "FPS", - "Shoot 'em up", - "Tactic", - "Tactical FPS", - "Wargame" - ], - "Dark Void": [ - "Action-adventure", - "Flying", - "Shoot 'em up", - "Shooter", - "TPS", - "Third-person", - "Wargame" - ], - "Agatha Knife": [ - "Adventure", - "Arcade", - "Point-and-click", - "Quest" - ], - "Ancestors: The Humankind Odyssey": [ - "Action-adventure", - "Action/RPG", - "Simulation", - "Survival", - "Third-person" - ], - "Anna's Quest": [ - "Adventure", - "Point-and-click", - "Puzzle", - "Quest" - ], - "BDSM: Big Drunk Satanic Massacre": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Shoot 'em up", - "Shooter" - ], - "Barn Finders": [ - "Adventure", - "Casual", - "Simulation", - "Time management" - ], - "Batman: Arkham VR": [ - "Action-adventure", - "Arcade", - "Shooter", - "Simulation" - ], - "Battle Chasers: Nightwar": [ - "Action/RPG", - "JRPG", - "Strategy" - ], - "Battletoads (2020)": [ - "Action-adventure", - "Arcade", - "Beat 'em up", - "Fighting", - "Platform", - "Platformer" - ], - "Borderlands 3": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS" - ], - "Borderlands 3: Bounty of Blood (DLC)": [], - "Borderlands 3: Guns, Love, and Tentacles (DLC)": [ - "Action/RPG", - "Add-on", - "DLC", - "Shooter" - ], - "Borderlands 3: Moxxi's Heist of the Handsome Jackpot (DLC)": [ - "Action-adventure", - "Action/RPG", - "Add-on", - "DLC", - "Shooter" - ], - "Borderlands 3: Psycho Krieg and the Fantastic Fustercluck (DLC)": [ - "Action/RPG", - "Add-on", - "Shooter" - ], - "Bright Memory": [ - "Action-adventure", - "FPS", - "Shooter", - "Slasher" - ], - "Call of Duty: Modern Warfare 2 Campaign Remastered": [ - "Action", - "Arcade", - "FPS" - ], - "Call of Duty: Modern Warfare Remastered": [ - "Action", - "Arcade", - "FPS", - "Tactic" - ], - "Carrion": [ - "2D", - "Action-adventure", - "Horror", - "Platformer" - ], - "Code Vein": [ - "Action/RPG" - ], - "Colt Canyon": [ - "Action/RPG", - "Casual", - "Shoot 'em up", - "Shooter" - ], - "Command & Conquer: Renegade": [ - "Action", - "FPS", - "TPS" - ], - "Control: AWE (DLC)": [ - "Action-adventure", - "Add-on", - "Shooter" - ], - "Control: The Foundation (DLC)": [ - "Action", - "Add-on", - "Shooter" - ], - "Crash Bandicoot N. Sane Trilogy": [ - "Action", - "Arcade", - "Platform", - "Platformer" - ], - "Creepy Tale": [ - "Adventure", - "Horror", - "Platform", - "Platformer", - "Puzzle" - ], - "Cuphead": [ - "2D", - "Action", - "Arcade", - "Platform", - "Platformer", - "Shoot 'em up" - ], - "DOOM Eternal": [ - "Action", - "Arcade", - "FPS" - ], - "Dear Esther": [ - "Adventure", - "Casual", - "Fantasy", - "First-person", - "Point-and-click", - "Quest" - ], - "Desire": [ - "Adventure", - "Point-and-click", - "Quest" - ], - "Destroy All Humans!": [ - "Action-adventure", - "Arcade", - "Open-World", - "Shooter" - ], - "Diablo": [ - "Action/RPG", - "Hack and slash", - "Puzzle" - ], - "Diablo: Hellfire": [ - "Action-adventure", - "Action/RPG", - "Hack and slash" - ], - "Dishonored 2": [ - "Action-adventure", - "Action/RPG", - "FPS", - "Stealth" - ], - "Dishonored: Death of the Outsider": [ - "Action-adventure", - "Action/RPG", - "Add-on", - "FPS", - "Stealth" - ], - "Doorways: Holy Mountains of Flesh": [ - "Action-adventure", - "First-person", - "Horror", - "Quest" - ], - "Doorways: Prelude": [ - "Action-adventure" - ], - "Doorways: The Underworld": [ - "Action-adventure", - "First-person", - "Horror", - "Quest" - ], - "Earthlock": [ - "Adventure", - "RPG", - "Strategy" - ], - "Fallout 4: Automatron (DLC)": [ - "Action/RPG", - "Add-on", - "DLC", - "Shooter" - ], - "Fallout 4: Far Harbor (DLC)": [ - "Action/RPG", - "Add-on", - "DLC", - "Shooter" - ], - "Fallout 4: Nuka-World (DLC)": [ - "Action/RPG", - "Add-on", - "DLC", - "Shooter" - ], - "Fallout: New Vegas - Dead Money (DLC)": [ - "Action/RPG", - "DLC" - ], - "Fallout: New Vegas - Honest Hearts (DLC)": [ - "Action/RPG", - "DLC" - ], - "Fallout: New Vegas - Lonesome Road (DLC)": [ - "Action/RPG", - "DLC" - ], - "Fallout: New Vegas - Old World Blues (DLC)": [ - "Action/RPG", - "DLC" - ], - "Forgotten Realms: Demon Stone": [ - "Action-adventure", - "Action/RPG", - "Beat 'em up", - "Fighting", - "Third-person" - ], - "Gears of War": [ - "Action-adventure", - "Arcade", - "Sci-fi", - "Shooter", - "Survival horror", - "TPS", - "Third-person" - ], - "Granny": [ - "Action-adventure", - "Arcade", - "Casual", - "Survival" - ], - "Granny: Chapter Two": [ - "Action-adventure", - "Arcade", - "Casual", - "Survival" - ], - "Grim Dawn": [ - "Action-adventure", - "Action/RPG", - "Hack and slash" - ], - "Grim Dawn: Ashes of Malmouth (DLC)": [ - "DLC" - ], - "Grim Dawn: Forgotten Gods (DLC)": [ - "Action/RPG", - "Add-on", - "DLC", - "RPG" - ], - "Grim Tales 9: Threads Of Destiny (Collector's Edition)": [], - "Half-Life 2: Lost Coast (MOD)": [], - "Half-Life: Alyx": [ - "Action-adventure", - "Arcade", - "FPS" - ], - "House on the Hill": [ - "Action-adventure", - "First-person", - "Puzzle" - ], - "Inmost": [ - "Action-adventure", - "Platformer", - "Puzzle" - ], - "Journey": [ - "Action-adventure", - "Arcade", - "Platform", - "Platformer", - "Puzzle", - "Quest" - ], - "Kane & Lynch: Dead Men": [ - "Action", - "FPS", - "Shoot 'em up", - "Shooter", - "TPS", - "Tactic", - "Third-person" - ], - "Lost Planet 2": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Platformer", - "Robot War", - "Sci-fi", - "Shoot 'em up", - "Shooter", - "TPS", - "Third-person" - ], - "Lost Planet 3": [ - "Action", - "Arcade", - "FPS", - "Robot War", - "Sci-fi", - "Shoot 'em up", - "Shooter", - "TPS", - "Third-person" - ], - "Lost in Vivo": [ - "Action", - "Arcade", - "FPS" - ], - "MechaNika": [ - "Adventure", - "Point-and-click", - "Puzzle", - "Quest" - ], - "Metro Exodus: Sam's Story (DLC)": [ - "Action", - "Add-on", - "DLC", - "Open-World", - "Shooter" - ], - "Metro Exodus: The Two Colonels (DLC)": [ - "Action", - "Add-on", - "DLC", - "Open-World", - "Shooter" - ], - "Middle-earth: Shadow of Mordor - Lord of the Hunt (DLC)": [ - "DLC" - ], - "Middle-earth: Shadow of Mordor - The Bright Lord (DLC)": [ - "Action", - "Add-on", - "DLC", - "Open-World" - ], - "Middle-earth: Shadow of War": [ - "Action-adventure", - "Action/RPG", - "Open-World", - "Stealth" - ], - "Middle-earth: Shadow of War - The Blade of Galadriel (DLC)": [ - "Action/RPG", - "Add-on", - "DLC", - "Open-World", - "RPG" - ], - "Middle-earth: Shadow of War - The Desolation of Mordor (DLC)": [ - "Action/RPG", - "Add-on", - "DLC", - "Open-World", - "RPG" - ], - "Minecraft Dungeons": [ - "Action-adventure", - "Action/RPG", - "Open-World" - ], - "Mortal Kombat 11": [ - "Action-adventure", - "Fighting" - ], - "Mortal Kombat 11: Aftermath (DLC)": [ - "DLC" - ], - "Neverwinter Nights 2: Mask of the Betrayer (DLC)": [ - "Adventure", - "DLC", - "RPG" - ], - "Neverwinter Nights 2: Mysteries of Westgate (DLC)": [ - "Adventure", - "DLC", - "Point-and-click", - "RPG" - ], - "Neverwinter Nights 2: Storm of Zehir (DLC)": [ - "Adventure", - "DLC", - "RPG" - ], - "Of Orcs And Men": [ - "Action-adventure", - "Action/RPG" - ], - "Ori and the Will of the Wisps": [ - "2D", - "Action-adventure", - "Arcade", - "Platform", - "Platformer", - "Puzzle" - ], - "Outlast 2": [ - "Action-adventure", - "Survival horror" - ], - "Outlast: Whistleblower (DLC)": [ - "Action", - "Add-on", - "DLC" - ], - "Painkiller: Hell & Damnation": [ - "Action", - "Arcade", - "FPS", - "Fantasy", - "Shoot 'em up" - ], - "Painkiller: Hell & Damnation - City Critters (DLC)": [ - "Action", - "DLC", - "Shooter" - ], - "Painkiller: Hell & Damnation - Demonic Vacation at the Blood Sea (DLC)": [ - "DLC" - ], - "Painkiller: Hell & Damnation - Operation \"Zombie Bunker\" (DLC)": [ - "Action", - "DLC" - ], - "Painkiller: Hell & Damnation - The Clock Strikes Meat Night (DLC)": [ - "Action", - "DLC" - ], - "Painkiller: Recurring Evil": [ - "Action", - "Arcade", - "FPS", - "Fantasy" - ], - "Painkiller: Redemption": [ - "Action", - "Arcade", - "FPS" - ], - "Painkiller: Resurrection": [ - "Action", - "Action-adventure", - "Arcade", - "FPS", - "Fantasy", - "Shoot 'em up" - ], - "Postal": [ - "2D", - "Action", - "Arcade", - "Shoot 'em up", - "Shooter", - "TPS", - "Third-person" - ], - "Postal 2": [ - "Action-adventure", - "Arcade", - "FPS", - "Shoot 'em up" - ], - "Postal 2: Apocalypse Weekend (DLC)": [ - "Action", - "DLC", - "Shooter" - ], - "Prince of Persia: The Sands of Time": [ - "Action-adventure", - "Fantasy", - "Platformer" - ], - "Prince of Persia: Warrior Within": [ - "Action-adventure", - "Fantasy" - ], - "Resident Evil 2 / Biohazard RE:2": [ - "Action", - "Horror" - ], - "Resident Evil 2 / Biohazard RE:2: The 4th Survivor (DLC)": [], - "Resident Evil 2 / Biohazard RE:2: The Ghost Survivors (DLC)": [], - "Resident Evil 2 Remake / Biohazard RE:2": [], - "Resident Evil 2 Remake / Biohazard RE:2 - The 4th Survivor (DLC)": [], - "Resident Evil 2 Remake / Biohazard RE:2 - The Ghost Survivors (DLC)": [], - "Resident Evil 3 Remake": [ - "Action-adventure", - "Survival horror" - ], - "Resident Evil 4 - Ultimate HD Edition": [ - "Action-adventure", - "Shooter", - "Survival horror" - ], - "Rock of Ages 3: Make & Break": [ - "Action", - "Arcade", - "RTS", - "Racing", - "Strategy" - ], - "RollerCoaster Legends": [ - "Adventure", - "Arcade" - ], - "S.T.A.L.K.E.R.: Call of Pripyat": [ - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up" - ], - "S.T.A.L.K.E.R.: Clear Sky": [ - "Action-adventure", - "Action/RPG", - "Arcade", - "FPS", - "Sci-fi", - "Shoot 'em up", - "Survival" - ], - "Sally Face": [ - "Adventure", - "Horror", - "Platformer", - "Quest" - ], - "Sekiro: Shadows Die Twice": [ - "Action-adventure", - "Action/RPG", - "Open-World", - "Slasher" - ], - "Serious Sam 3: BFE - Jewel of the Nile (DLC)": [], - "Serious Sam 4": [ - "Action-adventure", - "Arcade", - "FPS" - ], - "Sherlock Holmes: Crimes and Punishments": [ - "Adventure" - ], - "Sherlock Holmes: The Devil's Daughter": [ - "Action-adventure", - "Open-World", - "Quest" - ], - "Sniper Elite 4": [ - "Action-adventure", - "FPS", - "Stealth", - "TPS", - "Tactic" - ], - "Star Wars Jedi: Fallen Order": [ - "Action-adventure", - "Quest" - ], - "Star Wars: Knights of the Old Republic II – The Sith Lords": [ - "Action-adventure", - "Action/RPG", - "Third-person" - ], - "Stela": [ - "2D", - "Action-adventure", - "Arcade", - "Platform", - "Platformer", - "Puzzle" - ], - "Styx: Master of Shadows": [ - "Action-adventure", - "Action/RPG", - "Puzzle", - "Sci-fi", - "Stealth" - ], - "Sublustrum": [ - "Adventure", - "Quest" - ], - "Super Cloudbuilt": [ - "Action-adventure", - "Platformer", - "Racing" - ], - "Superhot: Mind Control Delete": [ - "Action", - "FPS", - "Tactic" - ], - "Swords & Souls: Neverseen": [ - "Action-adventure", - "Action/RPG" - ], - "Terminator Salvation": [ - "Action", - "FPS", - "Shoot 'em up", - "Shooter", - "TPS" - ], - "The Corridor": [ - "Adventure", - "Puzzle" - ], - "The Light Remake": [ - "Adventure", - "Simulation" - ], - "The Outer Worlds": [ - "Action/RPG", - "First-person" - ], - "The Outer Worlds: Peril on Gorgon (DLC)": [ - "Action/RPG", - "Add-on", - "RPG" - ], - "The Story of Henry Bishop": [ - "Action-adventure", - "Survival" - ], - "The Unfinished Swan": [ - "Action-adventure", - "First-person", - "Puzzle", - "Quest" - ], - "This Is the Police": [ - "Adventure", - "Quest", - "RTS", - "Simulation", - "Strategy", - "Tactic", - "Time management" - ], - "Torchlight III": [ - "Action-adventure", - "Action/RPG" - ], - "Tyranny": [ - "Action/RPG", - "Adventure", - "RPG" - ], - "Vanquish": [ - "Action", - "Arcade", - "Robots", - "Shooter", - "TPS", - "Third-person" - ], - "Violett Remastered": [ - "Adventure" - ], - "Wolfenstein: Youngblood": [ - "Action", - "Arcade", - "FPS" - ], - "XCOM: Chimera Squad": [ - "Strategy", - "TBS", - "Tactic" - ], - "Zenith": [ - "Action-adventure", - "Action/RPG", - "Strategy" - ], - "Анабиоз: Сон разума | Cryostasis: Sleep of Reason": [ - "FPS" - ], - "Вивисектор: зверь внутри": [ - "Action", - "Shooter" - ] -} \ No newline at end of file diff --git a/html_parsing/get_game_genres/generate_games/generate_games.py b/html_parsing/get_game_genres/generate_games/generate_games.py deleted file mode 100644 index fc89b970d..000000000 --- a/html_parsing/get_game_genres/generate_games/generate_games.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import datetime as DT -import json -from pathlib import Path -import shutil - -import sys -sys.path.append('..') -sys.path.append('../genre_translate_file') - -from db import Dump -from load import load -from common_utils import get_logger -from common import get_current_datetime_str - - -log = get_logger('generate_games.txt') - - -DIR = Path(__file__).parent.resolve() -FILE_NAME_GAMES = DIR / 'game_by_genres.json' -FILE_NAME_BACKUP = DIR / 'backup' - -FILE_NAME_BACKUP.mkdir(parents=True, exist_ok=True) - -# Example: "Action", "Adventure" -> "Action-adventure" -GENRE_COMPRESSION = [ - ("Action", "Adventure", "Action-adventure"), - ("Action", "RPG", "Action/RPG"), - ("First-person", "Shooter", "FPS"), - ("Survival", "Horror", "Survival horror"), -] - - -def do_genres_compression(genres: list) -> list: - genres = sorted(set(genres)) - to_remove = set() - - for src_1, src_2, target in GENRE_COMPRESSION: - if src_1 in genres and src_2 in genres: - to_remove.add(src_1) - to_remove.add(src_2) - genres.append(target) - - log.info(f'Compress genres {src_1!r} and {src_2!r} -> {target!r}') - - for x in to_remove: - genres.remove(x) - - return sorted(set(genres)) - - -log.info('Start.') - -if FILE_NAME_GAMES.exists(): - backup_file_name = str( - FILE_NAME_BACKUP / f'{get_current_datetime_str()}_{FILE_NAME_GAMES.name}' - ) - shutil.copy( - FILE_NAME_GAMES, - backup_file_name - ) - log.info(f'Save backup to: {backup_file_name}') - log.info('') - -log.info('Loading cache...') - -game_by_genres = load(FILE_NAME_GAMES) -log.info(f'game_by_genres ({len(game_by_genres)})') - -new_game_by_genres = Dump.dump() -log.info(f'new_game_by_genres ({len(new_game_by_genres)})') - -genre_translate = load() -log.info(f'genre_translate ({len(genre_translate)})') - -log.info('Finish loading cache.') -log.info('') - -log.info('Search games...') - -number = 0 - -for game, genres in new_game_by_genres.items(): - if game in game_by_genres: - continue - - log.info(f'Added game {game!r} with genres: {genres}') - number += 1 - - new_genres = [] - - for x in genres: - tr_genres = genre_translate.get(x) - if not tr_genres: # null, [], "" - continue - - if isinstance(tr_genres, str): - new_genres.append(tr_genres) - - elif isinstance(tr_genres, list): - new_genres.extend(tr_genres) - - else: - log.warning(f'Unsupported type genres {tr_genres} from {x!r}') - - new_genres = do_genres_compression(new_genres) - - log.info(f'Successful translate genres: {genres} -> {new_genres}') - game_by_genres[game] = new_genres - - log.info('') - -log.info(f'Finish search games. New games: {number}.') - -log.info(f'Saving to {FILE_NAME_GAMES}') - -json.dump( - game_by_genres, - open(FILE_NAME_GAMES, 'w', encoding='utf-8'), - ensure_ascii=False, - indent=4 -) - -log.info('Finish!') diff --git a/html_parsing/get_game_genres/genre_translate_file/create.py b/html_parsing/get_game_genres/genre_translate_file/create.py deleted file mode 100644 index 3820a405e..000000000 --- a/html_parsing/get_game_genres/genre_translate_file/create.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import json -import sys - -from pathlib import Path - -DIR = Path(__file__).resolve().parent.parent -ROOT_DIR = DIR.parent.parent - -sys.path.append(str(DIR)) -sys.path.append(str(ROOT_DIR)) - -from db import Dump -from load import FILE_NAME_GENRE_TRANSLATE, load -from common_utils import get_logger -from third_party.add_notify_telegram import add_notify - - -log = get_logger('genre_translate.txt') - - -def run(need_notify=True): - log.info('Start load genres.') - - genre_translate = load() - is_first_run = not genre_translate - - log.info(f'Current genres: {len(genre_translate)}') - - new_genres = [] - for genre in Dump.get_all_genres(): - if genre not in genre_translate: - log.info(f'Added new genre: {genre!r}') - genre_translate[genre] = None - new_genres.append(genre) - - if new_genres: - text = f"Added genres ({len(new_genres)}): {', '.join(new_genres)}" - log.info(text) - - # Если это первый запуск, то сообщение не отправляем - if not is_first_run and need_notify: - add_notify(log.name, text) - - log.info('Save genres') - - json.dump( - genre_translate, - open(FILE_NAME_GENRE_TRANSLATE, 'w', encoding='utf-8'), - ensure_ascii=False, - indent=4 - ) - - else: - log.info('No new genres') - - log.info('Finish!') - - -if __name__ == '__main__': - run() diff --git a/html_parsing/get_game_genres/genre_translate_file/data/genre_translate.json b/html_parsing/get_game_genres/genre_translate_file/data/genre_translate.json deleted file mode 100644 index 2caf43e16..000000000 --- a/html_parsing/get_game_genres/genre_translate_file/data/genre_translate.json +++ /dev/null @@ -1,328 +0,0 @@ -{ - "2D": "2D", - "3D": "", - "Action": "Action", - "Action Adventure": "Action-adventure", - "Action RPG": "Action/RPG", - "Add-on pack": "", - "Adventure": "Adventure", - "Adventure: Free Roaming": "Adventure", - "Adventure: Graphic": "Adventure", - "Adventure: Point and Click": [ - "Adventure", - "Point-and-click" - ], - "Adventure: Role Playing": [ - "Adventure", - "RPG" - ], - "Adventure: Survival Horror": [ - "Adventure", - "Survival horror" - ], - "Arcade": "Arcade", - "Automobile": "Racing", - "Beat 'Em Up": "Beat 'em up", - "Beat 'Em Up: Hack and Slash": "Beat 'em up", - "Beat-'Em-Up": "Beat 'em up", - "Beat-em-up": "Beat 'em up", - "Board / Card Game": "Cardgame", - "CCG": "Cardgame", - "Card Battle": "Cardgame", - "Card Game": "Cardgame", - "Career": "", - "Casual": "Casual", - "Combat": "Wargame", - "Combat Game": "Wargame", - "Combat Game: Infantry": "Wargame", - "Compilation": "", - "Console-style RPG": "RPG", - "DLC / add-on": "DLC", - "Defense": "Tower Defence", - "Driving": "Racing", - "Driving/Racing": "Racing", - "Early Access": "", - "FPS": "FPS", - "Fantasy": "Fantasy", - "Fighting": "Fighting", - "First-Person": "First-person", - "Fixed-Screen": "", - "Free to Play": "", - "GT / Street": "Racing", - "General": "", - "Hidden Object": "Hidden Object", - "Historic": "", - "Horizontal": "", - "Horror": "Horror", - "Indie": "", - "Japanese-Style": "", - "Light Gun": "", - "Light-Gun": "", - "Linear": "", - "MMOG": "MMO", - "MOBA": "MOBA", - "Massively Multiplayer": "MMO", - "Matching": "", - "Matching/Stacking": "", - "Miscellaneous": "", - "Modern": "", - "Music": "Music", - "Music/Rhythm": "Music/Rhythm", - "Open-World": "Open-World", - "PC-style RPG": "", - "Party": "", - "Party / Minigame": "", - "Party/Minigame": "", - "Platform": "Platformer", - "Platformer": "Platformer", - "Point-and-Click": "Point-and-click", - "Point-and-click": "Point-and-click", - "Puzzle": "Puzzle", - "Puzzle: Physics": "Puzzle", - "RPG": "RPG", - "RTS": "RTS", - "Racing": "Racing", - "Racing / driving": "Racing", - "Rail": "", - "Real-Time": "RTS", - "Rhythm": "Music/Rhythm", - "Roguelike": "Roguelike", - "Role-Playing": "RPG", - "Role-Playing (RPG)": "RPG", - "Sandbox": "Sandbox", - "Sci-Fi": "Sci-fi", - "Shoot 'Em Up": "Shoot 'em up", - "Shoot-'Em-Up": "Shoot 'em up", - "Shoot-em-up": "Shoot 'em up", - "Shooter": "Shooter", - "Simulation": "Simulation", - "Special edition": "", - "Sports": "Sport", - "Strategy": "Strategy", - "Strategy / tactics": [ - "Strategy", - "Tactic" - ], - "Strategy: Combat": "Wargame", - "Strategy: God game": "God game", - "Strategy: Stealth": "", - "Strategy: Trading": "", - "Survival": "Survival", - "TBS": "TBS", - "Tactical": "Tactic", - "Tactics": "Tactic", - "Third-Person": "Third-person", - "Top-Down": "", - "Tower defence": "Tower Defence", - "Trivia/Board Game": "Board game", - "Turn-Based": "TBS", - "VR": "", - "Various: Party Game": "", - "Vehicle": "", - "Vertical": "", - "Virtual": "", - "Virtual Life": "", - "Visual Novel": "Visual Novel", - "Western-Style": "", - "aRPG": "Action/RPG", - "action": "Action", - "add-on": "", - "adventure": "Adventure", - "arcade": "Arcade", - "beat 'em up": "Beat 'em up", - "cards": "Cardgame", - "fighting": "Fighting", - "for adults": "", - "for kids": "", - "hack & slash": "Hack and slash", - "jRPG": "JRPG", - "logic": "Logic", - "online": "", - "racing": "Racing", - "rpg": "RPG", - "runner": "Runner", - "shoot 'em up": "Shoot 'em up", - "simulator": "Simulation", - "stealth": "Stealth", - "strategy": "Strategy", - "survival": "Survival", - "tower defense": "Tower defense", - "Автомобили": "", - "Адвенчура": "Adventure", - "Аддон": "Add-on", - "Аркада": "Arcade", - "Аркадные": "Arcade", - "Аркады": "Arcade", - "Битвы машин": "Robot War", - "Боевик": "Action", - "Боевик от первого лица": [ - "Action", - "First-person" - ], - "Боевик от третьего лица": [ - "Action", - "Third-person" - ], - "Боевик-приключения": "Action-adventure", - "Боевик/Экшн": "Action", - "В пошаговом режиме": "TBS", - "В реальном времени": "RTS", - "Велоспорт": [ - "Cycle", - "Racing" - ], - "Вестерн": "Western", - "Вид сверху": "", - "Визуальный роман": "Visual Novel", - "Виртуальная реальность": "", - "Выживание": "Survival", - "Головоломка": "Puzzle", - "Головоломки": "Puzzle", - "Головоломки/Пазл": "Puzzle", - "Гонки": "Racing", - "Гонки/вождение": "Racing", - "Гоночные/Рейсинг": "Racing", - "Для детей": "", - "Дополнение": "DLC", - "Драки/Файтинг": "Fighting", - "Другой симулятор": "Simulation", - "Зомби": "Zombie", - "Инди": "", - "Интерактивный фильм": "Interactive movie", - "Казуальные": "Casual", - "Карточная": "Cardgame", - "Карточная игра": "Cardgame", - "Карточные": "Cardgame", - "Карточные игры": "Cardgame", - "Карты": "Cardgame", - "Квест": "Quest", - "Киберпанк": "Cyberpunk", - "Кооператив": "", - "Космос": "Space", - "Логика": "Logic", - "Логические": "Logic", - "Логические игры": "Logic", - "ММО": "MMO", - "ММОРПГ/Многопользоватеские онлайновые": "MMORPG", - "Менеджемент": "Time management", - "Менеджер": "Time management", - "Миниигры": "", - "Многопользоватеский шутер": "MMOFPS", - "Мототехника": "Motorcycle", - "Мотоциклы": "Motorcycle", - "Музыка/Ритм": "Music/Rhythm", - "Мультиплеер": "", - "На логику": "Logic", - "Настольные": "Board game", - "Научная фантастика": "Sci-fi", - "Образовательные": "", - "От первого лица": "First-person", - "От третьего лица": "Third-person", - "Открытый мир": "Open-World", - "Песочница": "Sandbox", - "Платформер": "Platformer", - "Платформеры": "Platformer", - "Подземелья": "Dungeon", - "Постапокалипсис": "Post apocalypse", - "Пошаговая": "TBS", - "Пошаговая стратегия": "TBS", - "Приключение": "Adventure", - "Приключения": "Adventure", - "Пристрели их всех": "Shoot 'Em Up", - "Прочее": "", - "РПГ": "RPG", - "Ремастер": "", - "Ремейк": "", - "Ретро": "Retro", - "Роботы": "Robots", - "Ролевая": "RPG", - "Ролевая игра": "RPG", - "Ролевые": "RPG", - "Ролевые игры": "RPG", - "Самостоятельный": "", - "Семейные": "", - "Симулятор": "Simulation", - "Симулятор жизни": "Life simulation", - "Симуляторы": "Simulation", - "Слэшер": "Slasher", - "Спорт": "Sport", - "Спортивные": "Sport", - "Спортивные игры": "Sport", - "Средневековье": "", - "Стелс": "Stealth", - "Стимпанк": "Steampunk", - "Стратегии": "Strategy", - "Стратегия": "Strategy", - "Стратегия в реальном времени": "RTS", - "Стратегия в реальном времени/РТС": "RTS", - "Строительство": "", - "Тактика": "Tactic", - "Тактический": "Tactic", - "Ужасы": "Horror", - "Условно-бесплатная": "", - "Файтинг": "Fighting", - "Файтинги": "Fighting", - "Фэнтези": "Fantasy", - "Шутер": "Shooter", - "Шутер от первого лица": "FPS", - "Шутеры": "Shooter", - "Экшен": "Action", - "Экшены": "Action", - "Юмор": "Humor", - "аркадные": "Arcade", - "визуальные новеллы": "Visual Novel", - "головоломки": "Puzzle", - "гонки": "Racing", - "детские": "", - "для взрослых": "", - "инди": "", - "карточные": "Cardgame", - "квесты": "Quest", - "командные шутеры": "Tactical Shooter", - "логические": "Logic", - "платформеры": "Platformer", - "приключения": "Adventure", - "симуляторы": "Simulation", - "симуляторы бога": "God game", - "спорт": "Sport", - "тактика": "Tactic", - "ужасы": "Horror", - "файтинги": "Fighting", - "шутеры от 3-го лица": "TPS", - "Beat 'em up": "Beat 'em up", - "Interactive Movie": "Interactive Movie", - "Platform Game": "Platform", - "Slasher": "Slasher", - "Survival Horror": "Survival horror", - "TPS": "TPS", - "Tactical FPS": "Tactical FPS", - "City Building": "City Building", - "JRPG": "JRPG", - "Management": "Time management", - "Point&Click": "Point-and-click", - "Scrolling Shooter": "Scrolling Shooter", - "Stealth": "Stealth", - "Tactical RPG": "TRPG", - "Text Game": "Text Game", - "Tower Defense": "Tower Defense", - "Combat Game: Flying": [ - "Wargame", - "Flying" - ], - "On-Rails": "", - "Add-on": "", - "Entertainment": "", - "Isometric": "Isometric", - "Shoot 'Em Up: Sniper": [ - "Shoot 'em up", - "Sniper" - ], - "Combat Game: Space": [ - "Wargame", - "Space" - ], - "Паззл": "Puzzle", - "Хоррор": "Horror", - "Пошаговые": "TBS", - "Space": "Space" -} \ No newline at end of file diff --git a/html_parsing/get_game_genres/genre_translate_file/data/merge_genre_translate.json b/html_parsing/get_game_genres/genre_translate_file/data/merge_genre_translate.json deleted file mode 100644 index 20babec16..000000000 --- a/html_parsing/get_game_genres/genre_translate_file/data/merge_genre_translate.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - - -} \ No newline at end of file diff --git a/html_parsing/get_game_genres/genre_translate_file/load.py b/html_parsing/get_game_genres/genre_translate_file/load.py deleted file mode 100644 index 1eae3f325..000000000 --- a/html_parsing/get_game_genres/genre_translate_file/load.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import json -from pathlib import Path - - -FILE_NAME_GENRE_TRANSLATE = str(Path(__file__).parent.resolve() / 'data' / 'genre_translate.json') - - -def load(file_name: str = FILE_NAME_GENRE_TRANSLATE) -> dict: - try: - genre_translate = json.load( - open(file_name, encoding='utf-8') - ) - except: - genre_translate = dict() - - return genre_translate - - -if __name__ == '__main__': - genre_translate = load() - print(f'Genre_translate ({len(genre_translate)}): {genre_translate}') - print() - - # Print all undefined genres without '{' / '}' and indent - genre_null_translate = { - k: v - for k, v in genre_translate.items() - if v is None - } - print(f'Genre null translate ({len(genre_null_translate)}):') - json_text = json.dumps(genre_null_translate, ensure_ascii=False, indent=4) - lines = json_text.splitlines()[1:-1] - for i, line in enumerate(lines): - print(line.strip()) - if i > 0 and i % 40 == 0: - print() diff --git a/html_parsing/get_game_genres/genre_translate_file/merge.py b/html_parsing/get_game_genres/genre_translate_file/merge.py deleted file mode 100644 index c04ce77b3..000000000 --- a/html_parsing/get_game_genres/genre_translate_file/merge.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import json -import shutil -from pathlib import Path - -import sys -sys.path.append('..') - -from load import load, FILE_NAME_GENRE_TRANSLATE -from common_utils import get_logger -from common import get_current_datetime_str - - -# Инструкция: -# Из скопировать в жанры, что еще не -# определены и в значение указать стандартизированное название жанра -# При выполнении скрипта значения из обновят - -FILE_NAME_MERGE_GENRE_TRANSLATE = str(Path(__file__).parent.resolve() / 'data' / 'merge_genre_translate.json') - -DIR = Path(__file__).parent.resolve() -FILE_NAME_BACKUP = DIR / 'backup' -FILE_NAME_BACKUP.mkdir(parents=True, exist_ok=True) - -log = get_logger('merge_genre_translate.txt') - -log.info('Start.') - -backup_file_name = ( - FILE_NAME_BACKUP / f'{get_current_datetime_str()}_{Path(FILE_NAME_GENRE_TRANSLATE).name}' -) -log.info(f'Save backup to: {backup_file_name}') -shutil.copy( - FILE_NAME_GENRE_TRANSLATE, - backup_file_name -) - -log.info('Load genres') - -genre_translate = load() -log.info( - f'Current genres: {len(genre_translate)}. ' - f'Null genres: {sum(1 for v in genre_translate.values() if v is None)}' -) - -if genre_translate: - number = 0 - - log.info('Load merge') - - merge_genre_translate = load(FILE_NAME_MERGE_GENRE_TRANSLATE) - log.info(f'Current merged genres: {len(merge_genre_translate)}') - - for k, v in merge_genre_translate.items(): - if v is not None and k in genre_translate and genre_translate.get(k) is None: - log.info(f'Merge: {k!r} -> {v!r}') - genre_translate[k] = v - number += 1 - - log.info(f'New merged: {number}') - log.info('Save merged genres') - - json.dump( - genre_translate, - open(FILE_NAME_GENRE_TRANSLATE, 'w', encoding='utf-8'), - ensure_ascii=False, - indent=4 - ) -else: - log.info('Empty genres. Skip.') - -log.info('Finish!') diff --git a/html_parsing/get_game_genres/main.py b/html_parsing/get_game_genres/main.py deleted file mode 100644 index 0e11c5327..000000000 --- a/html_parsing/get_game_genres/main.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from timeit import default_timer -from threading import Thread -import time -import sys - -sys.path.append('genre_translate_file') -import create as create_genre_translate - -from db import db_create_backup, Dump, db -from common_utils import get_parsers, get_games_list, wait, get_logger, AtomicCounter, seconds_to_str, print_parsers - - -IGNORE_SITE_NAMES = ['gamefaqs_gamespot_com'] - -# Test -USE_FAKE_PARSER = False -if USE_FAKE_PARSER: - class FakeParser: - @classmethod - def get_site_name(cls): return "" - - @staticmethod - def get_game_genres(game_name): - if game_name == 'Foo': - raise Exception('Error') - - return ['RGB-bar', 'Action-bar'] - - # Monkey Patch - def get_parsers(): - return [FakeParser] - - def get_games_list(): return ['Foo', 'Bar', 'Zet'] - - -log = get_logger() -counter = AtomicCounter() - - -def run_parser(parser, games: list, max_num_request=5): - try: - pauses = [ - ('15 minutes', 15 * 60), - ('30 minutes', 30 * 60), - ('45 minutes', 45 * 60), - ('1 hour', 60 * 60), - ] - SITE_NAME = parser.get_site_name() - timeout = 3 # 3 seconds - MAX_TIMEOUT = 10 # 10 seconds - TIMEOUT_EVERY_N_GAMES = 50 # Every 50 games - TIMEOUT_BETWEEN_N_GAMES = 3 * 60 # 3 minutes - number = 0 - - for game_name in games: - try: - if Dump.exists(SITE_NAME, game_name): - continue - - number += 1 - - num_request = 0 - - while True: - num_request += 1 - try: - if num_request == 1: - log.info(f'#{number}. Search genres for {game_name!r} ({SITE_NAME})') - else: - log.info(f'#{number}. Search genres for {game_name!r} ({SITE_NAME}). ' - f'Attempts {num_request}/{max_num_request}') - - genres = parser.get_game_genres(game_name) - log.info(f'#{number}. Found genres {game_name!r} ({SITE_NAME}): {genres}') - - Dump.add(SITE_NAME, game_name, genres) - counter.inc() - - time.sleep(timeout) - break - - except: - log.exception(f'#{number}. Error on request {num_request}/{max_num_request} ({SITE_NAME})') - if num_request >= max_num_request: - log.info(f'#{number}. Attempts ended for {game_name!r} ({SITE_NAME})') - break - - pause_text, pause_secs = pauses[num_request - 1] - log.info(f'#{number}. Pause: {pause_text} secs') - time.sleep(pause_secs) - - timeout += 1 - if timeout > MAX_TIMEOUT: - timeout = MAX_TIMEOUT - - if number % TIMEOUT_EVERY_N_GAMES == 0: - log.info( - f'#{number}. Pause for every {TIMEOUT_EVERY_N_GAMES} games: {TIMEOUT_BETWEEN_N_GAMES} secs' - ) - time.sleep(TIMEOUT_BETWEEN_N_GAMES) - - except: - log.exception(f'#{number}. Error by game {game_name!r} ({SITE_NAME})') - - except: - log.exception(f'Error:') - - -if __name__ == "__main__": - parsers = [x for x in get_parsers() if x.get_site_name() not in IGNORE_SITE_NAMES] - print_parsers(parsers, log=lambda *args, **kwargs: log.info(*args, **kwargs)) - - while True: - try: - log.info(f'Started') - t = default_timer() - - db_create_backup() - - games = get_games_list() - log.info(f'Total games: {len(games)}') - - threads = [] - for parser in parsers: - threads.append( - Thread(target=run_parser, args=[parser, games]) - ) - log.info(f'Total parsers/threads: {len(threads)}') - log.info(f'Ignore parsers ({len(IGNORE_SITE_NAMES)}): {", ".join(IGNORE_SITE_NAMES)}') - - counter.value = 0 - - for thread in threads: - thread.start() - - for thread in threads: - thread.join() - - log.info(f'Finished. Added games: {counter.value}. Total games: {Dump.select().count()}. ' - f'Elapsed time: {seconds_to_str(default_timer() - t)}') - - create_genre_translate.run() - - wait(days=1) - - except: - log.exception('') - wait(minutes=15) - - finally: - log.info('') diff --git a/html_parsing/get_game_genres/parsers/__init__.py b/html_parsing/get_game_genres/parsers/__init__.py deleted file mode 100644 index 06334aa61..000000000 --- a/html_parsing/get_game_genres/parsers/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - diff --git a/html_parsing/get_game_genres/parsers/ag_ru.py b/html_parsing/get_game_genres/parsers/ag_ru.py deleted file mode 100644 index 84932bb9c..000000000 --- a/html_parsing/get_game_genres/parsers/ag_ru.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import re -from typing import List - -from base_parser import BaseParser - - -class AgRu_Parser(BaseParser): - def _parse(self) -> List[str]: - headers = { - 'Host': 'ag.ru', - 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', - 'Accept-Encoding': 'gzip, deflate', - } - - # Для правдоподобности сделаем запрос на страницу с играми - rs = self.send_get('https://ag.ru/games/pc', headers=headers) - m = re.search(r'"rawgApiKey"\s*:\s*"(.+?)"', rs.text) - if not m: - raise Exception('Не удалось найти на странице ключ для API (rawgApiKey)!') - key = m.group(1) - - # Заголовки, что отправляются вместе с запросом к API - headers.update({ - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Referer': 'https://ag.ru/games/pc', - 'X-API-Language': 'ru', - 'X-API-Referer': '%2Fgames', - 'X-API-Client': 'website', - }) - - url = f'https://ag.ru/api/games?page_size=20&search={self.game_name}&page=1&key={key}' - rs = self.send_get(url, headers=headers) - - for item in rs.json()['results']: - title = item['name'] - if not self.is_found_game(title): - continue - - genres = [x['name'] for x in item['genres']] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return AgRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Шутеры', 'Экшены', 'Ролевые'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: [] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: ['Экшены', 'Ролевые'] - # - # Search 'Twin Sector'... - # Genres: ['Приключения', 'Экшены'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Шутеры'] diff --git a/html_parsing/get_game_genres/parsers/base_parser.py b/html_parsing/get_game_genres/parsers/base_parser.py deleted file mode 100644 index a9b2e1c4b..000000000 --- a/html_parsing/get_game_genres/parsers/base_parser.py +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from abc import ABCMeta, abstractmethod -from typing import List, Union -from pathlib import Path -import unicodedata - -# For common.py -import sys -sys.path.append('..') - -from bs4 import BeautifulSoup -import requests - -from common import ( - DIR_ERRORS, DIR_LOGS, NEED_LOGS, LOG_FORMAT, USER_AGENT, - pretty_path, get_uniques, get_current_datetime_str, smart_comparing_names, - get_valid_filename -) -import dump - - -class Singleton(ABCMeta): - _instances = dict() - - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - cls._instances[cls] = super().__call__(*args, **kwargs) - - return cls._instances[cls] - - -class BaseParser(metaclass=Singleton): - _site_name = '' - - def __init__(self, need_logs=NEED_LOGS, dir_errors=DIR_ERRORS, dir_logs=DIR_LOGS, log_format=LOG_FORMAT): - self.session = requests.session() - self.session.headers['User-Agent'] = USER_AGENT - - self._dir_errors = pretty_path(dir_errors) - self._dir_logs = pretty_path(dir_logs) - - self.game_name = '' - self._need_logs = need_logs - - self._log = self._get_logger(log_format) - - @classmethod - def instance(cls, *args, **kwargs): - return cls(*args, **kwargs) - - def send_get(self, url: str, return_html=False, **kwargs) -> Union[requests.Response, BeautifulSoup]: - rs = self.session.get(url, **kwargs) - self._on_check_response(rs) - - if return_html: - return BeautifulSoup(rs.content, 'html.parser') - - return rs - - def send_post(self, url: str, data=None, json=None, return_html=False, **kwargs) -> Union[requests.Response, BeautifulSoup]: - rs = self.session.post(url, data=data, json=json, **kwargs) - self._on_check_response(rs) - - if return_html: - return BeautifulSoup(rs.content, 'html.parser') - - return rs - - def _save_error_response(self, rs: requests.Response): - Path(self._dir_errors).mkdir(parents=True, exist_ok=True) - - file_name = pretty_path( - f'{self._dir_errors}/{self.get_site_name()}_{get_valid_filename(self.game_name)}_{get_current_datetime_str()}.dump' - ) - self.log_debug(f'Save dump to {file_name}') - - data = dump.dump_all(rs, request_prefix=b'> ', response_prefix=b'< ') - with open(file_name, 'wb') as f: - f.write(data) - - def _on_check_response(self, rs: requests.Response): - if rs.ok: - return - - self.log_warn(f'Something went wrong...: status_code: {rs.status_code}\n{rs.text}') - self._save_error_response(rs) - - def log_debug(self, msg, *args, **kwargs): - self._need_logs and self._log.debug(msg, *args, **kwargs) - - def log_info(self, msg, *args, **kwargs): - self._need_logs and self._log.info(msg, *args, **kwargs) - - def log_warn(self, msg, *args, **kwargs): - self._need_logs and self._log.warning(msg, *args, **kwargs) - - def log_error(self, msg, *args, **kwargs): - self._need_logs and self._log.error(msg, *args, **kwargs) - - def log_exception(self, msg, *args, **kwargs): - self._need_logs and self._log.exception(msg, *args, **kwargs) - - @classmethod - def get_site_name(cls) -> str: - if not cls._site_name: - import inspect - cls._site_name = Path(inspect.getfile(cls)).stem - return cls._site_name - - @abstractmethod - def _parse(self) -> List[str]: - pass - - def is_found_game(self, game_name: str) -> bool: - return smart_comparing_names(self.game_name, game_name) - - def get_game_genres(self, game_name: str) -> List[str]: - self.game_name = game_name - self.log_info(f'Search {game_name!r}...') - - try: - genres = self._parse() - genres = [x.strip() for x in genres] - genres = get_uniques(genres) - - except SystemExit as e: - raise e - - except BaseException as e: - self.log_exception('Parsing error:') - raise e - - self.log_info(f'Genres: {genres}') - return genres - - # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/163c91d6882b548c904ad40703dac00c0a64e5a2/logger_example.py#L7 - def _get_logger(self, log_format: str, encoding='utf-8'): - Path(self._dir_logs).mkdir(parents=True, exist_ok=True) - - site = self.get_site_name() - - name = 'parser_' + site - file = self._dir_logs + '/' + site + '.txt' - - import logging - log = logging.getLogger(name) - log.setLevel(logging.DEBUG) - - formatter = logging.Formatter(log_format) - - from logging.handlers import RotatingFileHandler - fh = RotatingFileHandler(file, maxBytes=10_000_000, backupCount=5, encoding=encoding) - fh.setFormatter(formatter) - log.addHandler(fh) - - import sys - sh = logging.StreamHandler(stream=sys.stdout) - sh.setFormatter(formatter) - log.addHandler(sh) - - return log - - @classmethod - def get_norm_text(cls, node) -> str: - if not node: - return "" - - text = node.get_text(strip=True) - - # NFKD ™ превратит в TM, что исказит текст, лучше удалить - text = text.replace('™', '').replace('©', '').replace('©', '®') - - # https://ru.wikipedia.org/wiki/Юникод#NFKD - # unicodedata.normalize для удаления \xa0 и подобных символов-заменителей - return unicodedata.normalize("NFKD", text) diff --git a/html_parsing/get_game_genres/parsers/dump.py b/html_parsing/get_game_genres/parsers/dump.py deleted file mode 100644 index 23b35e75a..000000000 --- a/html_parsing/get_game_genres/parsers/dump.py +++ /dev/null @@ -1,197 +0,0 @@ -"""This module provides functions for dumping information about responses.""" -import collections - -from requests import compat - - -__all__ = ('dump_response', 'dump_all') - -HTTP_VERSIONS = { - 9: b'0.9', - 10: b'1.0', - 11: b'1.1', -} - -_PrefixSettings = collections.namedtuple('PrefixSettings', - ['request', 'response']) - - -class PrefixSettings(_PrefixSettings): - def __new__(cls, request, response): - request = _coerce_to_bytes(request) - response = _coerce_to_bytes(response) - return super(PrefixSettings, cls).__new__(cls, request, response) - - -def _get_proxy_information(response): - if getattr(response.connection, 'proxy_manager', False): - proxy_info = {} - request_url = response.request.url - if request_url.startswith('https://'): - proxy_info['method'] = 'CONNECT' - - proxy_info['request_path'] = request_url - return proxy_info - return None - - -def _format_header(name, value): - return (_coerce_to_bytes(name) + b': ' + _coerce_to_bytes(value) + - b'\r\n') - - -def _build_request_path(url, proxy_info): - uri = compat.urlparse(url) - proxy_url = proxy_info.get('request_path') - if proxy_url is not None: - request_path = _coerce_to_bytes(proxy_url) - return request_path, uri - - request_path = _coerce_to_bytes(uri.path) - if uri.query: - request_path += b'?' + _coerce_to_bytes(uri.query) - - return request_path, uri - - -def _dump_request_data(request, prefixes, bytearr, proxy_info=None): - if proxy_info is None: - proxy_info = {} - - prefix = prefixes.request - method = _coerce_to_bytes(proxy_info.pop('method', request.method)) - request_path, uri = _build_request_path(request.url, proxy_info) - - # HTTP/1.1 - bytearr.extend(prefix + method + b' ' + request_path + b' HTTP/1.1\r\n') - - # Host: OR host header specified by user - headers = request.headers.copy() - host_header = _coerce_to_bytes(headers.pop('Host', uri.netloc)) - bytearr.extend(prefix + b'Host: ' + host_header + b'\r\n') - - for name, value in headers.items(): - bytearr.extend(prefix + _format_header(name, value)) - - bytearr.extend(prefix + b'\r\n') - if request.body: - if isinstance(request.body, compat.basestring): - bytearr.extend(prefix + _coerce_to_bytes(request.body)) - else: - # In the event that the body is a file-like object, let's not try - # to read everything into memory. - bytearr.extend(b'<< Request body is not a string-like type >>') - bytearr.extend(b'\r\n') - - -def _dump_response_data(response, prefixes, bytearr): - prefix = prefixes.response - # Let's interact almost entirely with urllib3's response - raw = response.raw - - # Let's convert the version int from httplib to bytes - version_str = HTTP_VERSIONS.get(raw.version, b'?') - - # HTTP/ - bytearr.extend(prefix + b'HTTP/' + version_str + b' ' + - str(raw.status).encode('ascii') + b' ' + - _coerce_to_bytes(response.reason) + b'\r\n') - - headers = raw.headers - for name in headers.keys(): - for value in headers.getlist(name): - bytearr.extend(prefix + _format_header(name, value)) - - bytearr.extend(prefix + b'\r\n') - - bytearr.extend(response.content) - - -def _coerce_to_bytes(data): - if not isinstance(data, bytes) and hasattr(data, 'encode'): - data = data.encode('utf-8') - # Don't bail out with an exception if data is None - return data if data is not None else b'' - - -def dump_response(response, request_prefix=b'< ', response_prefix=b'> ', - data_array=None): - """Dump a single request-response cycle's information. - - This will take a response object and dump only the data that requests can - see for that single request-response cycle. - - Example:: - - import requests - from requests_toolbelt.utils import dump - - resp = requests.get('https://api.github.com/users/sigmavirus24') - data = dump.dump_response(resp) - print(data.decode('utf-8')) - - :param response: - The response to format - :type response: :class:`requests.Response` - :param request_prefix: (*optional*) - Bytes to prefix each line of the request data - :type request_prefix: :class:`bytes` - :param response_prefix: (*optional*) - Bytes to prefix each line of the response data - :type response_prefix: :class:`bytes` - :param data_array: (*optional*) - Bytearray to which we append the request-response cycle data - :type data_array: :class:`bytearray` - :returns: Formatted bytes of request and response information. - :rtype: :class:`bytearray` - """ - data = data_array if data_array is not None else bytearray() - prefixes = PrefixSettings(request_prefix, response_prefix) - - if not hasattr(response, 'request'): - raise ValueError('Response has no associated request') - - proxy_info = _get_proxy_information(response) - _dump_request_data(response.request, prefixes, data, - proxy_info=proxy_info) - _dump_response_data(response, prefixes, data) - return data - - -def dump_all(response, request_prefix=b'< ', response_prefix=b'> '): - """Dump all requests and responses including redirects. - - This takes the response returned by requests and will dump all - request-response pairs in the redirect history in order followed by the - final request-response. - - Example:: - - import requests - from requests_toolbelt.utils import dump - - resp = requests.get('https://httpbin.org/redirect/5') - data = dump.dump_all(resp) - print(data.decode('utf-8')) - - :param response: - The response to format - :type response: :class:`requests.Response` - :param request_prefix: (*optional*) - Bytes to prefix each line of the request data - :type request_prefix: :class:`bytes` - :param response_prefix: (*optional*) - Bytes to prefix each line of the response data - :type response_prefix: :class:`bytes` - :returns: Formatted bytes of request and response information. - :rtype: :class:`bytearray` - """ - data = bytearray() - - history = list(response.history[:]) - history.append(response) - - for response in history: - dump_response(response, request_prefix, response_prefix, data) - - return data diff --git a/html_parsing/get_game_genres/parsers/gamebomb_ru.py b/html_parsing/get_game_genres/parsers/gamebomb_ru.py deleted file mode 100644 index cb7c17d36..000000000 --- a/html_parsing/get_game_genres/parsers/gamebomb_ru.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from urllib.parse import urljoin -from typing import List - -from base_parser import BaseParser - - -class GamebombRu_Parser(BaseParser): - def _parse(self) -> List[str]: - headers = { - 'X-Requested-With': 'XMLHttpRequest', - 'Origin': 'http://gamebomb.ru', - 'Referer': 'http://gamebomb.ru/games', - 'Accept': 'application/json', - } - data = { - 'query': self.game_name, - 'type': '', - } - - url = f'http://gamebomb.ru/base/ajaxSearch' - rs = self.send_post(url, data=data, headers=headers) - - for game_block_preview in rs.json(): - if game_block_preview['type'] != 'игра': - continue - - title = game_block_preview['title'] - if not self.is_found_game(title): - continue - - url_game = urljoin(rs.url, game_block_preview['url']) - self.log_info(f'Load {url_game!r}') - - game_block = self.send_get(url_game, return_html=True) - # - # Жанры - # - #
- # - # - # Шутер от первого лица - #
- #
- # - # - # Боевик-приключения - #
- game_block = game_block.find('td', text='Жанры') - if not game_block: - continue - - genres = [] - for div in game_block.find_next_sibling('td').find_all('div'): - if not div.select('input[name*="genres"]'): - continue - - genres.append(self.get_norm_text(div)) - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return GamebombRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Боевик/Экшн', 'Ролевые игры', 'Шутер от первого лица', 'ММОРПГ/Многопользоватеские онлайновые'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Боевик/Экшн', 'Ролевые игры'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: ['Головоломки/Пазл', 'Шутер от первого лица'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Боевик/Экшн', 'Приключения', 'Шутер от первого лица'] diff --git a/html_parsing/get_game_genres/parsers/gamefaqs_gamespot_com.py b/html_parsing/get_game_genres/parsers/gamefaqs_gamespot_com.py deleted file mode 100644 index f2bc18c81..000000000 --- a/html_parsing/get_game_genres/parsers/gamefaqs_gamespot_com.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from urllib.parse import urljoin -from typing import List - -from base_parser import BaseParser - - -class GamefaqsGamespotCom_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://gamefaqs.gamespot.com/search?game={self.game_name}' - root = self.send_get(url, return_html=True) - - for game_block_preview in root.select('.search_results_title > .search_result'): - a = game_block_preview.select_one('.sr_name > a.log_search') - title = self.get_norm_text(a) - if not self.is_found_game(title): - continue - - url_game = urljoin(url, a['href']) - self.log_info(f'Load {url_game!r}') - - game_block = self.send_get(url_game, return_html=True) - game_info = game_block.select_one('.pod_gameinfo_left') - if not game_info: - return [] - - #
  • Genre: - # Action » - # Shooter » - # Third-Person » - # Arcade - #
  • - genres = [ - self.get_norm_text(a) - for a in game_info.select_one('li > b:contains("Genre:")').find_next_siblings('a') - ] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return GamefaqsGamespotCom_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Role-Playing', 'Western-Style'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Role-Playing', 'Action RPG'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: ['Role-Playing', 'Action RPG'] - # - # Search 'Twin Sector'... - # Genres: ['Action Adventure', 'Linear'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Action Adventure', 'Survival'] diff --git a/html_parsing/get_game_genres/parsers/gameguru_ru.py b/html_parsing/get_game_genres/parsers/gameguru_ru.py deleted file mode 100644 index 3722f88b3..000000000 --- a/html_parsing/get_game_genres/parsers/gameguru_ru.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import re -from typing import List - -from base_parser import BaseParser - - -class GameguruRu_Parser(BaseParser): - def _parse(self) -> List[str]: - url_search = f'https://gameguru.ru/games/?search={self.game_name}' - - page = last_page = 1 - while page <= last_page: - url = url_search - if page > 1: - url = f'{url_search}&page={page}' - - self.log_info(f'Load {url!r}') - root = self.send_get(url, return_html=True) - - for game_block in root.select('#publications-wrap .short-news-content'): - title = self.get_norm_text(game_block.select_one('.short-news-title')) - if not self.is_found_game(title): - continue - - for info in game_block.select('.short-news-play-info'): - if 'Жанр'.upper() not in self.get_norm_text(info).upper(): - continue - - return [self.get_norm_text(x) for x in info.select('div > span')] - - # Обновление номера последней страницы - pages = root.select('.pagination a.page-link[href]') - if pages: - href = pages[-1]['href'] - m = re.search(r'&page=(\d+)', href) - last_page = int(m.group(1)) - - page += 1 - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return GameguruRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['RPG'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Экшен', 'RPG'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: ['RPG', 'aRPG'] - # - # Search 'Twin Sector'... - # Genres: ['Экшен'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Экшен', 'Шутер', 'Квест'] diff --git a/html_parsing/get_game_genres/parsers/gamer_info_com.py b/html_parsing/get_game_genres/parsers/gamer_info_com.py deleted file mode 100644 index b75087a2b..000000000 --- a/html_parsing/get_game_genres/parsers/gamer_info_com.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List - -from base_parser import BaseParser - - -class GamerInfoCom_Parser(BaseParser): - def _parse(self) -> List[str]: - headers = { - 'X-Requested-With': 'XMLHttpRequest', - } - form_data = { - 'search-query': self.game_name, - 'search-obl': 'games', - 'page': '1', - } - - url = 'https://gamer-info.com/search-q/' - root = self.send_post(url, headers=headers, data=form_data, return_html=True) - - for game_block in root.select('.games > .c2'): - g = game_block.select_one('.g') - if 'Жанр:' not in g.text: - continue - - title = self.get_norm_text(game_block.select_one('.n')) - if not self.is_found_game(title): - continue - - genres = g.text.replace('Жанр:', '').strip().split(', ') - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return GamerInfoCom_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['action', 'RPG'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: [] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: ['action', 'приключения'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['action', 'приключения'] diff --git a/html_parsing/get_game_genres/parsers/gamespot_com.py b/html_parsing/get_game_genres/parsers/gamespot_com.py deleted file mode 100644 index 6ad5639d0..000000000 --- a/html_parsing/get_game_genres/parsers/gamespot_com.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from urllib.parse import urljoin -from typing import List - -from base_parser import BaseParser - - -class GamespotCom_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://www.gamespot.com/search/?i=site&q={self.game_name}' - root = self.send_get(url, return_html=True) - - for game_block_preview in root.select('.media-body'): - if not game_block_preview.select_one('.media-date'): - continue - - a = game_block_preview.select_one('.media-title a') - title = self.get_norm_text(a) - if not self.is_found_game(title): - continue - - url_game = urljoin(url, a['href']) - self.log_info(f'Load {url_game!r}') - - game_block = self.send_get(url_game, return_html=True) - - tag_object_stats = game_block.select_one('#object-stats-wrap') - if not tag_object_stats: - return [] - - genres = [self.get_norm_text(a) for a in tag_object_stats.select('a[href]') if '/genre/' in a['href']] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return GamespotCom_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Role-Playing'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Role-Playing', 'Action'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: ['Action', 'Adventure'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Adventure', 'Survival', '3D', 'Action'] diff --git a/html_parsing/get_game_genres/parsers/igromania_ru.py b/html_parsing/get_game_genres/parsers/igromania_ru.py deleted file mode 100644 index de3caf917..000000000 --- a/html_parsing/get_game_genres/parsers/igromania_ru.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List - -from base_parser import BaseParser - - -class IgromaniaRu_Parser(BaseParser): - def _parse(self) -> List[str]: - headers = { - 'X-Requested-With': 'XMLHttpRequest', - } - form_data = { - 'mode': '11', - 's': '1', - 'p': '1', - 'fg': 'all', - 'fp': 'all', - 'fn': self.game_name - } - - url = 'https://www.igromania.ru/-Engine-/AJAX/games.list.v2/index.php' - root = self.send_post(url, headers=headers, data=form_data, return_html=True) - - for game_block in root.select('.gamebase_box'): - title = self.get_norm_text(game_block.select_one('.release_name')) - if not self.is_found_game(title): - continue - - genres = [self.get_norm_text(a) for a in game_block.select('.genre > a')] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return IgromaniaRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Боевик', 'Боевик от первого лица', 'Боевик от третьего лица', 'Ролевая игра'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Ролевая игра', 'Боевик', 'Боевик от третьего лица'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: ['Боевик', 'Боевик от первого лица'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Боевик', 'Ужасы', 'Боевик от первого лица', 'Приключение'] diff --git a/html_parsing/get_game_genres/parsers/iwantgames_ru.py b/html_parsing/get_game_genres/parsers/iwantgames_ru.py deleted file mode 100644 index 2370778a0..000000000 --- a/html_parsing/get_game_genres/parsers/iwantgames_ru.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List - -from base_parser import BaseParser - - -class IwantgamesRu_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://iwantgames.ru/?s={self.game_name}' - root = self.send_get(url, return_html=True) - - for game_block in root.select('.game__content'): - title = self.get_norm_text(game_block.h2.a) - if not self.is_found_game(title): - continue - - #
    Жанр:
    - #
    - # РПГ, - # Ужасы, - # Экшен - #
    - # -> ['РПГ', 'Ужасы', 'Экшен'] - dt = game_block.find('dt', text='Жанр:') - if not dt: - continue - - dd = dt.find_next_sibling('dd') - if not dd: - continue - - genres = [self.get_norm_text(a) for a in dd.find_all('a')] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return IwantgamesRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test, TEST_GAMES - TEST_GAMES.append('Dark Souls: Remastered') - - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: [] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: [] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: [] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: [] - # - # Search 'Dark Souls: Remastered'... - # Genres: ['РПГ', 'Ужасы', 'Экшен'] diff --git a/html_parsing/get_game_genres/parsers/metacritic_com.py b/html_parsing/get_game_genres/parsers/metacritic_com.py deleted file mode 100644 index e63792362..000000000 --- a/html_parsing/get_game_genres/parsers/metacritic_com.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from urllib.parse import urljoin -from typing import List - -from base_parser import BaseParser - - -class MetacriticCom_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://www.metacritic.com/search/game/{self.game_name}/results' - root = self.send_get(url, return_html=True) - - for game_block_preview in root.select('.result'): - a = game_block_preview.select_one('.product_title > a') - title = self.get_norm_text(a) - if not self.is_found_game(title): - continue - - url_game = urljoin(url, a['href']) - self.log_info(f'Load {url_game!r}') - - game_block = self.send_get(url_game, return_html=True) - #
  • - # Genre(s): - # Role-Playing, - # Action RPG - #
  • - genres = [ - self.get_norm_text(a) for a in game_block.select('.summary_detail.product_genre > .data') - ] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return MetacriticCom_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Role-Playing', 'First-Person', 'First-Person', 'Western-Style'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Role-Playing', 'Action RPG', 'Action RPG'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: ['Role-Playing', 'Action RPG', 'Action RPG'] - # - # Search 'Twin Sector'... - # Genres: ['Action Adventure', 'Modern', 'General', 'Modern', 'Linear'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Action Adventure', 'Horror', 'Survival'] diff --git a/html_parsing/get_game_genres/parsers/mobygames_com.py b/html_parsing/get_game_genres/parsers/mobygames_com.py deleted file mode 100644 index 1218c8f19..000000000 --- a/html_parsing/get_game_genres/parsers/mobygames_com.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from urllib.parse import urljoin -from typing import List - -from base_parser import BaseParser - - -class MobygamesCom_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://www.mobygames.com/search/quick?q={self.game_name}&p=3&search=Go&sFilter=1&sG=on' - root = self.send_get(url, return_html=True) - - for game_block_preview in root.select('.searchTitle > a'): - title = self.get_norm_text(game_block_preview) - if not self.is_found_game(title): - continue - - href = game_block_preview['href'] - url_game = urljoin(url, href) - - self.log_info(f'Load {url_game!r}') - - game_block = self.send_get(url_game, return_html=True) - genres = game_block\ - .select_one('#coreGameGenre').find_next('div', text='Genre')\ - .find_next_sibling('div').find_all('a') - - genres = [self.get_norm_text(a) for a in genres] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return MobygamesCom_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Role-Playing (RPG)'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Action', 'Role-Playing (RPG)'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: ['Action', 'Role-Playing (RPG)'] - # - # Search 'Twin Sector'... - # Genres: ['Action'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Action'] diff --git a/html_parsing/get_game_genres/parsers/playground_ru.py b/html_parsing/get_game_genres/parsers/playground_ru.py deleted file mode 100644 index 3c1011339..000000000 --- a/html_parsing/get_game_genres/parsers/playground_ru.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from urllib.parse import urljoin -from typing import List - -from base_parser import BaseParser - - -class PlaygroundRu_Parser(BaseParser): - base_url = 'https://www.playground.ru' - - def _parse(self) -> List[str]: - url = f'{self.base_url}/api/game.search?query={self.game_name}&include_addons=1' - data = self.send_get(url).json() - - for game in data: - title = game['name'] - if not self.is_found_game(title): - continue - - url_game = urljoin(self.base_url, game['slug']) - self.log_info(f'Load {url_game!r}') - - game_block = self.send_get(url_game, return_html=True) - genres = [ - x['content'] for x in game_block.select('.genres > [itemprop="genre"]') - ] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return PlaygroundRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Экшен', 'Ролевая', 'От первого лица', 'От третьего лица', 'Фэнтези'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Экшен', 'Ролевая', 'Стимпанк'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: ['Экшен'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Экшен', 'Адвенчура', 'Ужасы', 'От первого лица'] diff --git a/html_parsing/get_game_genres/parsers/spong_com.py b/html_parsing/get_game_genres/parsers/spong_com.py deleted file mode 100644 index 302ed6a2b..000000000 --- a/html_parsing/get_game_genres/parsers/spong_com.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List - -from base_parser import BaseParser - - -class SpongCom_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://spong.com/search/index.jsp?q={self.game_name}' - root = self.send_get(url, return_html=True) - - # Первая таблица -- та, что нужна нам - result = root.select_one('table.searchResult') - if result: - for game_block in result.select('tr'): - tds = game_block.select('td') - if len(tds) != 4: # Например, tr > th - continue - - td_title, _, genres_td, platforms_td = tds - - title = self.get_norm_text(td_title.a) - if not self.is_found_game(title): - continue - - # Adventure: Free Roaming
    Adventure: Survival Horror
    - # -> ['Adventure: Free Roaming', 'Adventure: Survival Horror'] - genres = list(genres_td.stripped_strings) - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return SpongCom_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Adventure: Survival Horror'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Adventure: Role Playing'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: ['Adventure: Role Playing'] - # - # Search 'Twin Sector'... - # Genres: [] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Adventure: Survival Horror'] diff --git a/html_parsing/get_game_genres/parsers/squarefaction_ru.py b/html_parsing/get_game_genres/parsers/squarefaction_ru.py deleted file mode 100644 index 6e982ab8f..000000000 --- a/html_parsing/get_game_genres/parsers/squarefaction_ru.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List -from bs4 import BeautifulSoup -from base_parser import BaseParser - - -class SquarefactionRu_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'http://squarefaction.ru/main/search/games?q={self.game_name}' - rs = self.send_get(url) - - root = BeautifulSoup(rs.content, 'html.parser') - - # http://squarefaction.ru/main/search/games?q=dead+space - if '/main/search/games' in rs.url: - self.log_info(f'Parsing of game list') - - for game_block in root.select('#games > .entry'): - title = self.get_norm_text(game_block.select_one('.name')) - if not self.is_found_game(title): - continue - - #
    TPS,Survival Horror,Action
    - genres = self.get_norm_text(game_block.select_one('.infos')).split(',') - - # Сойдет первый, совпадающий по имени, вариант - return genres - - # http://squarefaction.ru/game/dead-space - else: - self.log_info(f'Parsing of game page') - - game_block = root.select_one('#page-info') - if game_block: - title = self.get_norm_text(game_block.select_one('#title')) - if not self.is_found_game(title): - self.log_warn(f'Not match game title {title!r}') - - # - # TPS, - # Survival Horror, - # Action - # - genres = [ - self.get_norm_text(a) for a in game_block.select('a') if '?genre=' in a['href'] - ] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return SquarefactionRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Action RPG'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Action RPG'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: [] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['Survival Horror'] diff --git a/html_parsing/get_game_genres/parsers/stopgame_ru.py b/html_parsing/get_game_genres/parsers/stopgame_ru.py deleted file mode 100644 index 2ba735a4b..000000000 --- a/html_parsing/get_game_genres/parsers/stopgame_ru.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from typing import List - -from base_parser import BaseParser - - -class StopgameRu_Parser(BaseParser): - def _parse(self) -> List[str]: - url = f'https://stopgame.ru/search/?s={self.game_name}&where=games&sort=name' - root = self.send_get(url, return_html=True) - - for game_block in root.select('.game-summary'): - title = self.get_norm_text(game_block.select_one('.caption')) - if not self.is_found_game(title): - continue - - # Сойдет первый, совпадающий по имени, вариант - return [ - self.get_norm_text(a) for a in game_block.select('a[href*="?genre[]"]') - ] - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return StopgameRu_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['action', 'rpg'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['rpg'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: [] - # - # Search 'Twin Sector'... - # Genres: ['action', 'logic'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: ['action', 'adventure'] diff --git a/html_parsing/get_game_genres/parsers/store_steampowered_com.py b/html_parsing/get_game_genres/parsers/store_steampowered_com.py deleted file mode 100644 index b0106e642..000000000 --- a/html_parsing/get_game_genres/parsers/store_steampowered_com.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from urllib.parse import urljoin -from typing import List - -from base_parser import BaseParser - - -class StoreSteampoweredCom_Parser(BaseParser): - def _parse(self) -> List[str]: - # category1 = Игры - url = f'https://store.steampowered.com/search/?term={self.game_name}&category1=998' - root = self.send_get(url, return_html=True) - - for game_block_preview in root.select('.search_result_row'): - title = self.get_norm_text(game_block_preview.select_one('.search_name > .title')) - if not self.is_found_game(title): - continue - - href = game_block_preview['href'] - url_game = urljoin(url, href) - self.log_info(f'Load {url_game!r}') - - game_block = self.send_get(url_game, return_html=True) - #
    - # Title: HELLGATE: London
    - # Genre: - # Action, - # RPG - genres = [ - self.get_norm_text(a) for a in game_block.select('.details_block > a[href*="/genre/"]') - ] - - # Сойдет первый, совпадающий по имени, вариант - return genres - - self.log_info(f'Not found game {self.game_name!r}') - return [] - - -def get_game_genres(game_name: str, *args, **kwargs) -> List[str]: - return StoreSteampoweredCom_Parser(*args, **kwargs).get_game_genres(game_name) - - -if __name__ == '__main__': - from common import _common_test - _common_test(get_game_genres) - - # Search 'Hellgate: London'... - # Genres: ['Action', 'RPG'] - # - # Search 'The Incredible Adventures of Van Helsing'... - # Genres: ['Action', 'Adventure', 'Indie', 'RPG'] - # - # Search 'Dark Souls: Prepare to Die Edition'... - # Genres: ['Action', 'RPG'] - # - # Search 'Twin Sector'... - # Genres: ['Action', 'Adventure'] - # - # Search 'Call of Cthulhu: Dark Corners of the Earth'... - # Genres: [] diff --git a/html_parsing/get_game_genres/third_party/README.md b/html_parsing/get_game_genres/third_party/README.md deleted file mode 100644 index 6251861cd..000000000 --- a/html_parsing/get_game_genres/third_party/README.md +++ /dev/null @@ -1,2 +0,0 @@ -### SOURCE: - * [add_notify_telegram.py](https://github.com/gil9red/telegram_notifications_bot/blob/c1077c3971f6ad1f1daa8abb81921a5f67cfc5ff/add_notify_use_web.py) diff --git a/html_parsing/get_game_genres/third_party/add_notify_telegram.py b/html_parsing/get_game_genres/third_party/add_notify_telegram.py deleted file mode 100644 index 95d12f46e..000000000 --- a/html_parsing/get_game_genres/third_party/add_notify_telegram.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import time -import requests - - -HOST = '127.0.0.1' -PORT = 10016 - -URL = f'http://{HOST}:{PORT}/add_notify' - - -def add_notify( - name: str, - message: str, - type: str = 'INFO', - url: str = None, - has_delete_button: bool = False, -): - data = { - 'name': name, - 'message': message, - 'type': type, - 'url': url, - 'has_delete_button': has_delete_button, - } - - # Попытки - attempts_timeouts = [1, 5, 10, 30, 60] - - while True: - try: - rs = requests.post(URL, json=data) - rs.raise_for_status() - return - - except Exception as e: - # Если закончились попытки - if not attempts_timeouts: - raise e - - timeout = attempts_timeouts.pop(0) - time.sleep(timeout) - - -if __name__ == '__main__': - add_notify('TEST', 'Hello World! Привет мир!') - add_notify('', 'Hello World! Привет мир!') - add_notify('Ошибка!', 'Hello World! Привет мир!', 'ERROR') - add_notify('TEST', 'With url-button!', url='https://example.com/') - add_notify('TEST', 'With delete-button!', has_delete_button=True) - add_notify('TEST', 'With url and delete buttons!', url='https://example.com/', has_delete_button=True) diff --git a/html_parsing/get_population_from_wikidata.py b/html_parsing/get_population_from_wikidata.py index b298fdfe9..d845cdd57 100644 --- a/html_parsing/get_population_from_wikidata.py +++ b/html_parsing/get_population_from_wikidata.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import requests @@ -10,17 +10,17 @@ def get_populations(url: str) -> dict: rs = requests.get(url) - root = BeautifulSoup(rs.content, 'html.parser') + root = BeautifulSoup(rs.content, "html.parser") # P1082 -- идентификатор для population - population_node = root.select_one('#P1082') + population_node = root.select_one("#P1082") populations = dict() # Перебор строк в соседнем от population столбце - for row in population_node.select('.wikibase-statementview'): + for row in population_node.select(".wikibase-statementview"): # Небольшая хитрость -- берем только первые 2 значения, поидеи это будут: количество людей и дата - number_str, data_str = row.select('.wikibase-snakview-value')[:2] + number_str, data_str = row.select(".wikibase-snakview-value")[:2] # Вытаскиваем текст из number_str = number_str.text.strip() @@ -48,14 +48,15 @@ def get_population_from_url_by_year(url: str, year: int) -> str: return get_population_by_year(populations, year) -if __name__ == '__main__': - url = 'https://www.wikidata.org/wiki/Q148' +if __name__ == "__main__": + url = "https://www.wikidata.org/wiki/Q148" populations = get_populations(url) - print(populations) # {2012: '1,375,198,619', 2010: '1,359,755,102', 2015: '1,397,028,553', ... + print(populations) + # {2012: '1,375,198,619', 2010: '1,359,755,102', 2015: '1,397,028,553', ... # Выводим данные с сортировкой по ключу: по возрастанию for year in sorted(populations): - print("{}: {}".format(year, populations[year])) + print(f"{year}: {populations[year]}") # 2010: 1,359,755,102 # 2011: 1,367,480,264 diff --git a/html_parsing/get_price_game/from_direct2drive.py b/html_parsing/get_price_game/from_direct2drive.py index 61637d174..5c7c77cfb 100644 --- a/html_parsing/get_price_game/from_direct2drive.py +++ b/html_parsing/get_price_game/from_direct2drive.py @@ -1,20 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -text = 'titan' -url = 'https://www.direct2drive.com/backend/api/productquery/findpage?search.keywords=' + text +import requests -import requests +text = "titan" +url = "https://www.direct2drive.com/backend/api/productquery/findpage?search.keywords={text}" + rs = requests.get(url) print(rs) data = rs.json() print(data) -for game in data['products']['items']: - offer = game['offerActions'][0] - print(game['title'], offer['purchasePrice']['amount'] + ' ' + offer['purchasePrice']['currency']['isoCode']) +for game in data["products"]["items"]: + offer = game["offerActions"][0] + print( + game["title"], + offer["purchasePrice"]["amount"] + + " " + + offer["purchasePrice"]["currency"]["isoCode"], + ) diff --git a/html_parsing/get_price_game/from_gama-gama.py b/html_parsing/get_price_game/from_gama-gama.py index eec7bfe0a..46337d083 100644 --- a/html_parsing/get_price_game/from_gama-gama.py +++ b/html_parsing/get_price_game/from_gama-gama.py @@ -1,17 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import re + +from bs4 import BeautifulSoup + +from PyQt5.QtCore import QUrl +from PyQt5.QtWidgets import QApplication +from PyQt5.QtWebEngineWidgets import QWebEnginePage # Основа взята из http://stackoverflow.com/a/37755811/5909792 def get_html(url): - from PyQt5.QtCore import QUrl - from PyQt5.QtWidgets import QApplication - from PyQt5.QtWebEngineWidgets import QWebEnginePage - class ExtractorHtml: - def __init__(self, url): + def __init__(self, url) -> None: _app = QApplication([]) self._page = QWebEnginePage() self._page.loadFinished.connect(self._load_finished_handler) @@ -35,10 +40,10 @@ def __init__(self, url): # Чтобы избежать падений скрипта self._page = None - def _callable(self, data): + def _callable(self, data) -> None: self.html = data - def _load_finished_handler(self, _): + def _load_finished_handler(self, _) -> None: self._counter_finished += 1 if self._counter_finished == 2: @@ -47,35 +52,31 @@ def _load_finished_handler(self, _): return ExtractorHtml(url).html -text = 'mad' -url = 'http://gama-gama.ru/search/?searchField=' + text +text = "mad" +url = f"http://gama-gama.ru/search/?searchField={text}" html = get_html(url) -from bs4 import BeautifulSoup -root = BeautifulSoup(html, 'lxml') +root = BeautifulSoup(html, "lxml") -for game in root.select('.catalog-content > a'): - name = game['title'].strip() - name = name.replace('Купить ', '') +for game in root.select(".catalog-content > a"): + name = game["title"].strip() + name = name.replace("Купить ", "") price = None - price_holder = game.select_one('.catalog_price_holder') + price_holder = game.select_one(".catalog_price_holder") - price_1 = price_holder.select_one('.price_1') + price_1 = price_holder.select_one(".price_1") if price_1: price = price_1.text.strip() else: # Содержит описание цены со скидкой. Вытаскиваем цену со скидкой - price_2 = price_holder.select_one('.price_2') + price_2 = price_holder.select_one(".price_2") if price_2: - price = price_2.select_one('.price_group > .promo_price').text + price = price_2.select_one(".price_group > .promo_price").text # Удаление пустых символов пробелом - import re - price = re.sub(r'\s+', ' ', price) - - price = price.strip() + price = re.sub(r"\s+", " ", price).strip() print(name, price) diff --git a/html_parsing/get_price_game/from_gog.py b/html_parsing/get_price_game/from_gog.py index b134b3787..08aa0bcb7 100644 --- a/html_parsing/get_price_game/from_gog.py +++ b/html_parsing/get_price_game/from_gog.py @@ -1,26 +1,47 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import sys import requests -text = 'titan quest' -url = 'https://www.gog.com/games/ajax/filtered?language=en&mediaType=game&page=1&sort=bestselling' \ - '&system=windows_10,windows_7,windows_8,windows_vista,windows_xp&search=' + text +session = requests.Session() +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0" -rs = requests.get(url) -print(rs) -data = rs.json() -print(data) +def get_games(name: str) -> list[tuple[str, str]]: + url = ( + "https://www.gog.com/games/ajax/filtered?" + f"language=ru&mediaType=game&page=1&search={name}" + ) -if not data['totalGamesFound']: - print('Not found game') - sys.exit() + rs = session.get(url) + rs.raise_for_status() -for game in data['products']: - print(game['title'], game['price']['amount'] + game['price']['symbol']) + data = rs.json() + + return [ + (game["title"], game["price"]["baseAmount"]) + for game in data["products"] + ] + + +if __name__ == "__main__": + print(get_games("titan quest")) + # [('Titan Quest: Eternal Embers', '649'), ('Titan Quest Anniversary Edition', '649'), ('Titan Quest: Atlantis', '449'), ('Titan Quest: Ragnarök', '649')] + + print(get_games("Titan Quest: Atlantis")) + # [('Titan Quest: Atlantis', '449')] + + print(get_games("Prodeus")) + # [('Prodeus', '465')] + + print(get_games("Psychonauts 2")) + # [] + + print(get_games("dfsfsdfdsf")) + # [] diff --git a/html_parsing/get_price_game/from_gog_v2.py b/html_parsing/get_price_game/from_gog_v2.py new file mode 100644 index 000000000..1a007b5f4 --- /dev/null +++ b/html_parsing/get_price_game/from_gog_v2.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import requests + + +session = requests.Session() +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0" + + +def get_games(name: str) -> list[tuple[str, str]]: + url = ( + f"https://catalog.gog.com/v1/catalog?" + f"countryCode=RU¤cyCode=RUB&limit=20" + f"&locale=ru-RU&order=desc:score" + f"&page=1&productType=in:game,pack,dlc,extras" + f"&query=like:{name}" + ) + + rs = session.get(url) + rs.raise_for_status() + + data = rs.json() + return [ + (game["title"], game["price"]["baseMoney"]["amount"]) + for game in data["products"] + if game["price"] and game["price"]["baseMoney"]["currency"] == "RUB" + ] + + +if __name__ == "__main__": + print(get_games("titan quest")) + # [('Titan Quest: Eternal Embers', '649.00'), ('Titan Quest: Atlantis', '449.00'), ('Titan Quest: Ragnarök', '649.00'), ('Titan Quest Anniversary Edition', '649.00')] + + print(get_games("Titan Quest: Atlantis")) + # [('Titan Quest: Atlantis', '449.00')] + + print(get_games("Prodeus")) + # [('Prodeus', '465.00'), ('Prodeus MIDI Soundtrack', '0.00'), ('Prodeus Soundtrack', '892.00')] + + print(get_games("Psychonauts 2")) + # [('Psychonauts 2', '1085.00'), ('Psychonauts', '249.00')] + + print(get_games("dfsfsdfdsf")) + # [] diff --git a/html_parsing/get_price_game/from_origin.py b/html_parsing/get_price_game/from_origin.py index cd5c1ce97..833aa63e2 100644 --- a/html_parsing/get_price_game/from_origin.py +++ b/html_parsing/get_price_game/from_origin.py @@ -1,17 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +from bs4 import BeautifulSoup + +from PyQt5.QtCore import QUrl, QTimer +from PyQt5.QtWidgets import QApplication +from PyQt5.QtWebEngineWidgets import QWebEnginePage # Основа взята из http://stackoverflow.com/a/37755811/5909792 def get_html(url): - from PyQt5.QtCore import QUrl, QTimer - from PyQt5.QtWidgets import QApplication - from PyQt5.QtWebEngineWidgets import QWebEnginePage - class ExtractorHtml: - def __init__(self, url): + def __init__(self, url) -> None: _app = QApplication([]) self._page = QWebEnginePage() @@ -40,35 +43,32 @@ def __init__(self, url): self._page = None - def _callable(self, data): + def _callable(self, data) -> None: self.html = data - def _load_finished_handler(self): + def _load_finished_handler(self) -> None: self._page.toHtml(self._callable) return ExtractorHtml(url).html -text = 'titan' -url = 'https://www.origin.com/rus/ru-ru/search?searchString=' + text +text = "titan" +url = f"https://www.origin.com/rus/ru-ru/search?searchString={text}" html = get_html(url) +root = BeautifulSoup(html, "lxml") - -from bs4 import BeautifulSoup -root = BeautifulSoup(html, 'lxml') - -for game in root.select('.origin-search-section .origin-storegametile-details'): - name = game.select_one('.origin-storegametile-title').text.strip() +for game in root.select(".origin-search-section .origin-storegametile-details"): + name = game.select_one(".origin-storegametile-title").text.strip() price = None - storeprice = game.select_one('.origin-storeprice') + storeprice = game.select_one(".origin-storeprice") - otkprice = storeprice.select_one('.otkprice') + otkprice = storeprice.select_one(".otkprice") if otkprice: price = otkprice.text.strip() else: - free = storeprice.select_one('.otkprice-free') + free = storeprice.select_one(".otkprice-free") if free: price = 0 diff --git a/html_parsing/get_price_game/from_shop_buka_ru.py b/html_parsing/get_price_game/from_shop_buka_ru.py index 94bc32af1..fc6a4c853 100644 --- a/html_parsing/get_price_game/from_shop_buka_ru.py +++ b/html_parsing/get_price_game/from_shop_buka_ru.py @@ -1,23 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -text = 'titan' +import requests +from bs4 import BeautifulSoup -post_data = { - 'search': text, -} -import requests -rs = requests.post('http://shop.buka.ru/search', data=post_data) +def search(text: str) -> list[tuple[str, str]]: + rs = requests.post("https://shop.buka.ru/search", params=dict(q=text)) + rs.raise_for_status() -from bs4 import BeautifulSoup -root = BeautifulSoup(rs.content, 'lxml') + items = [] + root = BeautifulSoup(rs.content, "html.parser") + for game in root.select(".product-thumb"): + name = game["data-name"] + price = game["data-price"] + + items.append((name, price)) + + return items -for game in root.select('.product'): - name = game.select_one('.name').text.strip() - price = game.select_one('.costs .c2').text.strip() - print(name, price) +if __name__ == "__main__": + text = "titan" + for name, price in search(text): + print(name, price) diff --git a/html_parsing/get_price_game/from_steam.py b/html_parsing/get_price_game/from_steam.py index a0d1b0e9c..2a6dd97a5 100644 --- a/html_parsing/get_price_game/from_steam.py +++ b/html_parsing/get_price_game/from_steam.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re @@ -12,32 +12,31 @@ def search_game_price_list(name: str) -> list: # category1 = 998 (Game) - url = 'https://store.steampowered.com/search/?category1=998&os=win&supportedlang=english&term=' + name + url = f"https://store.steampowered.com/search/?category1=998&os=win&supportedlang=english&term={name}" game_price_list = [] rs = requests.get(url) if not rs.ok: - print(f'Что-то пошло не так: {rs.status_code}\n{rs.text}') + print(f"Что-то пошло не так: {rs.status_code}\n{rs.text}") return game_price_list - root = BeautifulSoup(rs.content, 'html.parser') + root = BeautifulSoup(rs.content, "html.parser") - for div in root.select('.search_result_row'): - name = div.select_one('.title').text.strip() + for div in root.select(".search_result_row"): + name = div.select_one(".title").text.strip() - # Ищем тег скидки - if div.select_one('.search_discount > span'): - price = div.select_one('.search_price > span > strike').text.strip() - else: - price = div.select_one('.search_price').text.strip() + # Ищем тег скидки, чтобы вытащить оригинальную цену, а не ту, что получилась со скидкой + price_el = div.select_one(".discount_original_price") or div.select_one(".discount_final_price") # Если цены нет (например, игра еще не продается) - if not price: + if not price_el: price = None else: + price = price_el.get_text(strip=True) + # Если в цене нет цифры считаем что это "Free To Play" или что-то подобное - match = re.search(r'\d', price) + match = re.search(r"\d", price) if not match: price = 0 @@ -46,14 +45,14 @@ def search_game_price_list(name: str) -> list: return game_price_list -if __name__ == '__main__': - text = 'resident evil 6' +if __name__ == "__main__": + text = "resident evil 6" game_price_list = search_game_price_list(text) for name, price in game_price_list: - print(name, price, sep=' / ') + print(name, price, sep=" / ") print() - text = 'prey' + text = "prey" game_price_list = search_game_price_list(text) for name, price in game_price_list: - print(name, price, sep=' / ') + print(name, price, sep=" / ") diff --git a/html_parsing/get_product_with_category cenoteka.rs.py b/html_parsing/get_product_with_category cenoteka.rs.py index 5fa7074a0..6b27238dc 100644 --- a/html_parsing/get_product_with_category cenoteka.rs.py +++ b/html_parsing/get_product_with_category cenoteka.rs.py @@ -1,29 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/questions/894302/ -from bs4 import BeautifulSoup import requests +from bs4 import BeautifulSoup -url = 'https://cenoteka.rs/proizvodi/mlecni-proizvodi/jogurt?page=3' +url = "https://cenoteka.rs/proizvodi/mlecni-proizvodi/jogurt?page=3" rs = requests.get(url) -root = BeautifulSoup(rs.content, 'lxml') +root = BeautifulSoup(rs.content, "lxml") -for category in root.select('#products > .row.section'): - category_title = category.select_one('.section-title').get_text(strip=True) +for category in root.select("#products > .row.section"): + category_title = category.select_one(".section-title").get_text(strip=True) print(category_title) - for product in category.select('[data-product-id]'): + for product in category.select("[data-product-id]"): try: - title = product.select_one('.article-name').get_text(strip=True) - price_list = [price.get_text(strip=True) for price in product.select('.article-price')] - print(' {}: {}'.format(title, price_list)) + title = product.select_one(".article-name").get_text(strip=True) + price_list = [ + price.get_text(strip=True) + for price in product.select(".article-price") + ] + print(f" {title}: {price_list}") except: pass diff --git a/html_parsing/get_seasons_anime_Dorohedoro.py b/html_parsing/get_seasons_anime_Dorohedoro.py deleted file mode 100644 index 03eadb8e8..000000000 --- a/html_parsing/get_seasons_anime_Dorohedoro.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import re -from typing import List - -import requests - - -def get_seasons() -> List[str]: - rs = requests.get('https://en.wikipedia.org/wiki/List_of_Dorohedoro_episodes') - rs.raise_for_status() - - items = re.findall(r'Season \w+', rs.text, flags=re.IGNORECASE) - return sorted(set(items)) - - -if __name__ == '__main__': - print(get_seasons()) - # ['Season One'] diff --git a/html_parsing/get_set_sushi_list__sushivkusno.com.py b/html_parsing/get_set_sushi_list__sushivkusno.com.py deleted file mode 100644 index f4d3b0d3b..000000000 --- a/html_parsing/get_set_sushi_list__sushivkusno.com.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import re -from bs4 import BeautifulSoup -import requests - - -rs = requests.get('http://sushivkusno.com/category/nabory-siety') -print(rs) - -root = BeautifulSoup(rs.content, 'lxml') -items = root.select('.CardContent') - -for i, product in enumerate(items, 1): - title = product.select_one('.CardText__title').text - weight = product.select_one('.CardText__subtitle > span > b').text - - price = product.select_one('.ProductParams__price').text - price = int(re.sub('\D', '', price)) - - print('{}. "{}": {}, {}'.format(i, title, weight, price)) diff --git a/html_parsing/get_stackoverflow_people_reached.py b/html_parsing/get_stackoverflow_people_reached.py index 43e696d08..25b2ac804 100644 --- a/html_parsing/get_stackoverflow_people_reached.py +++ b/html_parsing/get_stackoverflow_people_reached.py @@ -1,51 +1,50 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import Optional import re -from bs4 import BeautifulSoup import requests +from bs4 import BeautifulSoup -def get_stackoverflow_people_reached(url: str) -> Optional[str]: +def get_stackoverflow_people_reached(url: str) -> str | None: rs = requests.get(url) rs.raise_for_status() - root = BeautifulSoup(rs.content, 'html.parser') - profile_avatar_el = root.select_one('#main-content .s-card') + root = BeautifulSoup(rs.content, "html.parser") + profile_avatar_el = root.select_one("#main-content .s-card") if not profile_avatar_el: return - text = profile_avatar_el.get_text(strip=True, separator='\n') - m = re.search(r'(\d+\.?\d*[km]?)\s*(затронуто|reached)', text) + text = profile_avatar_el.get_text(strip=True, separator="\n") + m = re.search(r"(\d+\.?\d*[km]?)\s*(затронуто|reached)", text) if not m: - raise Exception('Reached not found!') + raise Exception("Reached not found!") return m.group(1) -if __name__ == '__main__': - url = 'https://ru.stackoverflow.com/users/201445/gil9red' +if __name__ == "__main__": + url = "https://ru.stackoverflow.com/users/201445/gil9red" print(get_stackoverflow_people_reached(url)) # 1.5m - url = 'https://ru.stackoverflow.com' + url = "https://ru.stackoverflow.com" print(get_stackoverflow_people_reached(url)) # None print() urls = [ - 'https://ru.stackoverflow.com/users/213987/a-k', - 'https://ru.stackoverflow.com/users/17609/%d0%ae%d1%80%d0%b8%d0%b9%d0%a1%d0%9f%d0%b1', - 'https://ru.stackoverflow.com/users/1984/nofate', - 'https://stackoverflow.com/users/541136/aaron-hall', - 'https://stackoverflow.com/users/106224/boltclock', - 'https://stackoverflow.com/users/168175/flexo', + "https://ru.stackoverflow.com/users/213987/a-k", + "https://ru.stackoverflow.com/users/17609/%d0%ae%d1%80%d0%b8%d0%b9%d0%a1%d0%9f%d0%b1", + "https://ru.stackoverflow.com/users/1984/nofate", + "https://stackoverflow.com/users/541136/aaron-hall", + "https://stackoverflow.com/users/106224/boltclock", + "https://stackoverflow.com/users/168175/flexo", ] for url in urls: print(get_stackoverflow_people_reached(url)) diff --git a/html_parsing/get_video_list__xcadr.com.py b/html_parsing/get_video_list__xcadr.com.py index a829c687f..219ab0db6 100644 --- a/html_parsing/get_video_list__xcadr.com.py +++ b/html_parsing/get_video_list__xcadr.com.py @@ -1,71 +1,77 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def get_video_list(url): - import requests - rs = requests.get(url) +import re + +import requests +from bs4 import BeautifulSoup - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'lxml') - import re - pattern_get_duration = re.compile('(\d+).:(\d+).') +def get_video_list(url) -> list[dict]: + rs = requests.get(url) + root = BeautifulSoup(rs.content, "lxml") + + pattern_get_duration = re.compile(r"(\d+).:(\d+).") items = [] - for item in root.select('#playlist_view_playlist_view_items > .item'): - url = item.a['href'] - title = item.a['title'] - url_thumb = item.select_one('img')['src'] + for item in root.select("#playlist_view_playlist_view_items > .item"): + url = item.a["href"] + title = item.a["title"] + url_thumb = item.select_one("img")["src"] # Example: "60%", "80%" - rating = item.select_one('.rating').text - rating = int(rating.replace('%', '')) + rating = item.select_one(".rating").text + rating = int(rating.replace("%", "")) # Example: "0м:31с", "6м:32с" - duration = item.select_one('.duration').text + duration = item.select_one(".duration").text minutes, seconds = map(int, pattern_get_duration.findall(duration)[0]) duration = minutes * 60 + seconds items.append({ - 'title': title, - 'url': url, - 'duration': duration, - 'rating': rating, - 'url_thumb': url_thumb + "title": title, + "url": url, + "duration": duration, + "rating": rating, + "url_thumb": url_thumb, }) return items -if __name__ == '__main__': - items = get_video_list('http://xcadr.com/collection/seks-v-poze-naezdnicy/') - print('Total:', len(items)) +if __name__ == "__main__": + items = get_video_list("http://xcadr.com/collection/seks-v-poze-naezdnicy/") + print("Total:", len(items)) - items = get_video_list('http://xcadr.com/collection/sceny-bdsm-v-filmah/') - print('Total:', len(items)) + items = get_video_list("http://xcadr.com/collection/sceny-bdsm-v-filmah/") + print("Total:", len(items)) print() - items = get_video_list('http://xcadr.com/collection/luchshie-sceny-v-bane/') - print('Total:', len(items)) + items = get_video_list("http://xcadr.com/collection/luchshie-sceny-v-bane/") + print("Total:", len(items)) - def print_items(items): + def print_items(items) -> None: for i, item in enumerate(items, 1): - print('{0:2}. "{title}" ({duration} secs, rating: {rating}): {url} [{url_thumb}]'.format(i, **item)) - - print('Sorted by duration (top 5):') - new_items = sorted(items, key=lambda x: x['duration'], reverse=True)[:5] + print( + '{0:2}. "{title}" ({duration} secs, rating: {rating}): {url} [{url_thumb}]'.format( + i, **item + ) + ) + + print("Sorted by duration (top 5):") + new_items = sorted(items, key=lambda x: x["duration"], reverse=True)[:5] print_items(new_items) print() - print('Sorted by duration:') - new_items = sorted(items, key=lambda x: x['duration'], reverse=True) + print("Sorted by duration:") + new_items = sorted(items, key=lambda x: x["duration"], reverse=True) print_items(new_items) print() - print('Sorted by rating:') - new_items = sorted(items, key=lambda x: x['rating'], reverse=True) + print("Sorted by rating:") + new_items = sorted(items, key=lambda x: x["rating"], reverse=True) print_items(new_items) diff --git a/html_parsing/gifer_com__ru__gifs__loading.py b/html_parsing/gifer_com__ru__gifs__loading.py index c9f72180c..9b5b7011d 100644 --- a/html_parsing/gifer_com__ru__gifs__loading.py +++ b/html_parsing/gifer_com__ru__gifs__loading.py @@ -1,50 +1,51 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import List - # pip install selenium from selenium import webdriver from selenium.webdriver.firefox.options import Options +from selenium.webdriver.common.by import By -def get_urls(driver) -> List[str]: +def get_urls(driver) -> list[str]: urls = [] - for link in driver.find_elements_by_css_selector('figure.media-thumb.desktop link[itemprop=contentUrl]'): - url_gif = link.get_attribute('content') - gif_id = url_gif.split('/')[-1] + for link in driver.find_elements( + By.CSS_SELECTOR, "figure.media-thumb.desktop link[itemprop=contentUrl]" + ): + url_gif = link.get_attribute("content") + gif_id = url_gif.split("/")[-1] url_download = URL_DOWNLOAD_TEMPLATE + gif_id urls.append(url_download) return urls -URL_DOWNLOAD_TEMPLATE = 'https://i.gifer.com/embedded/download/' +URL_DOWNLOAD_TEMPLATE = "https://i.gifer.com/embedded/download/" options = Options() -options.add_argument('--headless') +options.add_argument("--headless") driver = webdriver.Firefox(options=options) -driver.get('https://gifer.com/ru/gifs/loading') +driver.get("https://gifer.com/ru/gifs/loading") print(f'Title: "{driver.title}"') driver.implicitly_wait(20) urls = get_urls(driver) -print(f'{len(urls)}: {urls}') +print(f"{len(urls)}: {urls}") # 4: ['https://i.gifer.com/embedded/download/g0R5.gif', 'https://i.gifer.com/embedded/download/VAyR.gif', 'https://i.gifer.com/embedded/download/ZKZx.gif', 'https://i.gifer.com/embedded/download/ZZ5H.gif'] # Small scroll down -driver.execute_script(f'window.scrollTo(0, 200);') +driver.execute_script(f"window.scrollTo(0, 200);") urls = get_urls(driver) -print(f'{len(urls)}: {urls}') +print(f"{len(urls)}: {urls}") # 8: ['https://i.gifer.com/embedded/download/g0R5.gif', 'https://i.gifer.com/embedded/download/VAyR.gif', 'https://i.gifer.com/embedded/download/ZKZx.gif', 'https://i.gifer.com/embedded/download/ZZ5H.gif', 'https://i.gifer.com/embedded/download/g0R9.gif', 'https://i.gifer.com/embedded/download/ZWdx.gif', 'https://i.gifer.com/embedded/download/7pld.gif', 'https://i.gifer.com/embedded/download/AqCa.gif'] driver.quit() diff --git a/html_parsing/gitmanga_com__get_chapters.py b/html_parsing/gitmanga_com__get_chapters.py new file mode 100644 index 000000000..002f867dc --- /dev/null +++ b/html_parsing/gitmanga_com__get_chapters.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass +from urllib.parse import urljoin + +import requests +from bs4 import BeautifulSoup + + +session = requests.session() +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0" + + +@dataclass +class Chapter: + title: str + url: str + + +def get_chapters(url: str) -> list[Chapter]: + rs = session.get(url) + rs.raise_for_status() + + soup = BeautifulSoup(rs.content, "html.parser") + return [ + Chapter( + title=el.select_one(".item-title").get_text(strip=True), + url=urljoin(rs.url, el.a["href"]), + ) + for el in soup.select(".chapters-list__item") + ] + + +if __name__ == "__main__": + + def print_chapter(items: list[Chapter]) -> None: + print(f"Chapters ({len(items)}):") + print(f" {items[0]}") + print(" ...") + print(f" {items[-1]}") + + url = "https://gitmanga.com/764-berserk.html" + items = get_chapters(url) + print_chapter(items) + """ + Chapters (387): + Chapter(title='Том 1. Глава 0 - Berserk - The Prototype', url='https://gitmanga.com/read-764-berserk.html?t=1&g=0&p=1') + ... + Chapter(title='Том 42. Глава 371 - Угасающий свет в гнетущей тёмной ночи', url='https://gitmanga.com/read-764-berserk.html?t=42&g=371&p=1') + """ + + print() + + url = "https://gitmanga.com/605-igrok.html" + items = get_chapters(url) + print_chapter(items) + """ + Chapters (447): + Chapter(title='Том 1. Глава 1', url='https://gitmanga.com/read-605-igrok.html?t=1&g=1&p=1') + ... + Chapter(title='Том 6. Глава 15', url='https://gitmanga.com/read-605-igrok.html?t=6&g=15&p=1') + """ diff --git a/html_parsing/goldapple_ru/goldapple.ru_brands__products_API.py b/html_parsing/goldapple_ru/goldapple.ru_brands__products_API.py index 4e3664482..9a16c5c6f 100644 --- a/html_parsing/goldapple_ru/goldapple.ru_brands__products_API.py +++ b/html_parsing/goldapple_ru/goldapple.ru_brands__products_API.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re @@ -9,30 +9,32 @@ session = requests.session() -session.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0' +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0" -URL_BRAND = 'https://goldapple.ru/brands/elian-russia' +URL_BRAND = "https://goldapple.ru/brands/elian-russia" rs = session.get(URL_BRAND) m = re.search(r'"productsApiUrl":\s*"(.+?)",', rs.text) if not m: - raise Exception('Не получилось найти ссылку на API!') + raise Exception("Не получилось найти ссылку на API!") URL_API_PRODUCTS = m.group(1) -print(f'Using API: {URL_API_PRODUCTS}') +print(f"Using API: {URL_API_PRODUCTS}") # Using API: https://goldapple.ru/web_scripts/discover/category/products?cat=6577 total = 0 page = 1 while True: - rs = session.get(URL_API_PRODUCTS, params={'page': page}) + rs = session.get(URL_API_PRODUCTS, params={"page": page}) rs.raise_for_status() - products = rs.json().get('products') + products = rs.json().get("products") if not products: break diff --git a/html_parsing/grouple_co/[deprecated]_get_updates_from_rss.py b/html_parsing/grouple_co/[deprecated]_get_updates_from_rss.py new file mode 100644 index 000000000..5db8b2338 --- /dev/null +++ b/html_parsing/grouple_co/[deprecated]_get_updates_from_rss.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install feedparser==6.0.8 +import feedparser + + +URL_USER_RSS = "https://grouple.co/user/rss/315828?filter=" +USER_AGENT = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0" +) + + +def get_feeds_by_manga_chapters(url: str = URL_USER_RSS) -> list[str]: + feed = feedparser.parse(url, agent=USER_AGENT) + + feeds = [] + for entry in feed.entries: + title: str = entry.title + title = ( + title.replace(""", '"') + .replace("Манга", "") + .replace("Взрослая манга", "") + .strip() + ) + + feeds.append(title) + + return feeds + + +if __name__ == "__main__": + items = get_feeds_by_manga_chapters() + print(f"Items ({len(items)}):", items) diff --git a/html_parsing/grouple_co/common.py b/html_parsing/grouple_co/common.py index 5e533be99..4af15f367 100644 --- a/html_parsing/grouple_co/common.py +++ b/html_parsing/grouple_co/common.py @@ -1,13 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import enum +import json +import re import time + from dataclasses import dataclass, field -from typing import List, Dict +from urllib.parse import urljoin, urlsplit import requests from bs4 import BeautifulSoup, Tag @@ -19,10 +22,10 @@ class Bookmark: title: str url: str - tags: List[str] = field(default_factory=list) + tags: list[str] = field(default_factory=list) def get_title_with_tags(self) -> str: - return self.title + (f" [{', '.join(self.tags)}]" if self.tags else '') + return self.title + (f" [{', '.join(self.tags)}]" if self.tags else "") class AutoName(enum.Enum): @@ -31,43 +34,115 @@ def _generate_next_value_(name, start, count, last_values): class Status(AutoName): - WATCHING = enum.auto() # В процессе + WATCHING = enum.auto() # В процессе USER_DEFINED = enum.auto() # Пользовательская - ON_HOLD = enum.auto() # Пока бросил - PLANED = enum.auto() # В планах - COMPLETED = enum.auto() # Готово - FAVORITE = enum.auto() # Любимая + ON_HOLD = enum.auto() # Пока бросил + PLANED = enum.auto() # В планах + COMPLETED = enum.auto() # Готово + FAVORITE = enum.auto() # Любимая + + +HOST: str = "https://grouple.co" +ADDITIONAL_HOSTS: list[str] = [ + "https://1.grouple.co", + "https://2.grouple.co", + "https://3.grouple.co", +] session = requests.Session() -session.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0' +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0" + + +def get_url(uri: str) -> str: + return urljoin(HOST, uri) + + +def do_get(uri: str, **kwargs) -> requests.Response: + rs = session.get(get_url(uri), **kwargs) + rs.raise_for_status() + + return rs + + +def do_post(uri: str, **kwargs) -> requests.Response: + rs = session.post(get_url(uri), **kwargs) + rs.raise_for_status() + + return rs def do_auth() -> requests.Response: form_data = { - 'username': LOGIN, - 'password': PASSWORD, + "username": LOGIN, + "password": PASSWORD, + "remember_me": "true", + "_remember_me_yes": "", + "remember_me_yes": "on", } - rs = session.post('https://grouple.co/login/authenticate', data=form_data) + rs = do_get("/internal/auth/login") + rs.raise_for_status() + + rs = do_post("/login/authenticate", data=form_data) rs.raise_for_status() # Example: https://grouple.co/internal/auth/login?login_error=1 - if 'error' in rs.url: - raise Exception('Invalid auth!') + if "error" in rs.url: + raise Exception("Invalid auth!") return rs -def load(url: str) -> requests.Response: - while True: - rs = session.get(url) - rs.raise_for_status() +# NOTE: Была замечена смена домена +def update_host(): + global HOST + + def _update(url: str) -> bool: + global HOST + + if "grouple" in url and not url.startswith(HOST): + HOST = urlsplit(url)._replace(path="", query="", fragment="").geturl() + return True + + return False + + try: + rs = do_get("/") + except Exception as e: + # Перебор дополнительных хостов при ошибках + if HOST == ADDITIONAL_HOSTS[-1]: + raise e + + try: + idx = ADDITIONAL_HOSTS.index(HOST) + 1 + except ValueError: + idx = 0 + HOST = ADDITIONAL_HOSTS[idx] + + update_host() + return + + if not _update(rs.url): + rs = do_get("/internal/auth") + _update(rs.url) + + rs = do_auth() + _update(rs.url) + + +def load(uri: str, **kwargs) -> requests.Response: + update_host() + + while True: + rs = do_get(uri, **kwargs) time.sleep(1) # Если нужно авторизоваться - if '/login/auth' in rs.url: + if "/login/auth" in rs.url: do_auth() continue @@ -77,70 +152,126 @@ def load(url: str) -> requests.Response: def parse_bookmark(el: Tag) -> Bookmark: tags = [] if el.sup: - tags += [x.get_text(strip=True).lower() for x in el.sup.select('span[class]')] + tags += [x.get_text(strip=True).lower() for x in el.sup.select("span[class]")] # Удаление сноски ("Выпуск завершен", "переведено" и т.п.), чтобы в title она не попала el.sup.decompose() title = el.get_text(strip=True) - url = el['href'] + url = el["href"] return Bookmark(title=title, url=url, tags=tags) -def get_bookmarks(url: str) -> List[Bookmark]: - rs = load(url) - root = BeautifulSoup(rs.content, 'html.parser') +def get_bookmarks_by_status(status: Status) -> list[Bookmark]: + rs = load("/private/bookmarks") - return [ - parse_bookmark(row) - for row in root.select('.bookmark-row a.site-element') - ] + m = re.search(r"var SITES = (\[.+]);", rs.text) + if not m: + raise Exception('Не удалось найти "var SITES = "!') + # Example: var SITES = [{"id":1,"title":"ReadManga","url":"https://readmanga.io"}, ... + sites_str = m.group(1) + sites = json.loads(sites_str) + site_id_by_url = {x["id"]: x["url"] for x in sites} -def get_bookmarks_by_status(status: Status) -> List[Bookmark]: - return get_bookmarks(f'https://grouple.co/private/bookmarks?status={status.value}') + items: list[Bookmark] = [] + limit = 50 + offset = 0 -def get_plain_all_bookmarks_from_user(user_id: int) -> List[Bookmark]: - url = f'https://grouple.co/user/{user_id}/bookmarks' - rs = load(url) - root = BeautifulSoup(rs.content, 'html.parser') + while True: + data = { + "bookmarkSort": "NAME", + "query": "", + "elementFilter": [], + "statusFilter": [status.value], + "limit": limit, + "offset": offset, + } + headers = { + "Authorization": f'Bearer {session.cookies["gwt"]}', + "Referer": rs.url, + } + rs = do_post("/api/bookmark/list", json=data, headers=headers) - return [ - parse_bookmark(row) - for row in root.select('a.site-element') - ] + result = rs.json() + for item in result["list"]: + title = item["element"]["name"] + site_id = item["element"]["elementId"]["siteId"] + element_url = item["element"]["elementUrl"] + url = site_id_by_url[site_id] + element_url -def get_all_bookmarks() -> Dict[Status, List[Bookmark]]: - return { - status: get_bookmarks_by_status(status) - for status in Status - } + # Example: + # tags_str = " СборникOnline" + # tags = ['Сборник', 'Online'] + tags_str = item["element"]["tagsString"] + tags = [ + el.get_text(strip=True).lower() + for el in BeautifulSoup(tags_str, "html.parser").select("span") + ] + + items.append(Bookmark(title=title, url=url, tags=tags)) + + if result["offset"] + result["limit"] >= result["total"]: + break + offset += limit + time.sleep(2) -if __name__ == '__main__': + return items + + +def get_plain_all_bookmarks_from_user(user_id: int) -> list[Bookmark]: + rs = load(f"/user/{user_id}/bookmarks") + root = BeautifulSoup(rs.content, "html.parser") + + return [parse_bookmark(row) for row in root.select("a.site-element")] + + +def get_all_bookmarks() -> dict[Status, list[Bookmark]]: + return {status: get_bookmarks_by_status(status) for status in Status} + + +if __name__ == "__main__": assert Status.WATCHING.name == Status.WATCHING.value assert Status.WATCHING.name == "WATCHING" + print("HOST:", HOST) + # HOST: https://grouple.co + print(session.cookies) + print(load("/")) print(do_auth()) print(session.cookies) - print('\n' + '-' * 50 + '\n') + print("HOST:", HOST) + # HOST: https://1.grouple.co + + print("\n" + "-" * 50 + "\n") + + print(get_plain_all_bookmarks_from_user(315828)) + + print("\n" + "-" * 50 + "\n") bookmarks = get_bookmarks_by_status(Status.WATCHING) - print(f'Bookmarks ({len(bookmarks)}):') + print(f"Bookmarks ({len(bookmarks)}):") for i, bookmark in enumerate(bookmarks, 1): - print(f'{i}. {bookmark}') + print(f"{i}. {bookmark}") """ - Bookmarks (28): - 1. Bookmark(title='Башня Бога', url='https://readmanga.io/bashnia_boga__A339d2', tags=[]) - 2. Bookmark(title='Берсерк', url='https://readmanga.io/berserk', tags=[]) - 3. Bookmark(title='Боруто', url='https://readmanga.io/boruto__A5327', tags=[]) - ... - 26. Bookmark(title='Священная земля', url='https://readmanga.io/sviachennaia__zemlia__A533b', tags=['переведено']) - 27. Bookmark(title='Терраформирование', url='https://mintmanga.live/terraformirovanie__A5327', tags=[]) - 28. Bookmark(title='Фейри Тейл. Начало', url='https://readmanga.io/feiri_teil__nachalo', tags=['переведено', 'Без глав']) + Bookmarks (13): + 1. Bookmark(title='Башня Бога', url='https://web.usagi.one/tower_of_god', tags=[]) + 2. Bookmark(title='Боруто. Наруто: Новое поколение', url='https://web.usagi.one/boruto__naruto_next_generations', tags=['завершён']) + 3. Bookmark(title='Ван Пис', url='https://web.usagi.one/van_pis', tags=[]) + 4. Bookmark(title='Ванпанчмен', url='https://1.seimanga.me/vanpanchmen', tags=[]) + 5. Bookmark(title='Ванпанчмен (ONE)', url='https://1.seimanga.me/vanpanchmen__one_', tags=[]) + 6. Bookmark(title='Выживание в игре за варвара', url='https://web.usagi.one/surviving_the_game_as_a_barbarian', tags=[]) + 7. Bookmark(title='Необъятный океан', url='https://1.seimanga.me/neobiatnyi_okean', tags=[]) + 8. Bookmark(title='Охотник × Охотник', url='https://web.usagi.one/hunter_x_hunter', tags=[]) + 9. Bookmark(title='Священная земля', url='https://web.usagi.one/holyland', tags=['завершён']) + 10. Bookmark(title='Семь смертных грехов', url='https://web.usagi.one/the_seven_deadly_sins', tags=['завершён', 'без глав']) + 11. Bookmark(title='Серафим конца', url='https://web.usagi.one/seraph_of_the_end', tags=[]) + 12. Bookmark(title='Терраформирование', url='https://1.seimanga.me/terraformirovanie', tags=[]) + 13. Bookmark(title='Чародейки', url='https://web.usagi.one/witch', tags=['завершён']) """ diff --git a/html_parsing/grouple_co/config.py b/html_parsing/grouple_co/config.py index 6164dcf54..3a49963e2 100644 --- a/html_parsing/grouple_co/config.py +++ b/html_parsing/grouple_co/config.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os @@ -9,7 +9,7 @@ DIR = Path(__file__).resolve().parent -TOKEN_FILE_NAME = DIR / 'TOKEN.txt' +TOKEN_FILE_NAME = DIR / "TOKEN.txt" -TOKEN = os.environ.get('TOKEN') or TOKEN_FILE_NAME.read_text('utf-8').strip() +TOKEN = os.environ.get("TOKEN") or TOKEN_FILE_NAME.read_text("utf-8").strip() LOGIN, PASSWORD = TOKEN.splitlines() diff --git a/html_parsing/grouple_co/get_updates_from_api.py b/html_parsing/grouple_co/get_updates_from_api.py new file mode 100644 index 000000000..ce223c428 --- /dev/null +++ b/html_parsing/grouple_co/get_updates_from_api.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import session, do_post, load + + +def get_feeds_by_manga_chapters() -> list[str]: + # Auth and load + rs = load("/private/bookmarks/index#") + + data = { + "bookmarkSort": "NAME", + "elementFilter": [], + "statusFilter": ["ANOTHER_UPDATES"], + "includeUpdates": True, + "limit": 50, + "offset": 0, + } + + session.headers.update( + { + "Authorization": f'Bearer {session.cookies["gwt"]}', + "Referer": rs.url, + } + ) + + rs = do_post("/api/bookmark/activitiesList", json=data) + return [ + f'{item["element"]["name"]} {item["title"]}' + for item in rs.json()["list"] + if item["type"] == "CHAPTER_NEW" + ] + + +if __name__ == "__main__": + items = get_feeds_by_manga_chapters() + print(f"Items ({len(items)}):", items) diff --git a/html_parsing/grouple_co/print_all_bookmarks.py b/html_parsing/grouple_co/print_all_bookmarks.py index fc834d14d..5d8314c68 100644 --- a/html_parsing/grouple_co/print_all_bookmarks.py +++ b/html_parsing/grouple_co/print_all_bookmarks.py @@ -1,22 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_all_bookmarks status_by_bookmarks = get_all_bookmarks() -print('Total bookmarks:', sum(len(bookmarks) for bookmarks in status_by_bookmarks.values())) +print( + "Total bookmarks:", + sum(len(bookmarks) for bookmarks in status_by_bookmarks.values()), +) # Total bookmarks: 143 print() for status, bookmarks in status_by_bookmarks.items(): - print(f'{status.value}. Bookmarks ({len(bookmarks)}):') + print(f"{status.value}. Bookmarks ({len(bookmarks)}):") for i, bookmark in enumerate(bookmarks, 1): - print(f'{i}. {bookmark}') + print(f"{i}. {bookmark}") print() diff --git a/html_parsing/grouple_co/print_all_bookmarks_from_user.py b/html_parsing/grouple_co/print_all_bookmarks_from_user.py index fc1769b73..f6e8368fc 100644 --- a/html_parsing/grouple_co/print_all_bookmarks_from_user.py +++ b/html_parsing/grouple_co/print_all_bookmarks_from_user.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_plain_all_bookmarks_from_user bookmarks = get_plain_all_bookmarks_from_user(315828) -print(f'Bookmarks ({len(bookmarks)}):') +print(f"Bookmarks ({len(bookmarks)}):") for i, bookmark in enumerate(bookmarks, 1): - print(f'{i}. {bookmark}') + print(f"{i}. {bookmark}") """ Bookmarks (85): 1. Bookmark(title='Башня Бога', url='https://readmanga.io/bashnia_boga__A339d2', tags=[]) @@ -20,4 +20,4 @@ 83. Bookmark(title='Школа мертвецов', url='/internal/red/14905', tags=['переведено']) 84. Bookmark(title='Школьный эфир!', url='https://readmanga.io/love_live__dj___school_live', tags=['сингл']) 85. Bookmark(title='Энигма', url='https://readmanga.io/enigma__A5274', tags=['переведено']) -""" \ No newline at end of file +""" diff --git a/html_parsing/grouple_co/print_bookmarks_completed__mangaTranslationCompleted.py b/html_parsing/grouple_co/print_bookmarks_completed__mangaTranslationCompleted.py index 2e34cc5c5..34d962d81 100644 --- a/html_parsing/grouple_co/print_bookmarks_completed__mangaTranslationCompleted.py +++ b/html_parsing/grouple_co/print_bookmarks_completed__mangaTranslationCompleted.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import Status, get_bookmarks_by_status @@ -9,13 +9,13 @@ items = get_bookmarks_by_status(Status.WATCHING) -print(f'Total bookmarks ({len(items)}):') +print(f"Total bookmarks ({len(items)}):") for x in items: - print(f' {x.title!r}: {x.url}') + print(f" {x.title!r}: {x.url}") -print('\n') +print("\n") -completed = [x for x in items if 'переведено' in x.tags] -print(f'Total bookmarks completed ({len(completed)}):') +completed = [x for x in items if "завершён" in x.tags] +print(f"Total bookmarks completed ({len(completed)}):") for x in completed: - print(f' {x.title!r}: {x.url}') + print(f" {x.title!r}: {x.url}") diff --git a/html_parsing/grouple_co/print_pretty_title_all_bookmarks.py b/html_parsing/grouple_co/print_pretty_title_all_bookmarks.py index e4583ed84..b0ccecf97 100644 --- a/html_parsing/grouple_co/print_pretty_title_all_bookmarks.py +++ b/html_parsing/grouple_co/print_pretty_title_all_bookmarks.py @@ -1,33 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import List from common import get_all_bookmarks -def get_all_pretty_title_of_bookmarks() -> List[str]: +def get_all_pretty_title_of_bookmarks() -> list[str]: items = [] for bookmarks in get_all_bookmarks().values(): - items += [ - x.get_title_with_tags() - for x in bookmarks - ] + items += [x.get_title_with_tags() for x in bookmarks] return items -if __name__ == '__main__': +if __name__ == "__main__": all_bookmarks = get_all_pretty_title_of_bookmarks() - print('Total bookmarks:', len(all_bookmarks)) + print("Total bookmarks:", len(all_bookmarks)) # Total bookmarks: 143 print() for i, bookmark_title in enumerate(all_bookmarks, 1): - print(f'{i}. {bookmark_title}') + print(f"{i}. {bookmark_title}") """ 1. Башня Бога diff --git a/html_parsing/hentailib_me.py b/html_parsing/hentailib_me.py index 1dd309bea..1978dac33 100644 --- a/html_parsing/hentailib_me.py +++ b/html_parsing/hentailib_me.py @@ -1,45 +1,44 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import re import json -from typing import List +import re import requests -def get_images(url: str) -> List[str]: +def get_images(url: str) -> list[str]: rs = requests.get(url) - info = re.search('window.__info = (.+?);', rs.text) + info = re.search("window.__info = (.+?);", rs.text) if not info: - print('[#] Not found window.__info!') + print("[#] Not found window.__info!") return [] - pages = re.search('window.__pg = (.+?);', rs.text) + pages = re.search("window.__pg = (.+?);", rs.text) if not pages: - print('[#] Not found window.__pg!') + print("[#] Not found window.__pg!") return [] info = json.loads(info.group(1)) pages = json.loads(pages.group(1)) - url_chapter = info['img']['url'] - url_base = info['servers']['main'] + url_chapter + url_chapter = info["img"]["url"] + url_base = info["servers"]["main"] + url_chapter - return [url_base + p['u'] for p in pages] + return [url_base + p["u"] for p in pages] -if __name__ == '__main__': - url = 'https://hentailib.me/koshkodevochki-eto-lozh/v1/c1?page=1' +if __name__ == "__main__": + url = "https://hentailib.me/koshkodevochki-eto-lozh/v1/c1?page=1" items = get_images(url) - print(f'Images ({len(items)}):') + print(f"Images ({len(items)}):") for i, url in enumerate(items, 1): - print(f' {i}. {url}') + print(f" {i}. {url}") # Images (22): # 1. https://img2.hentailib.me/manga/koshkodevochki-eto-lozh/chapters/468545/02_iips.png diff --git a/html_parsing/horoscope/horo.mail.ru_pisces_today.py b/html_parsing/horoscope/horo.mail.ru_pisces_today.py index a394f5564..76e4e6b05 100644 --- a/html_parsing/horoscope/horo.mail.ru_pisces_today.py +++ b/html_parsing/horoscope/horo.mail.ru_pisces_today.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import requests -rs = requests.get('https://horo.mail.ru/prediction/pisces/today/') - from bs4 import BeautifulSoup -root = BeautifulSoup(rs.content, 'lxml') -text = root.select_one('.article__text').text.strip() + + +rs = requests.get("https://horo.mail.ru/prediction/pisces/today/") +root = BeautifulSoup(rs.content, "lxml") +text = root.select_one(".article__text").text.strip() print(repr(text)) print(text) diff --git a/html_parsing/howlongtobeat_com/common.py b/html_parsing/howlongtobeat_com/common.py new file mode 100644 index 000000000..8c244d9ef --- /dev/null +++ b/html_parsing/howlongtobeat_com/common.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass, field +from typing import Any + +import requests + +from seconds_to_str import seconds_to_str + + +URL_BASE = "https://howlongtobeat.com" + +USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0" + +session = requests.session() +session.headers["User-Agent"] = USER_AGENT + + +@dataclass +class Game: + id: int + title: str + aliases: list[str] + + duration_main_seconds: int # Main Story + duration_main_title: str = field(init=False) + + duration_plus_seconds: int # Main + Sides + duration_plus_title: str = field(init=False) + + duration_100_seconds: int # Completionist + duration_100_title: str = field(init=False) + + duration_all_seconds: int # All Styles + duration_all_title: str = field(init=False) + + release_world: int + profile_platforms: list[str] + + profile_genres: list[str] = field(default_factory=list) + + def __post_init__(self) -> None: + self.duration_main_title = seconds_to_str(self.duration_main_seconds) + self.duration_plus_title = seconds_to_str(self.duration_plus_seconds) + self.duration_100_title = seconds_to_str(self.duration_100_seconds) + self.duration_all_title = seconds_to_str(self.duration_all_seconds) + + @classmethod + def parse(cls, data: dict[str, Any]) -> "Game": + return cls( + id=data["game_id"], + title=data["game_name"], + aliases=data["game_alias"].split(", "), + duration_main_seconds=data["comp_main"], + duration_plus_seconds=data["comp_plus"], + duration_100_seconds=data["comp_100"], + duration_all_seconds=data["comp_all"], + release_world=data["release_world"], + + # В случаи поиска некоторые из полей недоступны в результатах - нужно на страницу игры идти + profile_platforms=data.get("profile_platform", "").split(", "), + profile_genres=data.get("profile_genre", "").split(", "), + ) diff --git a/html_parsing/howlongtobeat_com/get_game_info.py b/html_parsing/howlongtobeat_com/get_game_info.py new file mode 100644 index 000000000..a574df17b --- /dev/null +++ b/html_parsing/howlongtobeat_com/get_game_info.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import json + +# pip install dpath +import dpath.util + +from bs4 import BeautifulSoup + +from common import session, URL_BASE, Game + + +def get_game_info(game_id: int) -> Game: + url = f"{URL_BASE}/game/{game_id}" + + rs = session.get(url) + rs.raise_for_status() + + soup = BeautifulSoup(rs.content, "html.parser") + + next_data_el = soup.select_one("#__NEXT_DATA__") + if not next_data_el: + raise Exception('Not found id="__NEXT_DATA__"!') + + next_data_str = next_data_el.string + + next_data = json.loads(next_data_str) + return Game.parse( + data=dpath.util.get(next_data, "**/data/game/0") + ) + + +if __name__ == "__main__": + print( + get_game_info(game_id=3505) + ) + # Game(id=3505, title='Final Fantasy IX', aliases=['Final Fantasy 9', 'FF9'], duration_main_seconds=138721, duration_main_title='38:32:01', duration_plus_seconds=189924, duration_plus_title='52:45:24', duration_100_seconds=299502, duration_100_title='83:11:42', duration_all_seconds=171503, duration_all_title='47:38:23', release_world='2000-02-16', profile_platforms=['Mobile', 'Nintendo Switch', 'PC', 'PlayStation', 'PlayStation 4', 'Xbox One'], profile_genres=['Role-Playing']) + + print( + get_game_info(game_id=3519) + ) + # Game(id=3519, title='Final Fantasy VI', aliases=['Final Fantasy III [NA]', 'Final Fantasy 3 [NA]', 'Final Fantasy 6', 'FF6', 'Final Fantasy VI Advance', 'Final Fantasy VI: Pixel Remaster'], duration_main_seconds=124906, duration_main_title='34:41:46', duration_plus_seconds=147288, duration_plus_title='40:54:48', duration_100_seconds=220653, duration_100_title='61:17:33', duration_all_seconds=148583, duration_all_title='41:16:23', release_world='1994-04-02', profile_platforms=['Game Boy Advance', 'Mobile', 'Nintendo Switch', 'PC', 'PlayStation', 'PlayStation 4', 'Super Nintendo'], profile_genres=['Turn-Based', 'Role-Playing']) diff --git a/html_parsing/howlongtobeat_com/search.py b/html_parsing/howlongtobeat_com/search.py new file mode 100644 index 000000000..8b4abb967 --- /dev/null +++ b/html_parsing/howlongtobeat_com/search.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# import re # TODO: Удалить + +from datetime import datetime + +# from urllib.parse import urljoin # TODO: Удалить +from typing import Any + +from common import URL_BASE, session, Game + + +def api_search(text: str, page: int = 1) -> dict[str, Any]: + url_first = f"{URL_BASE}/?q={text}" + + rs = session.get(url_first) + rs.raise_for_status() + + headers = {"Referer": url_first} + + # TODO: Перестала быть рабочей. Работа API теперь в файлах вида "/_next/static/chunks/\w+.js" + # # NOTE: Получение url из js. Старая защита. Возможно, будет комбинироваться с новой + # m = re.search(r' + + + + + +
    +

    {{ title }}

    + +
    +
    + + + + {% for header in headers %} + + {% endfor %} + + + +
    {{ header }}
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + + + \ No newline at end of file diff --git "a/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204__crash_statistics/web.py" "b/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204__crash_statistics/web.py" index 2953120cd..684a9e88d 100644 --- "a/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204__crash_statistics/web.py" +++ "b/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204__crash_statistics/web.py" @@ -1,178 +1,51 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt -from flask import Flask, render_template_string +from flask import Flask, render_template from common import get_crash_statistics_list_db app = Flask(__name__) -@app.route('/') +@app.route("/") def index(): headers = ["Дата", "ДТП", "Погибли", "Погибло детей", "Ранены", "Ранено детей"] rows = get_crash_statistics_list_db() rows.reverse() - data = [] - for date, dtp, died, children_died, wounded, wounded_children in rows: + data: list[dict] = [] + year_by_number: dict[int, int] = dict() + for date_str, dtp, died, children_died, wounded, wounded_children in rows: + date = dt.datetime.strptime(date_str, "%d.%m.%Y").date() + dtp = int(dtp) + data.append({ - 'date': date, - 'date_iso': DT.datetime.strptime(date, '%d.%m.%Y').date().isoformat(), - 'dtp': int(dtp), - 'died': int(died), - 'children_died': int(children_died), - 'wounded': int(wounded), - 'wounded_children': int(wounded_children), + "date": date_str, + "date_iso": date.isoformat(), + "dtp": dtp, + "died": int(died), + "children_died": int(children_died), + "wounded": int(wounded), + "wounded_children": int(wounded_children), }) - return render_template_string(""" - - - - - {{ title }} - - - - - - - - -
    -

    {{ title }}

    - -
    -
    - - - - {% for header in headers %} - - {% endfor %} - - - -
    {{ header }}
    -
    -
    - -
    -
    -
    - - - - """, title="АВАРИЙНОСТЬ НА ДОРОГАХ РОССИИ", headers=headers, data=data -) + if date.year not in year_by_number: + year_by_number[date.year] = 0 + year_by_number[date.year] += dtp + + return render_template( + "index.html", + title="АВАРИЙНОСТЬ НА ДОРОГАХ РОССИИ", + headers=headers, + data=data, + year_by_number=year_by_number, + ) if __name__ == "__main__": diff --git "a/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204_request_main__auto_fill_fields__web_gui/main.py" "b/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204_request_main__auto_fill_fields__web_gui/main.py" index 5188ac0e2..f3278d76b 100644 --- "a/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204_request_main__auto_fill_fields__web_gui/main.py" +++ "b/https_\320\263\320\270\320\261\320\264\320\264_\321\200\321\204_request_main__auto_fill_fields__web_gui/main.py" @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -14,12 +14,12 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit() @@ -64,16 +64,13 @@ def log_uncaught_exceptions(ex_cls, ex, tb): """ - def run_js_code(page: QWebEnginePage, code: str) -> object: loop = QEventLoop() - result_value = { - 'value': None - } + result_value = {"value": None} - def _on_callback(result: object): - result_value['value'] = result + def _on_callback(result: object) -> None: + result_value["value"] = result loop.quit() @@ -81,15 +78,24 @@ def _on_callback(result: object): loop.exec() - return result_value['value'] + return result_value["value"] class MyWebEnginePage(QWebEnginePage): - def javaScriptConsoleMessage(self, level: 'JavaScriptConsoleMessage', message: str, line_number: int, source_id: str): - print(f'javascript_console_message: {level}, {message}, {line_number}, {source_id}', file=sys.stderr) - - -with open('js/jquery-3.1.1.min.js') as f: + def javaScriptConsoleMessage( + self, + level: "JavaScriptConsoleMessage", + message: str, + line_number: int, + source_id: str, + ) -> None: + print( + f"javascript_console_message: {level}, {message}, {line_number}, {source_id}", + file=sys.stderr, + ) + + +with open("js/jquery-3.1.1.min.js") as f: jquery_text = f.read() jquery_text += "\nvar qt = { 'jQuery': jQuery.noConflict(true) };" @@ -103,7 +109,7 @@ def javaScriptConsoleMessage(self, level: 'JavaScriptConsoleMessage', message: s # mw.setWindowTitle(str(url)) -url = 'https://гибдд.рф/request_main' +url = "https://гибдд.рф/request_main" page = MyWebEnginePage() page.load(QUrl(url)) @@ -114,17 +120,17 @@ def javaScriptConsoleMessage(self, level: 'JavaScriptConsoleMessage', message: s # page.urlChanged.connect(_on_url_changed) -def _on_load_finished(ok: bool): +def _on_load_finished(ok: bool) -> None: print(page.url().toString()) page.runJavaScript(jquery_text) result = run_js_code(page, "document.title") - print('run_java_script:', result) + print("run_java_script:", result) # TODO: обернуть в функцию has с css-selector result = run_js_code(page, "qt.jQuery('#surname_check').length > 0") - print('run_java_script:', result) + print("run_java_script:", result) # Клик на флажок "С информацией ознакомлен" run_js_code(page, """qt.jQuery('input[name="agree"]').click();""") @@ -138,7 +144,10 @@ def _on_load_finished(ok: bool): run_js_code(page, """qt.jQuery('#email_check').val('FOOBAR@EMAIL.COM');""") - run_js_code(page, """qt.jQuery('#message_check > textarea').val({});""".format(repr(TEXT_PATTERN))) + run_js_code( + page, + f"qt.jQuery('#message_check > textarea').val({TEXT_PATTERN!r});", + ) # code = """ # console.log('testetst'); @@ -159,7 +168,9 @@ def _on_load_finished(ok: bool): print() -view.loadProgress.connect(lambda value: mw.setWindowTitle('{} ({}%)'.format(view.url().toString(), value))) +view.loadProgress.connect( + lambda value: mw.setWindowTitle(f"{view.url().toString()} ({value}%)") +) view.loadFinished.connect(_on_load_finished) mw = QMainWindow() diff --git a/human__timestamp_to_date_string_format.py b/human__timestamp_to_date_string_format.py index da724d428..d0738ca77 100644 --- a/human__timestamp_to_date_string_format.py +++ b/human__timestamp_to_date_string_format.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/questions/843386/ @@ -9,20 +9,20 @@ def timestamp_to_human(time_s): items = [ - (31536000, '{} г. '), - (2592000, '{} мес. '), - (604800, '{} нед. '), - (86400, '{} д. '), - (3600, '{} ч.'), + (31536000, "{} г. "), + (2592000, "{} мес. "), + (604800, "{} нед. "), + (86400, "{} д. "), + (3600, "{} ч."), ] if time_s == 0: return None if time_s < 3600: - return 'меньше часа' + return "меньше часа" - result = '' + result = "" for value, fmt in items: if time_s >= value: @@ -32,9 +32,9 @@ def timestamp_to_human(time_s): return result -if __name__ == '__main__': +if __name__ == "__main__": print(timestamp_to_human(2827567.5759670734)) # 1 мес. 2 д. 17 ч. assert timestamp_to_human(2827567.5759670734) == "1 мес. 2 д. 17 ч." - print(timestamp_to_human(269649.6857390404)) # 3 д. 2 ч. + print(timestamp_to_human(269649.6857390404)) # 3 д. 2 ч. assert timestamp_to_human(269649.6857390404) == "3 д. 2 ч." diff --git a/human_byte_size.py b/human_byte_size.py index bf3eb0e56..964f17b9f 100644 --- a/human_byte_size.py +++ b/human_byte_size.py @@ -1,28 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import Union - - -def sizeof_fmt(num: Union[int, float]) -> str: - for x in ['bytes', 'KB', 'MB', 'GB']: +def sizeof_fmt(num: int | float) -> str: + for x in ["bytes", "KB", "MB", "GB"]: if num < 1024.0: return "%.1f %s" % (num, x) num /= 1024.0 - return "%.1f %s" % (num, 'TB') + return "%.1f %s" % (num, "TB") -if __name__ == '__main__': +if __name__ == "__main__": print(sizeof_fmt(25000000000)) print() import shutil - usage = shutil.disk_usage('C://') - print('total: {:>8} ({} bytes)'.format(sizeof_fmt(usage.total), usage.total)) - print('used: {:>8} ({} bytes)'.format(sizeof_fmt(usage.used), usage.used)) - print('free: {:>8} ({} bytes)'.format(sizeof_fmt(usage.free), usage.free)) + + usage = shutil.disk_usage("C://") + print(f"total: {sizeof_fmt(usage.total):>8} ({usage.total} bytes)") + print(f"used: {sizeof_fmt(usage.used):>8} ({usage.used} bytes)") + print(f"free: {sizeof_fmt(usage.free):>8} ({usage.free} bytes)") diff --git a/human_byte_size__using_humanize.py b/human_byte_size__using_humanize.py index 56fad2941..7ab4add8b 100644 --- a/human_byte_size__using_humanize.py +++ b/human_byte_size__using_humanize.py @@ -1,19 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import shutil + # pip install humanize import humanize -print(humanize.naturalsize(25000000000, binary=True)) # '23.3 GiB' -print(humanize.naturalsize(25000000000, binary=True).replace('i', '')) # '23.3 GB', Strange, misleading +print(humanize.naturalsize(25000000000, binary=True)) # '23.3 GiB' +print( + humanize.naturalsize(25000000000, binary=True).replace("i", "") +) # '23.3 GB', Strange, misleading print() -import shutil -usage = shutil.disk_usage('C://') -print('total: {:>8} ({} bytes)'.format(humanize.naturalsize(usage.total, binary=True), usage.total)) -print('used: {:>8} ({} bytes)'.format(humanize.naturalsize(usage.used, binary=True), usage.used)) -print('free: {:>8} ({} bytes)'.format(humanize.naturalsize(usage.free, binary=True), usage.free)) +usage = shutil.disk_usage("C://") +print( + f"total: {humanize.naturalsize(usage.total, binary=True):>8} ({usage.total} bytes)" +) +print( + f"used: {humanize.naturalsize(usage.used, binary=True):>8} ({usage.used} bytes)" +) +print( + f"free: {humanize.naturalsize(usage.free, binary=True):>8} ({usage.free} bytes)" +) diff --git a/human_format_number__1000_separator.py b/human_format_number__1000_separator.py index 9805b833d..c7e8f96b5 100644 --- a/human_format_number__1000_separator.py +++ b/human_format_number__1000_separator.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -print('{:,d}'.format(5213234)) # 5,213,234 -print('{:,d}'.format(123)) # 123 -print('{:,d}'.format(1004)) # 1,004 -print('{:,d}'.format(4321)) # 4,321 +print(f"{5213234:,d}") # 5,213,234 +print(f"{123:,d}") # 123 +print(f"{1004:,d}") # 1,004 +print(f"{4321:,d}") # 4,321 diff --git a/humanize__examples/date_and_time__humanization.py b/humanize__examples/date_and_time__humanization.py index 6f1c97951..6710dd1ec 100644 --- a/humanize__examples/date_and_time__humanization.py +++ b/humanize__examples/date_and_time__humanization.py @@ -1,34 +1,38 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/jmoiron/humanize +import datetime as dt + # pip install humanize import humanize -# Date & time humanization: -import datetime as DT -print(humanize.naturalday(DT.datetime.now())) # 'today' -print(humanize.naturalday(DT.datetime.now() - DT.timedelta(days=1))) # 'yesterday' -print(humanize.naturalday(DT.date(2007, 6, 5))) # 'Jun 05' +# Date & time humanization +print(humanize.naturalday(dt.datetime.now())) # 'today' +print(humanize.naturalday(dt.datetime.now() - dt.timedelta(days=1))) # 'yesterday' +print(humanize.naturalday(dt.date(2007, 6, 5))) # 'Jun 05' print() -print(DT.timedelta(seconds=1001)) # '0:16:41' -print(humanize.naturaldelta(DT.timedelta(seconds=1001))) # '16 minutes' -print(humanize.naturaldelta(DT.timedelta(seconds=5))) # '5 seconds' -print(humanize.naturaldelta(DT.timedelta(hours=30))) # 'a day' -print(humanize.naturaldelta(DT.timedelta(hours=60))) # '2 days' +print(dt.timedelta(seconds=1001)) # '0:16:41' +print(humanize.naturaldelta(dt.timedelta(seconds=1001))) # '16 minutes' +print(humanize.naturaldelta(dt.timedelta(seconds=5))) # '5 seconds' +print(humanize.naturaldelta(dt.timedelta(hours=30))) # 'a day' +print(humanize.naturaldelta(dt.timedelta(hours=60))) # '2 days' print() -print(humanize.naturaldate(DT.date(2007, 6, 5))) # 'Jun 05 2007' -print(humanize.naturaldate(DT.date(2007, 6, 5))) # 'Jun 05 2007' +print(humanize.naturaldate(dt.date(2007, 6, 5))) # 'Jun 05 2007' +print(humanize.naturaldate(dt.date(2007, 6, 5))) # 'Jun 05 2007' print() -print(humanize.naturaltime(DT.datetime.now() - DT.timedelta(seconds=1))) # 'a second ago' -print(humanize.naturaltime(DT.datetime.now() - DT.timedelta(seconds=3600))) # 'an hour ago' -print(humanize.naturaltime(DT.datetime.now() - DT.timedelta(hours=30))) # 'a day ago' +print(humanize.naturaltime(dt.datetime.now() - dt.timedelta(seconds=1))) +# 'a second ago' +print(humanize.naturaltime(dt.datetime.now() - dt.timedelta(seconds=3600))) +# 'an hour ago' +print(humanize.naturaltime(dt.datetime.now() - dt.timedelta(hours=30))) +# 'a day ago' diff --git a/humanize__examples/file_size__humanization.py b/humanize__examples/file_size__humanization.py index c9f20d515..467a1ac63 100644 --- a/humanize__examples/file_size__humanization.py +++ b/humanize__examples/file_size__humanization.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/jmoiron/humanize @@ -12,7 +12,7 @@ # File size humanization: -print(humanize.naturalsize(100)) # '100 Bytes' -print(humanize.naturalsize(10 ** 9)) # '1.0 GB' -print(humanize.naturalsize(10 ** 9, binary=True)) # '953.7 MiB' -print(humanize.naturalsize(10 ** 9, gnu=True)) # '953.7M' +print(humanize.naturalsize(100)) # '100 Bytes' +print(humanize.naturalsize(10**9)) # '1.0 GB' +print(humanize.naturalsize(10**9, binary=True)) # '953.7 MiB' +print(humanize.naturalsize(10**9, gnu=True)) # '953.7M' diff --git a/humanize__examples/integer__humanization.py b/humanize__examples/integer__humanization.py index eab43bbe4..74741cbc8 100644 --- a/humanize__examples/integer__humanization.py +++ b/humanize__examples/integer__humanization.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/jmoiron/humanize @@ -12,15 +12,15 @@ # Integer humanization: -print(humanize.intcomma(12345)) # '12,345' +print(humanize.intcomma(12345)) # '12,345' print(humanize.intcomma(123456789)) # '123,456,789' print() -print(humanize.intword(123455913)) # '123.5 million' -print(humanize.intword(12345591313)) # '12.3 billion' +print(humanize.intword(123455913)) # '123.5 million' +print(humanize.intword(12345591313)) # '12.3 billion' print(humanize.intword(1339014900000)) # '1.3 trillion' print() -print(humanize.apnumber(4)) # 'four' -print(humanize.apnumber(7)) # 'seven' +print(humanize.apnumber(4)) # 'four' +print(humanize.apnumber(7)) # 'seven' print(humanize.apnumber(41)) # '41' diff --git a/humanize__examples/localization__change_locale_in_runtime.py b/humanize__examples/localization__change_locale_in_runtime.py index 210973124..4069cc376 100644 --- a/humanize__examples/localization__change_locale_in_runtime.py +++ b/humanize__examples/localization__change_locale_in_runtime.py @@ -1,41 +1,42 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/jmoiron/humanize +import datetime as dt + # pip install humanize import humanize -import datetime as DT # Localization. How to change locale in runtime -print(humanize.naturaltime(DT.timedelta(seconds=3))) # '3 seconds ago' -print(humanize.intword(123455913)) # '123.5 million' -print(humanize.intword(12345591313)) # '12.3 billion' +print(humanize.naturaltime(dt.timedelta(seconds=3))) # '3 seconds ago' +print(humanize.intword(123455913)) # '123.5 million' +print(humanize.intword(12345591313)) # '12.3 billion' print(humanize.intword(1339014900000)) # '1.3 trillion' -print(humanize.apnumber(4)) # 'four' -print(humanize.apnumber(7)) # 'seven' +print(humanize.apnumber(4)) # 'four' +print(humanize.apnumber(7)) # 'seven' print() -_t = humanize.i18n.activate('ru_RU') +_t = humanize.i18n.activate("ru_RU") -print(humanize.naturaltime(DT.timedelta(seconds=3))) # '3 секунды назад' -print(humanize.intword(123455913)) # '123.5 миллиона' -print(humanize.intword(12345591313)) # '12.3 миллиарда' +print(humanize.naturaltime(dt.timedelta(seconds=3))) # '3 секунды назад' +print(humanize.intword(123455913)) # '123.5 миллиона' +print(humanize.intword(12345591313)) # '12.3 миллиарда' print(humanize.intword(1339014900000)) # '1.3 триллиона' -print(humanize.apnumber(4)) # 'четыре' -print(humanize.apnumber(7)) # 'семь' +print(humanize.apnumber(4)) # 'четыре' +print(humanize.apnumber(7)) # 'семь' print() humanize.i18n.deactivate() -print(humanize.naturaltime(DT.timedelta(seconds=3))) # '3 seconds ago' -print(humanize.intword(123455913)) # '123.5 million' -print(humanize.intword(12345591313)) # '12.3 billion' +print(humanize.naturaltime(dt.timedelta(seconds=3))) # '3 seconds ago' +print(humanize.intword(123455913)) # '123.5 million' +print(humanize.intword(12345591313)) # '12.3 billion' print(humanize.intword(1339014900000)) # '1.3 trillion' -print(humanize.apnumber(4)) # 'four' -print(humanize.apnumber(7)) # 'seven' +print(humanize.apnumber(4)) # 'four' +print(humanize.apnumber(7)) # 'seven' diff --git a/i_watching_u.py b/i_watching_u.py index e6cd4d033..09b45012e 100644 --- a/i_watching_u.py +++ b/i_watching_u.py @@ -1,18 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Программа делает скриншоты, сохраняет их в zip-архив.""" -if __name__ == '__main__': - from PIL import ImageGrab - from datetime import datetime +if __name__ == "__main__": import time - from zipfile import ZipFile + + from datetime import datetime from io import BytesIO + from zipfile import ZipFile + + from PIL import ImageGrab # Сбор статистики о действиях пользователя @@ -21,32 +23,34 @@ today = datetime.today() # Получаем строку текущего времени - str_today_time = today.strftime('%H.%M.%S') + str_today_time = today.strftime("%H.%M.%S") # Имя zip-архива - zip_file_name = 'screenshots_' + today.strftime('%d.%m.%Y') + '.zip' + zip_file_name = "screenshots_" + today.strftime("%d.%m.%Y") + ".zip" # Составляем имя файла, содержащее в названии текущее время - file_name = 'screenshot_{}.png'.format(str_today_time) + file_name = f"screenshot_{str_today_time}.png" # Делаем скриншот im = ImageGrab.grab() - print('Сделан скриншот "{}"'.format(file_name)) + print(f'Сделан скриншот "{file_name}"') # Открываем zip-файл - with ZipFile(zip_file_name, 'a') as zip_file: + with ZipFile(zip_file_name, "a") as zip_file: # Сохраняем изображение в буфер io = BytesIO() - im.save(io, 'png') - + im.save(io, "png") + # Байты изображения image_bytes = io.getvalue() # Добавляем изображение в архив zip_file.writestr(file_name, image_bytes) - print(' Скриншот "{}" добавлен в архив "{}"\n'.format(file_name, zip_file_name)) + print( + f' Скриншот "{file_name}" добавлен в архив "{zip_file_name}"\n' + ) # Ожидание каждые 15 минут time.sleep(15 * 60) diff --git a/imagehash__examples/find_similar_images__phash.py b/imagehash__examples/find_similar_images__phash.py index 5c3f27bd1..139d3d426 100644 --- a/imagehash__examples/find_similar_images__phash.py +++ b/imagehash__examples/find_similar_images__phash.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/JohannesBuchner/imagehash -from glob import glob import itertools +from glob import glob # pip install imagehash import imagehash @@ -17,8 +17,8 @@ from PIL import Image -file_names = glob('D:\все фотки\**\*.jpg', recursive=True) -print(f'Files: {len(file_names)}') +file_names = glob(r"D:\все фотки\**\*.jpg", recursive=True) +print(f"Files: {len(file_names)}") img_by_hash = dict() @@ -31,7 +31,7 @@ img_by_hash[file_name] = hash_img -print('Find similar images') +print("Find similar images") for img_1, img_2 in itertools.combinations(img_by_hash.items(), 2): file_name_1, hash_img_1 = img_1 diff --git a/imagehash__examples/find_similar_images_with_group_and_sort__phash.py b/imagehash__examples/find_similar_images_with_group_and_sort__phash.py index 69780bde2..25b5f54e4 100644 --- a/imagehash__examples/find_similar_images_with_group_and_sort__phash.py +++ b/imagehash__examples/find_similar_images_with_group_and_sort__phash.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/JohannesBuchner/imagehash +import itertools + from collections import defaultdict from glob import glob -import itertools # pip install imagehash import imagehash @@ -18,8 +19,8 @@ from PIL import Image -file_names = glob('D:\все фотки\**\*.jpg', recursive=True) -print(f'Files: {len(file_names)}') +file_names = glob(r"D:\все фотки\**\*.jpg", recursive=True) +print(f"Files: {len(file_names)}") img_by_hash = dict() @@ -32,7 +33,7 @@ img_by_hash[file_name] = hash_img -print('\nFind similar images') +print("\nFind similar images") file_name_by_similars = defaultdict(list) diff --git a/imagehash__examples/greedy_find_similar_images_with_group_and_sort__phash.py b/imagehash__examples/greedy_find_similar_images_with_group_and_sort__phash.py index bfec48b2a..9bdbedc92 100644 --- a/imagehash__examples/greedy_find_similar_images_with_group_and_sort__phash.py +++ b/imagehash__examples/greedy_find_similar_images_with_group_and_sort__phash.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/JohannesBuchner/imagehash +import itertools + from collections import defaultdict from glob import glob -import itertools # pip install imagehash import imagehash @@ -18,8 +19,8 @@ from PIL import Image -file_names = glob('D:\все фотки\**\*.jpg', recursive=True) -print(f'Files: {len(file_names)}') +file_names = glob(r"D:\все фотки\**\*.jpg", recursive=True) +print(f"Files: {len(file_names)}") img_by_hash = dict() @@ -32,7 +33,7 @@ img_by_hash[file_name] = hash_img -print('\nFind similar images') +print("\nFind similar images") file_name_by_similars = defaultdict(list) diff --git a/img_to_base64_html/main.py b/img_to_base64_html/main.py index d011e3e27..e123b21d7 100644 --- a/img_to_base64_html/main.py +++ b/img_to_base64_html/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import base64 @@ -11,14 +11,14 @@ from PIL import Image -def img_to_base64_html(file_name__or__bytes__or__file_object): +def img_to_base64_html(file_name__or__bytes__or__file_object: str | bytes | io.IOBase) -> str: arg = file_name__or__bytes__or__file_object - if type(arg) == str: - with open(arg, mode='rb') as f: + if isinstance(arg, str): + with open(arg, mode="rb") as f: img_bytes = f.read() - elif type(arg) == bytes: + elif isinstance(arg, bytes): img_bytes = arg else: @@ -27,14 +27,14 @@ def img_to_base64_html(file_name__or__bytes__or__file_object): bytes_io = io.BytesIO(img_bytes) img = Image.open(bytes_io) - img_base64 = base64.b64encode(img_bytes).decode('utf-8') - return f'data:image/{img.format.lower()};base64,{img_base64}' + img_base64 = base64.b64encode(img_bytes).decode("utf-8") + return f"data:image/{img.format.lower()};base64,{img_base64}" -if __name__ == '__main__': - file_name = 'img.jpg' +if __name__ == "__main__": + file_name = "img.jpg" img_base64 = img_to_base64_html(file_name) - print(f'[len {len(img_base64)}]: {img_base64[:50]}...') + print(f"[len {len(img_base64)}]: {img_base64[:50]}...") - with open(file_name + '_base64.txt', mode='w', encoding='utf-8') as f: + with open(file_name + "_base64.txt", mode="w", encoding="utf-8") as f: f.write(img_base64) diff --git a/incremental timeouts.py b/incremental timeouts.py index c7428222f..319f02832 100644 --- a/incremental timeouts.py +++ b/incremental timeouts.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -10,12 +10,15 @@ """ +import random +import time + + def work(): - import random if random.randint(0, 5): raise Exception() - print('Work!') + print("Work!") timeout = 0.5 @@ -29,9 +32,7 @@ def work(): break except: - print('Неудача с таймаутом {}.'.format(timeout)) + print(f"Неудача с таймаутом {timeout}.") - import time time.sleep(timeout) timeout += 0.1 - diff --git a/infinity_iterator.py b/infinity_iterator.py index c7a440a4f..5d3d98e7b 100644 --- a/infinity_iterator.py +++ b/infinity_iterator.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def inf_it() -> iter: return iter(lambda: 0, 1) -if __name__ == '__main__': +if __name__ == "__main__": from tqdm import tqdm + for _ in tqdm(inf_it()): pass diff --git a/info_from_bitkurs.ru.py b/info_from_bitkurs.ru.py index f1c587f6b..6803a9b0e 100644 --- a/info_from_bitkurs.ru.py +++ b/info_from_bitkurs.ru.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re import grab -URL = 'http://bitkurs.ru/' +URL = "http://bitkurs.ru/" # Copy page yandex # URL = 'http://hghltd.yandex.net/yandbtm?fmode=inject&url=http%3A%2F%2Fbitkurs.ru%2F&tld=ru&lang=ru&la=1453455360&tm=1454924280&text=bitkurs.ru&l10n=ru&mime=html&sign=2105e628b413520a0e087ae4ac0db180&keyno=0' g = grab.Grab() -g.setup(proxy='proxy.compassplus.ru:3128', proxy_type='http') +g.setup(proxy="proxy.compassplus.ru:3128", proxy_type="http") g.go(URL) #
    @@ -30,14 +30,15 @@ def get_amt(text): # Удаление всех символов, кроме цифр и точки - text = re.sub('[^\\d\.]+', '', text) + text = re.sub(r"[^\d.]+", "", text) # Вытаскивание строки, описывающей число - text = re.search('\\d+(\.\\d*)?', text).group() + text = re.search(r"\d+(\.\d*)?", text).group() return text + usd = get_amt(usd) rub = get_amt(rub) eur = get_amt(eur) -print('1 BTC:\n {} USD\n {} RUB\n {} EUR'.format(usd, rub, eur)) +print(f"1 BTC:\n {usd} USD\n {rub} RUB\n {eur} EUR") diff --git a/ini__configparser__examples/hello_world.py b/ini__configparser__examples/hello_world.py index 23eb83e61..ba4269454 100644 --- a/ini__configparser__examples/hello_world.py +++ b/ini__configparser__examples/hello_world.py @@ -1,36 +1,36 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import configparser ini = configparser.ConfigParser() -ini['Default'] = { - 'x': 10, - 'y': 15, - 'z': 3, +ini["Default"] = { + "x": 10, + "y": 15, + "z": 3, } -ini['Additional'] = {} -additional = ini['Additional'] -additional['top'] = str(True) -additional['text'] = "Hello World!" -additional['arrays'] = str([1, 2, 3, 4, 5]) +ini["Additional"] = {} +additional = ini["Additional"] +additional["top"] = str(True) +additional["text"] = "Hello World!" +additional["arrays"] = str([1, 2, 3, 4, 5]) -ini['Empty'] = {} +ini["Empty"] = {} -with open('config.ini', 'w') as f: +with open("config.ini", "w") as f: ini.write(f) ini_read = configparser.ConfigParser() -ini_read.read('config.ini') +ini_read.read("config.ini") print(ini_read.sections()) -print(ini_read['Additional']['top']) -print(ini_read['Additional']['text']) -print(ini_read['Additional']['arrays']) -print(ini_read['Additional']['arrays'].replace('[', '').replace(']', '').split(', ')) +print(ini_read["Additional"]["top"]) +print(ini_read["Additional"]["text"]) +print(ini_read["Additional"]["arrays"]) +print(ini_read["Additional"]["arrays"].replace("[", "").replace("]", "").split(", ")) diff --git a/ini__configparser__examples/read__list_of_dicts.py b/ini__configparser__examples/read__list_of_dicts.py index e8d396fc6..c0d427268 100644 --- a/ini__configparser__examples/read__list_of_dicts.py +++ b/ini__configparser__examples/read__list_of_dicts.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/q/1270426/201445 @@ -11,9 +11,9 @@ config = configparser.ConfigParser() -config.read('example.ini') +config.read("example.ini") -print(dict(config['DEFAULT'])) +print(dict(config["DEFAULT"])) for section in config.sections(): print(dict(config[section])) diff --git a/ini__configparser__examples/write__list_of_dicts.py b/ini__configparser__examples/write__list_of_dicts.py index ddd72b078..2485467fb 100644 --- a/ini__configparser__examples/write__list_of_dicts.py +++ b/ini__configparser__examples/write__list_of_dicts.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/q/1270426/201445 @@ -11,17 +11,42 @@ dict1 = [ - {'object': 'label', 'icon': 'icon', 'icon_hover': 'icon_hover', 'command': 'command'}, - {'object': 'button', 'icon': 'icon', 'icon_hover': 'icon_hover', 'command': 'command'}, - {'object': 'color', 'icon': 'icon', 'icon_hover': 'icon_hover', 'command': 'command'}, - {'object': 'line', 'icon': 'icon', 'icon_hover': 'icon_hover', 'command': 'command'}, - {'object': 'unknown', 'icon': 'icon', 'icon_hover': 'icon_hover', 'command': 'command'}, + { + "object": "label", + "icon": "icon", + "icon_hover": "icon_hover", + "command": "command", + }, + { + "object": "button", + "icon": "icon", + "icon_hover": "icon_hover", + "command": "command", + }, + { + "object": "color", + "icon": "icon", + "icon_hover": "icon_hover", + "command": "command", + }, + { + "object": "line", + "icon": "icon", + "icon_hover": "icon_hover", + "command": "command", + }, + { + "object": "unknown", + "icon": "icon", + "icon_hover": "icon_hover", + "command": "command", + }, ] config = configparser.ConfigParser() for d in dict1: - config[d['object']] = d + config[d["object"]] = d -with open('example.ini', 'w') as f: +with open("example.ini", "w") as f: config.write(f) diff --git a/input_email_list.py b/input_email_list.py index 541fed476..5f365969a 100644 --- a/input_email_list.py +++ b/input_email_list.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # http://stackoverflow.com/questions/35332611/a-program-which-creates-emails -def fill_list(l, promt): + +def fill_list(l, promt) -> None: while True: x = input(promt) if not x: @@ -15,15 +16,15 @@ def fill_list(l, promt): l.append(x) -if __name__ == '__main__': +if __name__ == "__main__": first_names = list() last_names = list() fill_list(first_names, "Input first name: ") - print('first_names: {}'.format(first_names)) + print(f"first_names: {first_names}") fill_list(last_names, "Input last name: ") - print('last_names: {}'.format(last_names)) + print(f"last_names: {last_names}") for first, last in zip(first_names, last_names): - print('{}.{}@abc.mail.com'.format(first, last)) + print(f"{first}.{last}@abc.mail.com") diff --git a/inputs__examples/read_events_from_gamepad.py b/inputs__examples/read_events_from_gamepad.py new file mode 100644 index 000000000..1fd065a29 --- /dev/null +++ b/inputs__examples/read_events_from_gamepad.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install inputs +from inputs import get_gamepad + + +if __name__ == "__main__": + while True: + events = get_gamepad() + for event in events: + print(event.ev_type, event.code, event.state) diff --git a/inputs__examples/show_all_devices.py b/inputs__examples/show_all_devices.py new file mode 100644 index 000000000..ff435b884 --- /dev/null +++ b/inputs__examples/show_all_devices.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time + +# pip install inputs +from inputs import DeviceManager + + +while True: + devices = DeviceManager() + print( + f"keyboards: {len(devices.keyboards)}, mice: {len(devices.mice)}, " + f"gamepads: {len(devices.gamepads)}, microbits: {len(devices.microbits)}, " + f"leds: {len(devices.leds)}, other_devices: {len(devices.other_devices)}, " + f"all_devices: {len(devices.all_devices)}" + ) + time.sleep(1) diff --git a/inspect__examples/check_is_empty_function__use__inspect__dis.py b/inspect__examples/check_is_empty_function__use__inspect__dis.py index 53900e818..bc9188079 100644 --- a/inspect__examples/check_is_empty_function__use__inspect__dis.py +++ b/inspect__examples/check_is_empty_function__use__inspect__dis.py @@ -1,24 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from dis__bytecode__is_empty_function import check_is_empty_function as check_is_empty_function__use_dis from inspect__get_source__function import check_is_empty_function as check_is_empty_function__use_inspect -def foo(): +def foo() -> None: pass -def foo2(): +def foo2() -> int: return 1 -print('check_is_empty_function__use_inspect:', check_is_empty_function__use_inspect(foo)) -print('check_is_empty_function__use_inspect:', check_is_empty_function__use_inspect(foo2)) +print("check_is_empty_function__use_inspect:", check_is_empty_function__use_inspect(foo)) +print("check_is_empty_function__use_inspect:", check_is_empty_function__use_inspect(foo2)) print() -print('check_is_empty_function__use_dis:', check_is_empty_function__use_dis(foo)) -print('check_is_empty_function__use_dis:', check_is_empty_function__use_dis(foo2)) +print("check_is_empty_function__use_dis:", check_is_empty_function__use_dis(foo)) +print("check_is_empty_function__use_dis:", check_is_empty_function__use_dis(foo2)) diff --git a/inspect__examples/hello_world.py b/inspect__examples/hello_world.py index aa4c602dc..7ee1f58d3 100644 --- a/inspect__examples/hello_world.py +++ b/inspect__examples/hello_world.py @@ -1,29 +1,34 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import inspect -print('Current line:', inspect.getframeinfo(inspect.currentframe()).lineno) -print('Current line:', inspect.getframeinfo(inspect.currentframe()).lineno) -print('Current line:', inspect.getframeinfo(inspect.currentframe()).lineno) +print("Current line:", inspect.getframeinfo(inspect.currentframe()).lineno) +print("Current line:", inspect.getframeinfo(inspect.currentframe()).lineno) +print("Current line:", inspect.getframeinfo(inspect.currentframe()).lineno) print() -print('Current filename:', inspect.getframeinfo(inspect.currentframe()).filename) +print("Current filename:", inspect.getframeinfo(inspect.currentframe()).filename) print() -print('Current code context:', inspect.getframeinfo(inspect.currentframe()).code_context) +print( + "Current code context:", inspect.getframeinfo(inspect.currentframe()).code_context +) print() -print('Current function:', inspect.getframeinfo(inspect.currentframe()).function) -print('Current function:', (lambda: inspect.getframeinfo(inspect.currentframe()).function)()) +print("Current function:", inspect.getframeinfo(inspect.currentframe()).function) +print( + "Current function:", + (lambda: inspect.getframeinfo(inspect.currentframe()).function)(), +) def foo(): return inspect.getframeinfo(inspect.currentframe()).function -print('Current function:', foo()) +print("Current function:", foo()) diff --git a/inspect__examples/inspect__get_source__function.py b/inspect__examples/inspect__get_source__function.py index b17eab794..1f9cbbcba 100644 --- a/inspect__examples/inspect__get_source__function.py +++ b/inspect__examples/inspect__get_source__function.py @@ -1,21 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import inspect def check_is_empty_function(function): - import inspect lines = inspect.getsourcelines(function) - return lines[0][-1].strip() == 'pass' + return lines[0][-1].strip() == "pass" + +if __name__ == "__main__": -if __name__ == '__main__': - def foo(): + def foo() -> None: pass - def foo2(): + def foo2() -> int: return 1 - print('Empty:', check_is_empty_function(foo)) - print('Empty:', check_is_empty_function(foo2)) + print("Empty:", check_is_empty_function(foo)) + print("Empty:", check_is_empty_function(foo2)) diff --git a/inspect__examples/is_function.py b/inspect__examples/is_function.py index 9419af8a0..ba007dc9c 100644 --- a/inspect__examples/is_function.py +++ b/inspect__examples/is_function.py @@ -1,30 +1,34 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def foo(): +import inspect + + + +def foo() -> int: return 1 class Foo: - def __call__(self): + def __call__(self) -> int: return 1 -import inspect print(inspect.isroutine(lambda: 1)) # True -print(inspect.isroutine(foo)) # True +print(inspect.isroutine(foo)) # True print() # Functor foo = Foo() -print(inspect.isroutine(foo)) # False -print(hasattr(foo, '__call__')) # True -print(callable(foo)) # True +print(inspect.isroutine(foo)) # False +print(hasattr(foo, "__call__")) # True +print(callable(foo)) # True print() import math + print(inspect.isroutine(math.sin)) # True -print(inspect.isroutine(print)) # True +print(inspect.isroutine(print)) # True diff --git a/invert_number.py b/invert_number.py index 18fb08adf..b32538251 100644 --- a/invert_number.py +++ b/invert_number.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def invert_number(number: int) -> int: @@ -14,18 +14,18 @@ def invert_number(number: int) -> int: :return: invert number """ - return int(''.join('1' if i == '0' else '0' for i in bin(number)[2:]), base=2) + return int("".join("1" if i == "0" else "0" for i in bin(number)[2:]), base=2) -if __name__ == '__main__': +if __name__ == "__main__": print(invert_number(0)) print(invert_number(1)) print(invert_number(7)) print(invert_number(111)) - print(invert_number(10 ** 10)) + print(invert_number(10**10)) assert invert_number(0) == 1 assert invert_number(1) == 0 assert invert_number(7) == 0 assert invert_number(111) == 16 - assert invert_number(10 ** 10) == 7179869183 + assert invert_number(10**10) == 7179869183 diff --git a/iri_to_uri__cyrillic__no_ascii_url__idna.py b/iri_to_uri__cyrillic__no_ascii_url__idna.py index 43ea72bfb..ca08bd768 100644 --- a/iri_to_uri__cyrillic__no_ascii_url__idna.py +++ b/iri_to_uri__cyrillic__no_ascii_url__idna.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/a/487922/201445 @@ -10,45 +10,53 @@ from urllib.parse import quote, urlsplit, urlunsplit -def iri_to_uri(iri): +def iri_to_uri(iri: str) -> str: parts = urlsplit(iri) - uri = urlunsplit(( - parts.scheme, - parts.netloc.encode('idna').decode('ascii'), - quote(parts.path), - quote(parts.query, '=&'), - quote(parts.fragment), - )) + uri = urlunsplit( + ( + parts.scheme, + parts.netloc.encode("idna").decode("ascii"), + quote(parts.path), + quote(parts.query, "=&"), + quote(parts.fragment), + ) + ) return uri -if __name__ == '__main__': - url = 'https://сайт.рф/документы?номер=1234&устройство=телефон' +if __name__ == "__main__": + url = "https://сайт.рф/документы?номер=1234&устройство=телефон" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://xn--80aswg.xn--p1ai/%D0%B4%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D1%8B?%D0%BD%D0%BE%D0%BC%D0%B5%D1%80=1234&%D1%83%D1%81%D1%82%D1%80%D0%BE%D0%B9%D1%81%D1%82%D0%B2%D0%BE=%D1%82%D0%B5%D0%BB%D0%B5%D1%84%D0%BE%D0%BD' + assert ( + new_url + == "https://xn--80aswg.xn--p1ai/%D0%B4%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D1%8B?%D0%BD%D0%BE%D0%BC%D0%B5%D1%80=1234&%D1%83%D1%81%D1%82%D1%80%D0%BE%D0%B9%D1%81%D1%82%D0%B2%D0%BE=%D1%82%D0%B5%D0%BB%D0%B5%D1%84%D0%BE%D0%BD" + ) - url = 'https://anapa.russianrealty.ru/Продажа-квартир/' + url = "https://anapa.russianrealty.ru/Продажа-квартир/" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://anapa.russianrealty.ru/%D0%9F%D1%80%D0%BE%D0%B4%D0%B0%D0%B6%D0%B0-%D0%BA%D0%B2%D0%B0%D1%80%D1%82%D0%B8%D1%80/' + assert ( + new_url + == "https://anapa.russianrealty.ru/%D0%9F%D1%80%D0%BE%D0%B4%D0%B0%D0%B6%D0%B0-%D0%BA%D0%B2%D0%B0%D1%80%D1%82%D0%B8%D1%80/" + ) - url = 'http://www.a\u0131b.com/a\u0131b' + url = "http://www.a\u0131b.com/a\u0131b" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'http://www.xn--ab-hpa.com/a%C4%B1b' + assert new_url == "http://www.xn--ab-hpa.com/a%C4%B1b" - url = 'https://ya.ru' + url = "https://ya.ru" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://ya.ru' + assert new_url == "https://ya.ru" - url = 'https://google.com' + url = "https://google.com" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://google.com' + assert new_url == "https://google.com" - url = 'http://домены.рф' + url = "http://домены.рф" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'http://xn--d1acufc5f.xn--p1ai' + assert new_url == "http://xn--d1acufc5f.xn--p1ai" diff --git a/iri_to_uri__get_ascii_url.py b/iri_to_uri__get_ascii_url.py index 41cf72661..e49d9da6a 100644 --- a/iri_to_uri__get_ascii_url.py +++ b/iri_to_uri__get_ascii_url.py @@ -1,53 +1,59 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from urllib.parse import urlparse, urlunparse import re +from urllib.parse import urlparse, urlunparse def url_encode_non_ascii(b: bytes) -> bytes: - return re.sub(b'[\x80-\xFF]', lambda c: b'%%%02X' % ord(c.group(0)), b) + return re.sub(b"[\x80-\xFF]", lambda c: b"%%%02X" % ord(c.group(0)), b) # SOURCE: https://stackoverflow.com/a/4391299/5909792 def iri_to_uri(iri: str) -> str: parts = urlparse(iri) return urlunparse( - part.encode('idna') if parti == 1 else url_encode_non_ascii(part.encode('utf-8')) + part.encode("idna") if parti == 1 else url_encode_non_ascii(part.encode("utf-8")) for parti, part in enumerate(parts) - ).decode('utf-8') + ).decode("utf-8") -if __name__ == '__main__': - url = 'https://сайт.рф/документы?номер=1234&устройство=телефон' +if __name__ == "__main__": + url = "https://сайт.рф/документы?номер=1234&устройство=телефон" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://xn--80aswg.xn--p1ai/%D0%B4%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D1%8B?%D0%BD%D0%BE%D0%BC%D0%B5%D1%80=1234&%D1%83%D1%81%D1%82%D1%80%D0%BE%D0%B9%D1%81%D1%82%D0%B2%D0%BE=%D1%82%D0%B5%D0%BB%D0%B5%D1%84%D0%BE%D0%BD' + assert ( + new_url + == "https://xn--80aswg.xn--p1ai/%D0%B4%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D1%8B?%D0%BD%D0%BE%D0%BC%D0%B5%D1%80=1234&%D1%83%D1%81%D1%82%D1%80%D0%BE%D0%B9%D1%81%D1%82%D0%B2%D0%BE=%D1%82%D0%B5%D0%BB%D0%B5%D1%84%D0%BE%D0%BD" + ) - url = 'https://anapa.russianrealty.ru/Продажа-квартир/' + url = "https://anapa.russianrealty.ru/Продажа-квартир/" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://anapa.russianrealty.ru/%D0%9F%D1%80%D0%BE%D0%B4%D0%B0%D0%B6%D0%B0-%D0%BA%D0%B2%D0%B0%D1%80%D1%82%D0%B8%D1%80/' + assert ( + new_url + == "https://anapa.russianrealty.ru/%D0%9F%D1%80%D0%BE%D0%B4%D0%B0%D0%B6%D0%B0-%D0%BA%D0%B2%D0%B0%D1%80%D1%82%D0%B8%D1%80/" + ) - url = 'http://www.a\u0131b.com/a\u0131b' + url = "http://www.a\u0131b.com/a\u0131b" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'http://www.xn--ab-hpa.com/a%C4%B1b' + assert new_url == "http://www.xn--ab-hpa.com/a%C4%B1b" - url = 'https://ya.ru' + url = "https://ya.ru" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://ya.ru' + assert new_url == "https://ya.ru" - url = 'https://google.com' + url = "https://google.com" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'https://google.com' + assert new_url == "https://google.com" - url = 'http://домены.рф' + url = "http://домены.рф" new_url = iri_to_uri(url) print(new_url) - assert new_url == 'http://xn--d1acufc5f.xn--p1ai' + assert new_url == "http://xn--d1acufc5f.xn--p1ai" diff --git a/is_correct_brackets.py b/is_correct_brackets.py index b9941d0e4..77bf8f993 100644 --- a/is_correct_brackets.py +++ b/is_correct_brackets.py @@ -1,23 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def is_correct_brackets(text): - while '()' in text or '[]' in text or '{}' in text: - text = text.replace('()', '') - text = text.replace('[]', '') - text = text.replace('{}', '') +def is_correct_brackets(text) -> bool: + while "()" in text or "[]" in text or "{}" in text: + text = text.replace("()", "") + text = text.replace("[]", "") + text = text.replace("{}", "") # Возвращаем True, если text с пустой строкой return not text -if __name__ == '__main__': - assert is_correct_brackets('(((())))') - assert is_correct_brackets('(((())') is False - assert is_correct_brackets('())))') is False - assert is_correct_brackets('((((){}[]{}[])))') - assert is_correct_brackets('(){}[]{}[])))') is False - assert is_correct_brackets('(){}[]{}[]') +if __name__ == "__main__": + assert is_correct_brackets("(((())))") + assert is_correct_brackets("(((())") is False + assert is_correct_brackets("())))") is False + assert is_correct_brackets("((((){}[]{}[])))") + assert is_correct_brackets("(){}[]{}[])))") is False + assert is_correct_brackets("(){}[]{}[]") diff --git a/is_even__is_odd.py b/is_even__is_odd.py index 1d31fb9ae..2228e8c6c 100644 --- a/is_even__is_odd.py +++ b/is_even__is_odd.py @@ -1,23 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def is_even(num): +def is_even(num) -> bool: return num % 2 == 0 -def is_odd(num): +def is_odd(num) -> bool: return not is_even(num) -def is_even_2(num): +def is_even_2(num) -> bool: return num & 1 == 0 -if __name__ == '__main__': +if __name__ == "__main__": for i in range(10): - print('{} is even: {}, {}'.format(i, is_even(i), is_even_2(i))) - print('{} is odd: {}'.format(i, is_odd(i))) + print(f"{i} is even: {is_even(i)}, {is_even_2(i)}") + print(f"{i} is odd: {is_odd(i)}") print() diff --git a/is_free_port.py b/is_free_port.py new file mode 100644 index 000000000..d825d8a80 --- /dev/null +++ b/is_free_port.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import socket + + +def is_free_port(port: int) -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.bind(("", port)) + return True + except OSError: + return False + + +if __name__ == "__main__": + print(is_free_port(5510)) + print(is_free_port(8080)) + print(is_free_port(9999)) diff --git a/is_ip.py b/is_ip.py index 21b9ebbda..ec07a4be1 100644 --- a/is_ip.py +++ b/is_ip.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re @@ -9,17 +9,21 @@ # SOURCE: https://stackoverflow.com/a/23166561/5909792 IP_RANGE = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])" -IP_REGEXP = f'^{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}$' +IP_REGEXP = f"^{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}$" def is_ip(ip: str) -> bool: return bool(re.match(IP_REGEXP, ip)) -if __name__ == '__main__': +if __name__ == "__main__": tests = [ - ('127.0.0.1', True), ('100.100.100.100', True), ('0.0.0.0', True), ('300.300.300.300', False), - ('300.0.0.1', False), ('255.255.255.255', True) + ("127.0.0.1", True), + ("100.100.100.100", True), + ("0.0.0.0", True), + ("300.300.300.300", False), + ("300.0.0.1", False), + ("255.255.255.255", True), ] for ip, expected in tests: diff --git a/is_math_magic_square.py b/is_math_magic_square.py index 0ee9f457d..6715cc329 100644 --- a/is_math_magic_square.py +++ b/is_math_magic_square.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Для решения головоломки в локации аэропорта из Grim Tales 9: Threads Of Destiny diff --git a/is_user_admin.py b/is_user_admin.py index c3bbc9440..d1495c90b 100644 --- a/is_user_admin.py +++ b/is_user_admin.py @@ -1,34 +1,43 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/19719292/5909792 -def is_user_admin(): - import os +import ctypes +import os +import traceback - if os.name == 'nt': - import ctypes + +def is_windows() -> bool: + return os.name == "nt" + + +# Linux or mac +def is_posix() -> bool: + return os.name == "posix" + + +def is_user_admin() -> bool: + if is_windows(): try: - # WARNING: requires Windows XP SP2 or higher! + # WARNING: Requires Windows XP SP2 or higher! return bool(ctypes.windll.shell32.IsUserAnAdmin()) - - except: - import traceback + except (AttributeError, OSError): traceback.print_exc() print("Admin check failed, assuming not an admin.") return False - elif os.name == 'posix': + elif is_posix(): # Check for root on Posix - return os.getuid() == 0 + return os.geteuid() == 0 else: - raise RuntimeError("Unsupported operating system for this module: %s" % (os.name,)) + raise RuntimeError(f"Unsupported operating system for this module: {os.name}") -if __name__ == '__main__': - print(is_user_admin()) +if __name__ == "__main__": + print("is_user_admin:", is_user_admin()) diff --git a/is_valid_ip.py b/is_valid_ip.py index 10c180608..c1082430f 100644 --- a/is_valid_ip.py +++ b/is_valid_ip.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def is_valid_ip(ip): - import ipaddress +import ipaddress + +def is_valid_ip(ip) -> bool: try: ipaddress.IPv4Address(ip) return True - except ipaddress.AddressValueError: return False -if __name__ == '__main__': - assert is_valid_ip('127.0.0.1') is True - assert is_valid_ip('255.255.255.255') is True - assert is_valid_ip('127.0.0.0.1') is False - assert is_valid_ip('127.0.0.0.-1') is False +if __name__ == "__main__": + assert is_valid_ip("127.0.0.1") is True + assert is_valid_ip("255.255.255.255") is True + assert is_valid_ip("127.0.0.0.1") is False + assert is_valid_ip("127.0.0.0.-1") is False diff --git a/isinstance_example/isinstance.py b/isinstance_example/isinstance.py index 1f0e40533..c7f5bc24a 100644 --- a/isinstance_example/isinstance.py +++ b/isinstance_example/isinstance.py @@ -1,9 +1,8 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - print(isinstance(5, str)) - print(isinstance(5, int)) - print(isinstance(5, bool)) - print(isinstance(False, bool)) - print(isinstance("Hello", str)) \ No newline at end of file +print(isinstance(5, str)) +print(isinstance(5, int)) +print(isinstance(5, bool)) +print(isinstance(False, bool)) +print(isinstance("Hello", str)) diff --git a/jazzcinema_ru/print_all_coming_films.py b/jazzcinema_ru/print_all_coming_films.py index 33291057e..36d705982 100644 --- a/jazzcinema_ru/print_all_coming_films.py +++ b/jazzcinema_ru/print_all_coming_films.py @@ -1,48 +1,50 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +from datetime import datetime + from urllib.parse import urljoin from urllib.request import urlopen -from datetime import datetime - from bs4 import BeautifulSoup -if __name__ == '__main__': - url = 'http://www.jazzcinema.ru/' +if __name__ == "__main__": + url = "http://www.jazzcinema.ru/" with urlopen(url) as f: - root = BeautifulSoup(f.read(), 'lxml') + root = BeautifulSoup(f.read(), "lxml") # Список расписаний - schedule_list = root.select('.schedule') + schedule_list = root.select(".schedule") for schedule in schedule_list: - schedule_date = datetime.strptime(schedule['rel'], 'calendar-%Y-%m-%d-schedule').strftime('%d/%m/%Y') - border_list = schedule.select('.border') + schedule_date = datetime.strptime( + schedule["rel"], "calendar-%Y-%m-%d-schedule" + ).strftime("%d/%m/%Y") + border_list = schedule.select(".border") # Если фильмов нет if not border_list: continue - print('Расписание фильмов за {}:'.format(schedule_date)) + print(f"Расписание фильмов за {schedule_date}:") # Получение фильмов в текущей вкладке (по идеи, текущая вкладка -- текущий день) - for border in schedule.select('.border'): - a = border.select_one('.movie .title > a') - url = urljoin(url, a['href']) - print(' "{}": {}'.format(a['title'], url)) - genre = border.select_one('.genre') + for border in schedule.select(".border"): + a = border.select_one(".movie .title > a") + url = urljoin(url, a["href"]) + print(f' "{a["title"]}": {url}') + genre = border.select_one(".genre") if genre: - print(' {}'.format(genre.text)) + print(f" {genre.text}") - for seanse in border.select('.seanses li'): - time = seanse.select_one('a').text - price = seanse.select_one('.price').text - print(' {} : {}'.format(time, price)) + for seanse in border.select(".seanses li"): + time = seanse.select_one("a").text + price = seanse.select_one(".price").text + print(f" {time} : {price}") print() diff --git a/jazzcinema_ru/print_today_films.py b/jazzcinema_ru/print_today_films.py index 3e964530b..c73f5a354 100644 --- a/jazzcinema_ru/print_today_films.py +++ b/jazzcinema_ru/print_today_films.py @@ -1,52 +1,54 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +from datetime import datetime, date + from urllib.parse import urljoin from urllib.request import urlopen from bs4 import BeautifulSoup -if __name__ == '__main__': - url = 'http://www.jazzcinema.ru/' +if __name__ == "__main__": + url = "http://www.jazzcinema.ru/" with urlopen(url) as f: - root = BeautifulSoup(f.read(), 'lxml') + root = BeautifulSoup(f.read(), "lxml") # Список расписаний - schedule_list = root.select('.schedule') + schedule_list = root.select(".schedule") - from datetime import datetime, date today = date.today() - today_found = False # Проходим по списку и ищем расписание на текущую дату for schedule in schedule_list: - schedule_date = datetime.strptime(schedule['rel'], 'calendar-%Y-%m-%d-schedule').date() - schedule_date_str = schedule_date.strftime('%d/%m/%Y') + schedule_date = datetime.strptime( + schedule["rel"], "calendar-%Y-%m-%d-schedule" + ).date() + schedule_date_str = schedule_date.strftime("%d/%m/%Y") - border_list = schedule.select('.border') + border_list = schedule.select(".border") if schedule_date == today and border_list: today_found = True - print('Расписание фильмов на сегодня {}:'.format(schedule_date_str)) + print(f"Расписание фильмов на сегодня {schedule_date_str}:") # Получение фильмов в текущей вкладке (по идеи, текущая вкладка -- текущий день) for border in border_list: - a = border.select_one('.movie .title > a') - url = urljoin(url, a['href']) - print(' "{}": {}'.format(a['title'], url)) - print(' {}'.format(border.select_one('.genre').text)) + a = border.select_one(".movie .title > a") + url = urljoin(url, a["href"]) + print(f' "{a["title"]}": {url}') + print(f" {border.select_one('.genre').text}") - for seanse in border.select('.seanses li'): - time = seanse.select_one('a').text - price = seanse.select_one('.price').text - print(' {} : {}'.format(time, price)) + for seanse in border.select(".seanses li"): + time = seanse.select_one("a").text + price = seanse.select_one(".price").text + print(f" {time} : {price}") print() if not today_found: - print("Фильмов на сегодня ({}) нет".format(today.strftime('%d/%m/%Y'))) + print(f"Фильмов на сегодня ({today.strftime('%d/%m/%Y')}) нет") diff --git a/jazzcinema_ru/show_schedule_gui/main.py b/jazzcinema_ru/show_schedule_gui/main.py index 80ec6d82a..b49e40b43 100644 --- a/jazzcinema_ru/show_schedule_gui/main.py +++ b/jazzcinema_ru/show_schedule_gui/main.py @@ -1,15 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from urllib.parse import urljoin -from urllib.request import urlopen - -from datetime import datetime +import base64 +import logging +import traceback +import sys from collections import OrderedDict +from datetime import datetime +from urllib.parse import urljoin +from urllib.request import urlopen from bs4 import BeautifulSoup @@ -18,47 +21,43 @@ from qtpy.QtCore import * -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) - import traceback - text += ''.join(traceback.format_tb(tb)) - - import logging logging.critical(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions -URL = 'http://www.jazzcinema.ru/schedule/' +URL = "http://www.jazzcinema.ru/schedule/" class Movie: - def __init__(self, border): - a = border.select_one('.movie .title > a') - self.movie_url = urljoin(URL, a['href']) + def __init__(self, border) -> None: + a = border.select_one(".movie .title > a") + self.movie_url = urljoin(URL, a["href"]) - self.title = a['title'] + self.title = a["title"] - self.genre = border.select_one('.genre') + self.genre = border.select_one(".genre") if self.genre: self.genre = self.genre.text self.seanses = OrderedDict() - for seanse in border.select('.seanses li'): - time = seanse.select_one('a').text - price = seanse.select_one('.price').text + for seanse in border.select(".seanses li"): + time = seanse.select_one("a").text + price = seanse.select_one(".price").text self.seanses[time] = price movie_info = border.next_sibling - self.annotation = movie_info.select_one('.text').text.strip() + self.annotation = movie_info.select_one(".text").text.strip() - img_url = movie_info.select_one('.poster img')["src"] + img_url = movie_info.select_one(".poster img")["src"] self.img_url = urljoin(URL, img_url) # Начало и конец проката @@ -69,29 +68,29 @@ def __init__(self, border): self.producer = None self.actors = None self.duration = None - self.age_restrictions = movie_info.select_one('.text.age-count').text.strip() + self.age_restrictions = movie_info.select_one(".text.age-count").text.strip() - for td in movie_info.select('table td'): + for td in movie_info.select("table td"): key = td.text.strip() if key.endswith(":"): value = td.next_sibling.text.strip() - if 'Начало проката:' == key: + if "Начало проката:" == key: self.start_rentals = value - elif 'Окончание проката:' == key: + elif "Окончание проката:" == key: self.end_rentals = value - elif 'Страна:' == key: + elif "Страна:" == key: self.country = value - elif 'Режиссёр:' == key: + elif "Режиссёр:" == key: self.producer = value - elif 'В ролях:' == key: + elif "В ролях:" == key: self.actors = value - elif 'Продолжительность:' == key: + elif "Продолжительность:" == key: self.duration = value class MovieInfoWidget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() # TODO: можно и видео добавить @@ -104,19 +103,20 @@ def __init__(self): self.setLayout(layout) - def set_movie(self, movie): + def set_movie(self, movie) -> None: data = dict() # Скачивание обложки и получени base64 with urlopen(movie.img_url) as f: - import base64 - img_base64 = base64.standard_b64encode(f.read()).decode('utf-8') + img_base64 = base64.standard_b64encode(f.read()).decode("utf-8") data["img_url"] = "data:image/png;base64," + img_base64 - seanses_table = '' + seanses_table = "
    " for time, price in movie.seanses.items(): - seanses_table += "".format(time, price) + seanses_table += ( + f"" + ) seanses_table += "
    {}    {}
    {time}    {price}
    " @@ -159,20 +159,27 @@ def set_movie(self, movie): - """.format(movie, **data) + """.format( + movie, **data + ) self.browser.setHtml(html) class SchedulerMoviePage(QWidget): - def __init__(self, schedule): + def __init__(self, schedule) -> None: super().__init__() - self.schedule_date_str = datetime.strptime(schedule['rel'], 'calendar-%Y-%m-%d-schedule').strftime('%d/%m/%Y') + self.schedule_date_str = datetime.strptime( + schedule["rel"], "calendar-%Y-%m-%d-schedule" + ).strftime("%d/%m/%Y") self.movie_list_widget = QListWidget() - self.movie_list_widget.currentItemChanged.connect(lambda current, previous: - self.movie_info.set_movie(current.data(Qt.UserRole))) + self.movie_list_widget.currentItemChanged.connect( + lambda current, previous: self.movie_info.set_movie( + current.data(Qt.UserRole) + ) + ) self.movie_info = MovieInfoWidget() @@ -185,7 +192,7 @@ def __init__(self, schedule): self.setLayout(layout) # Получение фильмов в текущей вкладке (в каждой вкладке будет свой список фильмов на день) - for border in schedule.select('.border'): + for border in schedule.select(".border"): movie = Movie(border) item = QListWidgetItem(movie.title) @@ -196,15 +203,15 @@ def __init__(self, schedule): class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('show_schedule_gui.py') + self.setWindowTitle("show_schedule_gui.py") self.tab_widget = QTabWidget() layout = QVBoxLayout() - layout.addWidget(QLabel('Расписание фильмов на:')) + layout.addWidget(QLabel("Расписание фильмов на:")) layout.addWidget(self.tab_widget) widget = QWidget() @@ -212,24 +219,24 @@ def __init__(self): self.setCentralWidget(widget) - def load(self): + def load(self) -> None: with urlopen(URL) as f: - root = BeautifulSoup(f.read(), 'lxml') + root = BeautifulSoup(f.read(), "lxml") # Список расписаний - schedule_list = root.select('.schedule') + schedule_list = root.select(".schedule") # Проходим по списку расписаний for schedule in schedule_list: # Если фильмов нет - if not schedule.select('.border'): + if not schedule.select(".border"): continue tab = SchedulerMoviePage(schedule) self.tab_widget.addTab(tab, tab.schedule_date_str) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/jinja2__examples/hello_world.py b/jinja2__examples/hello_world.py index 4f44a8558..afb190cc0 100644 --- a/jinja2__examples/hello_world.py +++ b/jinja2__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from dataclasses import dataclass @@ -16,19 +16,21 @@ class User: url: str -template = jinja2.Template("""\ +template = jinja2.Template( + """\ {{ title }} -""") +""" +) users = [ - User('1', 'https://a.bc/user/1'), - User('2', 'https://a.bc/user/2'), - User('3', 'https://a.bc/user/3'), + User("1", "https://a.bc/user/1"), + User("2", "https://a.bc/user/2"), + User("3", "https://a.bc/user/3"), ] html = template.render(title="Hello World!", users=users) diff --git a/jinja2__examples/table_by_date_name_number.py b/jinja2__examples/table_by_date_name_number.py index 4f1d332b7..3a1de2392 100644 --- a/jinja2__examples/table_by_date_name_number.py +++ b/jinja2__examples/table_by_date_name_number.py @@ -1,24 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime from collections import defaultdict +from datetime import datetime import jinja2 array1 = [ - [datetime.datetime(2020, 10, 27, 12, 37), 'Саша'], - [datetime.datetime(2020, 10, 28, 16, 2), 'Олег'], - [datetime.datetime(2020, 10, 27, 16, 40), 'Саша'], - [datetime.datetime(2020, 10, 27, 16, 41), 'Саша'], - [datetime.datetime(2020, 10, 27, 12, 54), 'Костя'], - [datetime.datetime(2020, 10, 27, 12, 27), 'Костя'], - [datetime.datetime(2020, 10, 27, 12, 27), 'Олег'], - [datetime.datetime(2020, 10, 27, 12, 54), 'Саша'] + [datetime(2020, 10, 27, 12, 37), "Саша"], + [datetime(2020, 10, 28, 16, 2), "Олег"], + [datetime(2020, 10, 27, 16, 40), "Саша"], + [datetime(2020, 10, 27, 16, 41), "Саша"], + [datetime(2020, 10, 27, 12, 54), "Костя"], + [datetime(2020, 10, 27, 12, 27), "Костя"], + [datetime(2020, 10, 27, 12, 27), "Олег"], + [datetime(2020, 10, 27, 12, 54), "Саша"], ] result = defaultdict(dict) @@ -37,7 +37,8 @@ dates.add(date) -template = jinja2.Template("""\ +template = jinja2.Template( + """\ @@ -54,7 +55,8 @@ {% endfor %}
    Отчет
    -""") +""" +) html_tab = template.render( dates=sorted(dates), diff --git a/jinja2__examples/table_tr_class_by_condition.py b/jinja2__examples/table_tr_class_by_condition.py index b1f2e4f12..d879f4662 100644 --- a/jinja2__examples/table_tr_class_by_condition.py +++ b/jinja2__examples/table_tr_class_by_condition.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install jinja2 import jinja2 -template = jinja2.Template("""\ +template = jinja2.Template( + """\ {% for x in rows_1 %} @@ -21,18 +22,19 @@ {% endfor %}
    -""") +""" +) in_dict = { - 'a': [1, 2, 3], - 'b': [3, 4, 5], - 'c': [99, 3], + "a": [1, 2, 3], + "b": [3, 4, 5], + "c": [99, 3], } html = template.render( rows_1=[k for k, v in in_dict.items()], - rows_2=[sum(v) for k, v in in_dict.items()] + rows_2=[sum(v) for k, v in in_dict.items()], ) print(html) """ @@ -56,4 +58,4 @@ -""" \ No newline at end of file +""" diff --git a/jira_logged_human_time_to_seconds.py b/jira_logged_human_time_to_seconds.py new file mode 100644 index 000000000..1a92ae403 --- /dev/null +++ b/jira_logged_human_time_to_seconds.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from enum import IntEnum + + +class UnitSeconds(IntEnum): + MINUTE = 60 + HOUR = 60 * MINUTE + DAY = 8 * HOUR + WEEK = 5 * DAY + + +def logged_human_time_to_seconds(human_time: str) -> int: + """Конвертирование человеко-читаемого времени в секунды. + + >>> logged_human_time_to_seconds("1 minute") + 60 + >>> logged_human_time_to_seconds("1 hour") + 3600 + >>> logged_human_time_to_seconds("6 hours, 30 minutes") + 23400 + >>> logged_human_time_to_seconds("1 day, 30 minutes") + 30600 + """ + + # Jira help: + # You can specify a time unit after a time value 'X', such as Xw, Xd, Xh or Xm, to represent weeks (w), + # days (d), hours (h) and minutes (m), respectively. + # If you do not specify a time unit, minute will be assumed. + # Your current conversion rates are 1w = 5d and 1d = 8h. + + total_seconds = 0 + for part in human_time.split(", "): + value, metric = part.upper().split() + + value = int(value) + metric = metric.removesuffix("S") # NOTE: "HOURS" -> "HOUR" + + total_seconds += value * UnitSeconds[metric] + + return total_seconds + + +def seconds_to_logged_human_time(seconds: int) -> str: + weeks, seconds = divmod(seconds, UnitSeconds.WEEK) + days, seconds = divmod(seconds, UnitSeconds.DAY) + hours, seconds = divmod(seconds, UnitSeconds.HOUR) + minutes, _ = divmod(seconds, UnitSeconds.MINUTE) + + get_suffix = lambda n: 's' if n > 1 else '' + items = [ + f"{weeks} week{get_suffix(weeks)}" if weeks else '', + f"{days} day{get_suffix(days)}" if days else '', + f"{hours} hour{get_suffix(hours)}" if hours else '', + f"{minutes} minute{get_suffix(minutes)}" if minutes else '', + ] + return ", ".join(filter(None, items)) + + +if __name__ == "__main__": + items = [ + ("2 hours", 2 * 60 * 60), + ("1 hour, 15 minutes", 3600 + 15 * 60), + ("1 hour, 30 minutes", 3600 + 1800), + ("4 hours", 4 * 3600), + ("7 hours", 7 * 3600), + ("1 day, 1 hour", 8 * 3600 + 3600), + ("6 hours, 30 minutes", 6 * 3600 + 1800), + ("30 minutes", 30 * 60), + ("1 day", 8 * 3600), + ("1 hour", 3600), + ("4 hours, 30 minutes", 4 * 3600 + 1800), + ("40 minutes", 40 * 60), + ("45 minutes", 45 * 60), + ("7 hours, 30 minutes", 7 * 3600 + 1800), + ("1 day, 30 minutes", 8 * 3600 + 1800), + ("5 hours", 5 * 3600), + ("5 hours, 30 minutes", 5 * 3600 + 1800), + ("1 minute", 60), + ("1 hour, 20 minutes", 3600 + 20 * 60), + ("6 hours", 6 * 3600), + ("3 hours, 30 minutes", 3 * 3600 + 1800), + ("15 minutes", 15 * 60), + ("3 hours", 3 * 3600), + ("1 week", 5 * 8 * 3600), + ( + "1 week, 2 days, 1 hour", + logged_human_time_to_seconds("5 day") + + logged_human_time_to_seconds("2 day") + + logged_human_time_to_seconds("1 hour") + ), + ] + for value, expected in items: + seconds = logged_human_time_to_seconds(value) + print(f"{value!r} -> {seconds} seconds") + assert expected == seconds + + logged_human = seconds_to_logged_human_time(seconds) + print(f"{seconds} seconds -> {logged_human!r}") + assert value == logged_human + + print() + + assert logged_human_time_to_seconds("1 day") == logged_human_time_to_seconds("8 hours") + assert logged_human_time_to_seconds("5 days") == logged_human_time_to_seconds("1 week") + + value = "1 day, 4 hours, 15 minutes" + assert seconds_to_logged_human_time(logged_human_time_to_seconds(value)) == value diff --git a/jira_time.py b/jira_time.py index 0d3a7c1c7..b83b4eb61 100644 --- a/jira_time.py +++ b/jira_time.py @@ -1,75 +1,81 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def parser_my_jira_time_logs(log): - """ Функция принимает список строк вида: - 7417 10:00-12:00 - 7417 12:19-14:00 - 7417 14:37-15:30 - 7417 15:58-17:50 +import re - 7415 15:58-15:59 +from collections import defaultdict +from datetime import datetime - 7456 14:28-15:59 - То, что перед ' ' -- уникальный номер задания - Диапазон после ' ' -- отрезок времени вида: начало - конец +def seconds_to_jira_str(seconds: int) -> str: + hours, minutes = divmod(seconds, 3600) + minutes //= 60 + days, hours = divmod(hours, 8) + weeks, days = divmod(days, 5) - Далее функция подсчитает количество часов и минут для каждого задания - и выведет их - """ - - # TODO: Защита от копипаста: строки могут повторяться и время подсчитается неправильно - # TODO: Если часы перевалят за 24, то начнется отсчет заного - # TODO: Для джиры дни и недели не астрономические: 1d = 8h и 1w = 5d + items = [ + f"{weeks}w" if weeks else "", + f"{days}d" if days else "", + f"{hours}h" if hours else "", + f"{minutes}m" if minutes else "", + ] + return " ".join(filter(None, items)) - import re - pattern = re.compile(r'(.+) (\d\d:\d\d)-(\d\d:\d\d)') - from datetime import datetime as dt - import time - from collections import defaultdict +def str_to_time(s: str) -> datetime: + return datetime.strptime(s, "%H:%M") - jira_time = defaultdict(int) - for line in log.split('\n'): - if line: - m = pattern.match(line.strip()) +def parser_my_jira_time_logs(log: str) -> str: + pattern = re.compile(r"(.+) (\d\d:\d\d)-(\d\d:\d\d)") - jira = m.group(1) - t1 = m.group(2) - t2 = m.group(3) - delta = dt.strptime(t2, '%H:%M') - dt.strptime(t1, '%H:%M') - seconds = delta.seconds + jira_by_seconds: dict[str, int] = defaultdict(int) - jira_time[jira] += seconds + for line in log.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue - for jira, secs in jira_time.items(): - t = time.gmtime(secs) - h = t.tm_hour - m = t.tm_min - jira_time = None - if h: - jira_time = str(h) + 'h' - if m: - if jira_time: - jira_time += ' ' + str(m) + 'm' - else: - jira_time = str(m) + 'm' + m = pattern.match(line) - print('%s: %s' % (jira, jira_time)) + jira: str = m.group(1) + t1: str = m.group(2) + t2: str = m.group(3) + seconds: int = (str_to_time(t2) - str_to_time(t1)).seconds + jira_by_seconds[jira] += seconds -parser_my_jira_time_logs(""" -7417 10:00-12:00 -7417 12:19-14:00 -7417 14:37-15:30 -7417 15:58-17:50 + return "\n".join( + f"{jira}: {seconds_to_jira_str(seconds)}" + for jira, seconds in jira_by_seconds.items() + ) -7415 15:58-15:59 -7456 14:28-15:59 -""") +if __name__ == "__main__": + print( + parser_my_jira_time_logs( + """ + # Foo + 7417 10:00-18:00 + + # Bar + 7417 10:00-12:00 + 7417 12:19-14:00 + 7417 14:37-15:30 + 7417 15:58-17:50 + + 7415 15:58-15:59 + 7456 14:28-15:59 + 7425 10:00-18:10 + """ + ) + ) + """ + 7417: 1d 6h 26m + 7415: 1m + 7456: 1h 31m + 77425: 1d 10m + """ diff --git a/job_compassplus/.deprecated/job_report/main.py b/job_compassplus/.deprecated/job_report/main.py new file mode 100644 index 000000000..832529c6e --- /dev/null +++ b/job_compassplus/.deprecated/job_report/main.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from itertools import chain +from utils import get_report_persons_info, get_person_info + + +report_dict = get_report_persons_info() + +# Вывести всех сотрудников, отсортировав их по количестве переработанных часов +person_list = set(chain(*report_dict.values())) + +# Проверка того, что сортировка работает (в принципе, думаю можно удалить) +assert sorted(person_list, key=lambda x: x.deviation_of_time) == sorted( + person_list, key=lambda x: x.deviation_of_time.total +) + +sorted_person_list = sorted( + person_list, key=lambda x: x.deviation_of_time, reverse=True +) + +for i, person in enumerate(sorted_person_list, 1): + print(f"{i:>3}. {person.full_name} {person.deviation_of_time}") + +print() +person = get_person_info( + second_name="Петраш", first_name="Илья", report_dict=report_dict +) +if person: + print( + f"#{sorted_person_list.index(person) + 1}. {person.full_name} {person.deviation_of_time}" + ) diff --git a/job_compassplus/.deprecated/job_report/report_person.py b/job_compassplus/.deprecated/job_report/report_person.py new file mode 100644 index 000000000..4d52fc0c9 --- /dev/null +++ b/job_compassplus/.deprecated/job_report/report_person.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from functools import total_ordering + + +class ReportPerson: + """Класс для описания сотрудника в отчете.""" + + def __init__(self, tags: list[str]): + # ФИО + self.second_name, self.first_name, self.middle_name = tags[0].split(maxsplit=2) + + # Невыходов на работу + self.absence_from_work = int(tags[1]) + + # По календарю (смен / ч:мин) + # Для точного значения посещенных дней, может быть указано как "3 = 4- (1 О)", поэтому + # отсекаем правую, от знака равно, сторону, удаляем пробелы и переводим в число + self.need_to_work_days = self.get_work_day(tags[2]) + self.need_to_work_on_time = self.get_work_time(tags[3]) + + # Фактически (смен / ч:мин) + self.worked_days = self.get_work_day(tags[4]) + self.worked_time = self.get_work_time(tags[5]) + + # Отклонение (смен / ч:мин) + self.deviation_of_day = self.get_work_day(tags[6]) + self.deviation_of_time = self.get_work_time(tags[7]) + + @property + def full_name(self) -> str: + return f"{self.second_name} {self.first_name} {self.middle_name}" + + @staticmethod + def get_work_day(day_str) -> int: + return ( + int(day_str) if "=" not in day_str else int(day_str.split("=")[0].strip()) + ) + + @total_ordering + class Time: + """Простой класс для хранения даты работы.""" + + def __init__(self, time_str: str): + self._hours, self._minutes, self._seconds = map(int, time_str.split(":")) + + @property + def total(self) -> int: + """Всего минут""" + + return self._hours * 60 + self._minutes + + def __repr__(self) -> str: + return f"{self._hours:0>2}:{self._minutes:0>2}" + + def __eq__(self, other: "Time") -> bool: + return self.total == other.total + + def __lt__(self, other: "Time") -> bool: + return self.total < other.total + + @staticmethod + def get_work_time(time_str: str) -> Time: + return ReportPerson.Time(time_str) + + def __hash__(self) -> int: + return hash(self.full_name) + + def __eq__(self, other: "ReportPerson") -> bool: + return self.full_name == other.full_name + + def __repr__(self) -> str: + return ( + f"{self.full_name}. Невыходов на работу: {self.absence_from_work}. " + f"По календарю ({self.need_to_work_days} смен / {self.need_to_work_on_time} ч:мин). " + f"Фактически ({self.worked_days} смен / {self.worked_time} ч:мин). " + f"Отклонение ({self.deviation_of_day} смен / {self.deviation_of_time} ч:мин)" + ) diff --git a/job_compassplus/.deprecated/job_report/utils.py b/job_compassplus/.deprecated/job_report/utils.py new file mode 100644 index 000000000..679acf7e0 --- /dev/null +++ b/job_compassplus/.deprecated/job_report/utils.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os.path +import sys + +from datetime import datetime +from collections import defaultdict +from itertools import chain +from pathlib import Path + + +DIR = Path(__file__).resolve().parent + +sys.path.append(str(DIR.parent)) +from current_job_report.utils import get_report_context + +from bs4 import BeautifulSoup + +from report_person import ReportPerson + + +LOGGING_DEBUG = False + + +if LOGGING_DEBUG: + import logging + + logging.basicConfig(level=logging.DEBUG) + + +def get_report_persons_info() -> dict[str, set[ReportPerson]]: + today = datetime.today().strftime("%Y-%m-%d") + report_file_name = f"report_{today}.html" + + # Если кэш-файл отчета не существует, загружаем новые данные и сохраняем в кэш-файл + if not os.path.exists(report_file_name): + if LOGGING_DEBUG: + print(f"{report_file_name} not exist") + + context = get_report_context() + + with open(report_file_name, mode="w", encoding="utf-8") as f: + f.write(context) + else: + if LOGGING_DEBUG: + print(f"{report_file_name} exist") + + with open(report_file_name, encoding="utf-8") as f: + context = f.read() + + html = BeautifulSoup(context, "lxml") + report = html.select("#report tbody tr") + + current_dep = None + report_dict = defaultdict(set) + + for row in report: + children = list(row.children) + if len(children) == 1 and children[0].name == "th": + current_dep = children[0].text.strip() + continue + + if children[0].has_attr("class") and children[0].attrs["class"][0] == "person": + person_tags = [children[0].text] + [ + i.text for i in row.nextSibling.select("td")[1:] + ] + if len(person_tags) != 8: + continue + + person = ReportPerson(person_tags) + report_dict[current_dep].add(person) + + return report_dict + + +def get_person_info(second_name, first_name=None, middle_name=None, report_dict=None): + if not report_dict: + report_dict = get_report_persons_info() + + # Вывести всех сотрудников, отсортировав их по количеству переработанных часов + for person in list(chain(*report_dict.values())): + found = person.second_name == second_name + + if first_name is not None: + found = found and person.first_name == first_name + + if middle_name is not None: + found = found and person.middle_name == middle_name + + if found: + return person + + +if __name__ == "__main__": + report_dict = get_report_persons_info() + print(len(report_dict)) + print(sum(map(len, report_dict.values()))) + print() + + for dep, persons in report_dict.items(): + print(f"{dep} ({len(persons)})") + for p in persons: + print(f" {p}") + + print() diff --git a/job_compassplus/.deprecated/statistics_by_names/print_statistic_all_names.py b/job_compassplus/.deprecated/statistics_by_names/print_statistic_all_names.py new file mode 100644 index 000000000..a069def45 --- /dev/null +++ b/job_compassplus/.deprecated/statistics_by_names/print_statistic_all_names.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +from pathlib import Path + +from bs4 import BeautifulSoup + +DIR = Path(__file__).resolve().parent +sys.path.append(str(DIR.parent)) +from current_job_report.utils import get_report_context + + +def get_all_names(split_name=False): + """ + split_name = False: ["<Фамилия> <Имя> <Отчество>", ...] + split_name = True: [["<Фамилия>", "<Имя>", "<Отчество>"], ...] + + """ + + text = get_report_context() + root = BeautifulSoup(text, "html.parser") + + # Имена описаны как "<Фамилия> <Имя> <Отчество>" + items = sorted( + {" ".join(report.text.split()) for report in root.select("#report .person")} + ) + + if split_name: + return [x.split(maxsplit=2) for x in items] + + return items + + +if __name__ == "__main__": + # Имена описаны как "<Фамилия> <Имя> <Отчество>" + name_list = get_all_names() + + total = len(name_list) + print("Total:", total) + + print_line_format = "{:%s}. {}" % len(str(total)) + + for i, name in enumerate(name_list, 1): + print(print_line_format.format(i, name)) diff --git a/job_compassplus/.deprecated/statistics_by_names/print_statistic_find_namesake.py b/job_compassplus/.deprecated/statistics_by_names/print_statistic_find_namesake.py new file mode 100644 index 000000000..d6c2ca880 --- /dev/null +++ b/job_compassplus/.deprecated/statistics_by_names/print_statistic_find_namesake.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +""" +Скрипт для поиска однофамильцев. + +""" + + +from collections import defaultdict + +# pip install pymorphy2 +import pymorphy2 + +from print_statistic_all_names import get_all_names + + +name_list = get_all_names() +total = len(name_list) +print("Total:", total) +print() + + +morph = pymorphy2.MorphAnalyzer() + + +def get_normal_form(word): + return morph.parse(word)[0].normal_form + + +second_name_by_full_name_list = defaultdict(list) + +for name in name_list: + second_name = name.split()[1] + normal_form = get_normal_form(second_name) + + second_name_by_full_name_list[normal_form].append(name) + +# Фильтр по однофамильцам +second_name_by_full_name_list = filter( + lambda x: len(x[1]) > 1, second_name_by_full_name_list.items() +) + +# Сортировка по количеству фамилий +second_name_by_full_name_list = sorted( + second_name_by_full_name_list, key=lambda x: len(x[1]), reverse=True +) + +# Вывод итоговых данных +for second_name, full_name_list in second_name_by_full_name_list: + print(f"{second_name.upper()} ({len(full_name_list)}): {sorted(full_name_list)}") diff --git a/job_compassplus/.deprecated/statistics_by_names/print_statistic_number_male_and_female.py b/job_compassplus/.deprecated/statistics_by_names/print_statistic_number_male_and_female.py new file mode 100644 index 000000000..f4785dd05 --- /dev/null +++ b/job_compassplus/.deprecated/statistics_by_names/print_statistic_number_male_and_female.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +""" +Скрипт для подсчета мужчин и женщин по именам. + +""" + + +# pip install pymorphy2 +import pymorphy2 + +from print_statistic_all_names import get_all_names + + +name_list = get_all_names(split_name=True) +total = len(name_list) +print("Total:", total) +print() + +morph = pymorphy2.MorphAnalyzer() + +masc_name_list = [] +femn_name_list = [] + +for _, first_name, _ in name_list: + parsed_word_list = morph.parse(first_name) + parsed_word = None + + if len(parsed_word_list) > 1: + parsed_word_list_filtered = list( + filter( + lambda x: x.normal_form.lower() == first_name.lower(), parsed_word_list + ) + ) + if not parsed_word_list_filtered: + # Алена -> Алёна + parsed_word_list_filtered = list( + filter( + lambda x: x.normal_form.lower() + == first_name.lower().replace("е", "ё"), + parsed_word_list, + ) + ) + if not parsed_word_list_filtered: + print("Error by parsing name:", first_name.upper(), parsed_word_list) + continue + + parsed_word = parsed_word_list_filtered[0] + else: + parsed_word = parsed_word_list_filtered[0] + else: + parsed_word = parsed_word_list[0] + + gender = parsed_word.tag.gender + + if gender == "masc": + masc_name_list.append(first_name) + else: + femn_name_list.append(first_name) + +# Сортировка списка имен +masc_name_list.sort() +femn_name_list.sort() + +print(f"Masc ({len(masc_name_list)}): {masc_name_list}") +print(f"Femn ({len(femn_name_list)}): {femn_name_list}") diff --git a/job_compassplus/statistics_by_names/print_statistic_top_first_name.py b/job_compassplus/.deprecated/statistics_by_names/print_statistic_top_first_name.py similarity index 89% rename from job_compassplus/statistics_by_names/print_statistic_top_first_name.py rename to job_compassplus/.deprecated/statistics_by_names/print_statistic_top_first_name.py index bb4c8c6b4..d11f22212 100644 --- a/job_compassplus/statistics_by_names/print_statistic_top_first_name.py +++ b/job_compassplus/.deprecated/statistics_by_names/print_statistic_top_first_name.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -35,12 +35,12 @@ first_name_list = [name[1] for name in get_all_names(split_name=True)] total = len(first_name_list) -print('Total:', total) +print("Total:", total) print() -print('Top 15:') +print("Top 15:") counter = Counter(first_name_list) # Сортировка по количеству for name, number in sorted(counter.items(), key=lambda x: x[1], reverse=True)[:15]: - print(f' {name}: {number} ({number * 100 / total:.1f}%)') + print(f" {name}: {number} ({number * 100 / total:.1f}%)") diff --git a/job_compassplus/.deprecated/statistics_by_names/print_statistic_unique_first_name.py b/job_compassplus/.deprecated/statistics_by_names/print_statistic_unique_first_name.py new file mode 100644 index 000000000..2f84c9a99 --- /dev/null +++ b/job_compassplus/.deprecated/statistics_by_names/print_statistic_unique_first_name.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +""" +Скрипт для вывода имен без повторений. + +""" + + +from print_statistic_all_names import get_all_names + + +first_name_list = [name[1] for name in get_all_names(split_name=True)] +print("Total:", len(first_name_list)) + +unique_first_name_list = list(set(first_name_list)) +print(f"Total unique ({len(unique_first_name_list)}): {unique_first_name_list}") diff --git a/UpdateTxEnums/README.md b/job_compassplus/UpdateTxEnums/README.md similarity index 100% rename from UpdateTxEnums/README.md rename to job_compassplus/UpdateTxEnums/README.md diff --git a/UpdateTxEnums/UpdateTxEnums.py b/job_compassplus/UpdateTxEnums/UpdateTxEnums.py similarity index 99% rename from UpdateTxEnums/UpdateTxEnums.py rename to job_compassplus/UpdateTxEnums/UpdateTxEnums.py index 34e444277..014b5c416 100644 --- a/UpdateTxEnums/UpdateTxEnums.py +++ b/job_compassplus/UpdateTxEnums/UpdateTxEnums.py @@ -2,7 +2,7 @@ # TXSST-148: # Нужно написать скрипт, который будет автоматически генерировать классы -# перечеслений на C# в соответствии с этими перечислениями в транзаксисе. +# перечислений на C# в соответствии с этими перечислениями в транзаксисе. # Особенно актуально для CustInfoKind, т.к. оно периодически обновляется # и его можно задавать при настройке объектов сценария. @@ -43,7 +43,7 @@ ) ## Комментарии констант перечислений. -# Ключем является id перечисления, а значением словарь: ключ - имя константы, значение - комментарий константы. +# Ключом является id перечисления, а значением словарь: ключ - имя константы, значение - комментарий константы. dict_enum_constants_comments = { # # AppContextType # 'acsIFBAJ3UCLJCJ7GPUOYTFLPG6SA' : { @@ -727,7 +727,7 @@ def rowsToCSharpXmlComment(rows): # @param rows list[unicode] Список строк текста, который нужно "обернуть" в документирущий комментарий C# # @return unicode Текст в комментарии , строки которого разделены '\n' - return u'/// \n%s/// ' % ''.join([u'/// {}\n'.format(row) + '' for row in rows]) + return u'/// \n%s/// ' % ''.join(f'/// {row}\n' + '' for row in rows) def getSortedEnumItems(enum_items): @@ -761,7 +761,7 @@ def addCommentBlockAboutTxEnum(enum): # @param enum TxEnum Перечисление, по которому нужно сгенерировать дополнительную информацию. # @return str Текст с комментарием c#, содержащий информацию об перечислении TX. - return "/// \n" % (enum.getId(), enum.getStrValType()) + return f"/// \n" def addStandartUsingDirective(): diff --git a/job_compassplus/copying_jar_from_radix/main.py b/job_compassplus/copying_jar_from_radix/main.py new file mode 100644 index 000000000..5f30b83cc --- /dev/null +++ b/job_compassplus/copying_jar_from_radix/main.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import shutil +import sys +from pathlib import Path + +sys.path.append("..") +from tx_parse_xml.get_layer_version import get_layer_version + + +# Example: org.radixware\kernel\common\bin +KERNEL_COMMON_BIN_JARS: list[str] = [ + "xb_acs.jar", + "xb_adsdef.jar", + "xb_commondef.jar", + "xb_utils.jar", + "general.jar", + "xb_msdl.jar", + "xb_types.jar", +] + +# Example: org.radixware\kernel\common\lib +KERNEL_COMMON_LIB_JARS: list[str] = [ + "log4j-api-2.17.1.jar", + "log4j-core-2.17.1.jar", + "saxon-HE-10.3.jar", + "xbean.jar", + "xbean_xpath.jar", + "xercesImpl-2.9.1.jar", +] + +OTHER_PATHS: dict[str, str] = { + "ads/ServiceBus/bin/common.jar": "ServiceBus-common.jar", +} + + +def process(root_dir: Path): + path_radix = root_dir / "org.radixware" + if not path_radix.exists(): + raise Exception(f'Not found path {str(path_radix)!r}!') + + radix_version = get_layer_version(path_radix) + print("Radix version:", radix_version) + + # NOTE: Текущая папка скрипта + path_to_copy = Path(__file__).resolve().parent / f"radix-{radix_version}" + + path_kernel_common_bin = path_radix / "kernel/common/bin" + if not path_kernel_common_bin.exists(): + raise Exception(f'Not found path {str(path_kernel_common_bin)!r}!') + + path_kernel_common_lib = path_radix / "kernel/common/lib" + if not path_kernel_common_lib.exists(): + raise Exception(f'Not found path {str(path_kernel_common_lib)!r}!') + + old_to_new_paths: dict[Path, Path] = dict() + + for file in KERNEL_COMMON_BIN_JARS: + old_path = path_kernel_common_bin / file + old_to_new_paths[old_path] = path_to_copy / old_path.name + + for file in KERNEL_COMMON_LIB_JARS: + old_path = path_kernel_common_lib / file + old_to_new_paths[old_path] = path_to_copy / old_path.name + + for file, new_file in OTHER_PATHS.items(): + old_path = path_radix / file + old_to_new_paths[old_path] = path_to_copy / new_file + + # Проверка наличия всех путей + for old_path in old_to_new_paths.keys(): + if not old_path.exists(): + raise Exception(f'Not found path {str(old_path)!r}!') + + path_to_copy.mkdir(exist_ok=True, parents=True) + for old_path, new_path in old_to_new_paths.items(): + print(f"Copy {str(old_path)!r} to {str(new_path)!r}") + shutil.copy(old_path, new_path) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Copying jar from radix" + ) + parser.add_argument( + "root_path", + metavar="/PATH/TO/PROJECT", + type=Path, + help="Path to source tree (the directory containing branch.xml)", + ) + args = parser.parse_args() + + process(args.root_path) diff --git a/job_compassplus/current_job_report/.deprecated/get_latecomers.py b/job_compassplus/current_job_report/.deprecated/get_latecomers.py new file mode 100644 index 000000000..88f30d32a --- /dev/null +++ b/job_compassplus/current_job_report/.deprecated/get_latecomers.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from collections import defaultdict +from datetime import date, datetime +from typing import Callable + +from bs4 import BeautifulSoup, Tag +from utils import NotFoundReport, get_report_context + + +def get_latecomers( + dep: str = "dep12", + period_date: date = None, + is_latecome: Callable[[datetime], bool] = lambda day: day.hour >= 11, +) -> dict[str, list[datetime]]: + html = get_report_context( + dep=dep, + rep="rep2", # Детальный отчет + period=period_date.strftime("%Y-%m"), + ) + + root = BeautifulSoup(html, "html.parser") + + person_by_dates: dict[str, list[datetime]] = defaultdict(list) + + report_table = root.select_one("table#report") + if not report_table: # Отчет еще не готов + raise NotFoundReport() + + dates: list[str | None] = [] + for el in report_table.select("thead > tr:nth-child(2) > th"): + # NOTE: "17.08" -> (17, 08) + day, month = map(int, el.get_text(strip=True).split(".")) + + d = date(year=period_date.year, month=month, day=day) + dates.append(d.isoformat() if d.isoweekday() not in (6, 7) else None) + + tr_list = report_table.select("tr:has(td.person)") + assert tr_list, "Другой формат таблицы. Не нашлось строк с пользователями" + + for tr in tr_list: + tds: list[Tag] = tr.select("td") + if len(tds) < 3: + continue + + # NOTE: Содержит ячейки с временем входа, типа "08:41:18" + td_person, td_total, td_enter, *td_enter_times = tds + assert ( + td_enter.get_text(strip=True) == "Вход" + ), "Другой формат таблицы. Не строка с временем входа" + assert len(td_enter_times) == len( + dates + ), "Другой формат таблицы. Количество столбцов в заголовке и в строке не совпадает" + + # NOTE: Содержит ячейки с временем нахождения, типа "08:41:18" или отсутствия: + # "О\n-" - отпуск, "Т\n07:00:00" - временный отпуск и т.п. Время есть, если в офис входили в этот день + # (исключение для Т) + td_time, *td_total_day_times = ( + tr.find_next_sibling("tr").find_next_sibling("tr").select("td") + ) + assert ( + td_time.get_text(strip=True) == "Время" + ), "Другой формат таблицы. Не строка с временем нахождения" + assert len(td_total_day_times) == len( + dates + ), "Другой формат таблицы. Количество столбцов в заголовке и в строке не совпадает" + + person: str = td_person.get_text(strip=True) + + # Применительно для текущего пользователя + if person in person_by_dates: + continue + + enter_times: list[str | None] = [ + el if re.search(r"\d{2}:\d{2}:\d{2}", el) else None + for el in map(lambda el: el.get_text(strip=True), td_enter_times) + ] + total_day_times: list[str | None] = [ + # Проверка, что в строке есть только цифры и двоеточие + None if re.sub(r"[\d:\s]", "", el) else el + for el in map(lambda el: el.get_text(strip=True), td_total_day_times) + ] + + for date_str, enter_time_str, total_day_time_str in zip( + dates, enter_times, total_day_times + ): + if not date_str or not enter_time_str or not total_day_time_str: + continue + + day: datetime = datetime.fromisoformat(f"{date_str} {enter_time_str}") + if is_latecome(day): + person_by_dates[person].append(day) + + return person_by_dates + + +if __name__ == "__main__": + DEP: str = "dep12" + + period_date: date = date.today() + # NOTE: За конкретную дату + # period_date: date = date(year=2024, month=9, day=1) + print(f"Отчет за {period_date:%Y-%m}") + + try: + person_by_dates: dict[str, list[datetime]] = get_latecomers( + dep=DEP, period_date=period_date + ) + print(f"Опоздавшие ({len(person_by_dates)}):") + + for person, days in person_by_dates.items(): + print(f" {person} ({len(days)}):") + for day in days: + print(f" {day}") + + print() + + except NotFoundReport: + print("Не готов") + + # NOTE: За период + # print() + # + # person_by_dates: dict[str, list[datetime]] = defaultdict(list) + # for month in [7, 8, 9]: + # try: + # result = get_latecomers( + # dep=DEP, period_date=date(year=2024, month=month, day=1) + # ) + # except NotFoundReport: + # continue + # + # for person, days in result.items(): + # person_by_dates[person] += days + # + # print(f"Опоздавшие ({len(person_by_dates)}):") + # for person, days in person_by_dates.items(): + # print(f" {person} ({len(days)}):") + # for day in days: + # print(f" {day}") + # + # print() diff --git a/job_compassplus/current_job_report/favicon.ico b/job_compassplus/current_job_report/favicon.ico deleted file mode 100644 index d0a42183a..000000000 Binary files a/job_compassplus/current_job_report/favicon.ico and /dev/null differ diff --git a/job_compassplus/current_job_report/favicon.png b/job_compassplus/current_job_report/favicon.png new file mode 100644 index 000000000..7fbeb22cf Binary files /dev/null and b/job_compassplus/current_job_report/favicon.png differ diff --git a/job_compassplus/current_job_report/get_hours_worked.py b/job_compassplus/current_job_report/get_hours_worked.py new file mode 100644 index 000000000..89c05ffad --- /dev/null +++ b/job_compassplus/current_job_report/get_hours_worked.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from bs4 import BeautifulSoup, Tag +from utils import ( + NotFoundReport, + ReportTypeEnum, + PeriodTypeEnum, + get_report, + clear_hours, +) + + +def get_text_children(el: Tag, idx: int) -> str: + try: + return el.find_all(recursive=False)[idx].text.strip() + except Exception: + return "" + + +def get_tr_for_current_user(html: str) -> Tag: + root = BeautifulSoup(html, "html.parser") + + tbody_el = root.select_one("#report > tbody") + if not tbody_el: + tbody_el = root.select_one(".report > tbody") + if not tbody_el: + raise NotFoundReport() + + pattern_current_user: re.Pattern = re.compile(".*Текущий пользователь.*") + + th_el = tbody_el.find("th", string=pattern_current_user) + if not th_el: + raise NotFoundReport() + + return th_el.parent + + +def parse_current_user_deviation_hours(html: str) -> tuple[str, str]: + current_user_tr: Tag = get_tr_for_current_user(html) + + # Получение следующего элемента после текущего, у него получение первого ребенка, у которого вытаскивается текст + tr_name: Tag = current_user_tr.find_next_sibling() + name: str = get_text_children(tr_name, idx=0) + + # NOTE: В новом отчете в первой строке указано отклонение + deviation_hours: str = get_text_children(tr_name, idx=7) + if not deviation_hours: # Если старый отчет, то в самом низу + # Получение следующего элемента после текущего, у него получение последнего + # ребенка, у которого вытаскивается текст + # Ищем последнюю строку текущего пользователя -- в ней и находится время работы + # Ее легко найти -- ее первая ячейка пустая + deviation_tr = current_user_tr.find_next_sibling() + + # Ищем строку с пустой ячейкой + while get_text_children(deviation_tr, idx=0): + deviation_tr = deviation_tr.find_next_sibling() + + deviation_hours: str = get_text_children(deviation_tr, idx=-1) + + return name, clear_hours(deviation_hours) + + +def get_user_and_deviation_hours() -> tuple[str, str]: + report: str = get_report( + report_type=ReportTypeEnum.SUMMARY, + period_type=PeriodTypeEnum.MONTH, + ) + return parse_current_user_deviation_hours(report) + + +def get_quarter_user_and_deviation_hours() -> tuple[str, str]: + report: str = get_report( + report_type=ReportTypeEnum.SUMMARY, + period_type=PeriodTypeEnum.QUARTER, + ) + return parse_current_user_deviation_hours(report) + + +if __name__ == "__main__": + name, deviation_hours = get_user_and_deviation_hours() + print(name) + print( + ("Недоработка" if deviation_hours[0] == "-" else "Переработка") + + " " + + deviation_hours + ) + """ + Петраш Илья Андреевич + Переработка 04:19:57 + """ + + print() + + name, deviation_hours = get_quarter_user_and_deviation_hours() + print(name) + print( + ("Недоработка" if deviation_hours[0] == "-" else "Переработка") + + " за квартал " + + deviation_hours + ) + """ + Петраш Илья Андреевич + Переработка за квартал 15:09 + """ + + print() + + from get_worklog import get_worklog + + print(get_worklog()) + """ + Worklog(actually='103:05:14', logged='66:35', logged_percent=65) + """ diff --git a/job_compassplus/current_job_report/get_time_spent_in_office.py b/job_compassplus/current_job_report/get_time_spent_in_office.py new file mode 100644 index 000000000..b344c7a91 --- /dev/null +++ b/job_compassplus/current_job_report/get_time_spent_in_office.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from dataclasses import dataclass + +from bs4 import BeautifulSoup + +from utils import session, HOST, NotFoundReport + + +URL = f"{HOST}/pa-reports-new/report/" + + +@dataclass +class TimeSpent: + first_enter: str + today: str + + +def get_time_spent_in_office() -> TimeSpent: + rs = session.get(URL) + if not rs.ok: + raise NotFoundReport(f"HTTP status is {rs.status_code}") + + soup = BeautifulSoup(rs.content, "html.parser") + text = soup.get_text(strip=True) + + def _find(pattern: str, about: str) -> str: + if m := re.search(pattern, text, flags=re.IGNORECASE): + return m.group(1) + raise Exception(f"Not found {about!r}") + + return TimeSpent( + first_enter=_find( + pattern=r"First enter:\s*([\d+:]+)", + about="First enter", + ), + today=_find( + pattern=r"Today\s*\(Possible\):\s*([\d+:]+)", + about="Today(Possible)", + ), + ) + + +if __name__ == "__main__": + print(get_time_spent_in_office()) + """ + TimeSpent(first_enter='10:53:30', today='07:31:36') + """ diff --git a/job_compassplus/current_job_report/get_user_and_deviation_hours.py b/job_compassplus/current_job_report/get_user_and_deviation_hours.py deleted file mode 100644 index 0cf1ece45..000000000 --- a/job_compassplus/current_job_report/get_user_and_deviation_hours.py +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -__author__ = 'ipetrash' - - -import datetime as DT -import re -import sys - -from pathlib import Path -from typing import Tuple - -from lxml import etree - - -DIR = Path(__file__).resolve().parent - -sys.path.append(str(DIR.parent)) -from root_common import session - -sys.path.append(str(DIR.parent.parent)) -from get_quarter import get_quarter, get_quarter_num # NOTE: оставить get_quarter_num для main.py - - -class NotFoundReport(Exception): - pass - - -URL = 'https://jira.compassplus.ru/pa-reports/' - - -def clear_hours(hours: str) -> str: - return re.sub(r'[^\d:-]', '', hours) - - -def _send_data(data: dict) -> str: - # В какой-то момент адрес временно поменялся, тогда предварительный GET поможет получить актуальный адрес - rs = session.get(URL) - if not rs.ok: - raise NotFoundReport(f"HTTP status is {rs.status_code}") - - rs = session.post(rs.url, data=data) - if not rs.ok: - raise NotFoundReport(f"HTTP status is {rs.status_code}") - - return rs.text - - -def get_report_context() -> str: - today = DT.datetime.today() - data = { - 'dep': 'all', - 'rep': 'rep1', - 'period': today.strftime('%Y-%m'), - 'v': int(today.timestamp() * 1000), - 'type': 'normal', - } - return _send_data(data) - - -def get_quarter_report_context() -> str: - today = DT.datetime.today() - data = { - 'dep': 'all', - 'rep': 'rep1', - 'quarter': 'quarter', - 'period': f'{today.year}-q{get_quarter(today)}', - 'v': int(today.timestamp() * 1000), - 'type': 'normal', - } - return _send_data(data) - - -def parse_current_user_deviation_hours(html: str) -> Tuple[str, str]: - root = etree.HTML(html) - - XPATH_1 = '//table[@id="report"]/tbody/tr[th[contains(text(),"Текущий пользователь")]]' - XPATH_2 = '//table[@class="report"]/tbody/tr[th[contains(text(),"Текущий пользователь")]]' - - # Вытаскивание tr, у которого есть вложенный th, имеющий в содержимом текст "Текущий пользователь" - try: - items = root.xpath(XPATH_1) - if not items: - items = root.xpath(XPATH_2) - - current_user_tr = items[0] - - except IndexError: - raise NotFoundReport() - - # Получение следующего элемента после текущего, у него получение первого ребенка, у которого вытаскивается текст - name = next(current_user_tr.getnext().iterchildren()).text.strip() - - # Получение следующего следующего элемента после текущего, у него получение последнего - # ребенка, у которого вытаскивается текст - # Ищем последную строку текущего пользователя -- в ней и находится время работы - # Ее легко найти -- ее первая ячейка пустая - deviation_tr = current_user_tr.getnext() - - # Ищем строку с пустой ячейкой - while next(deviation_tr.iterchildren()).text.strip(): - deviation_tr = deviation_tr.getnext() - - deviation_hours = deviation_tr.getchildren()[-1].text.strip() - return name, clear_hours(deviation_hours) - - -def parse_user_deviation_hours(html: str, user_name: str = 'Петраш') -> Tuple[str, str]: - root = etree.HTML(html) - - XPATH_1 = f'//table[@id="report"]/tbody/tr[td[contains(text(),"{user_name}")]]' - XPATH_2 = f'//table[@class="report"]/tbody/tr[td[contains(text(),"{user_name}")]]' - - # Вытаскивание tr, у которого есть вложенный th, имеющий в содержимом текст "Текущий пользователь" - try: - items = root.xpath(XPATH_1) - if not items: - items = root.xpath(XPATH_2) - - current_user_tr = items[0] - - except IndexError: - raise NotFoundReport() - - # Получение текста текущего элемента - name = next(current_user_tr.iterchildren()).text.strip() - - # Получение следующего следующего элемента после текущего, у него получение последнего - # ребенка, у которого вытаскивается текст - # Ищем последную строку текущего пользователя -- в ней и находится время работы - # Ее легко найти -- ее первая ячейка пустая - deviation_tr = current_user_tr.getnext() - - # Ищем строку с пустой ячейкой - while next(deviation_tr.iterchildren()).text.strip(): - deviation_tr = deviation_tr.getnext() - - deviation_hours = deviation_tr.getchildren()[-1].text.strip() - return name, clear_hours(deviation_hours) - - -def get_user_and_deviation_hours() -> Tuple[str, str]: - content = get_report_context() - return parse_current_user_deviation_hours(content) - - -def get_quarter_user_and_deviation_hours() -> Tuple[str, str]: - content = get_quarter_report_context() - return parse_user_deviation_hours(content) - - -if __name__ == '__main__': - name, deviation_hours = get_user_and_deviation_hours() - print(name) - print(('Недоработка' if deviation_hours[0] == '-' else 'Переработка') + ' ' + deviation_hours) - print() - - name, deviation_hours = get_quarter_user_and_deviation_hours() - print(name) - print(('Недоработка' if deviation_hours[0] == '-' else 'Переработка') + ' за квартал ' + deviation_hours) diff --git a/job_compassplus/current_job_report/get_worklog.py b/job_compassplus/current_job_report/get_worklog.py new file mode 100644 index 000000000..5a80ddd21 --- /dev/null +++ b/job_compassplus/current_job_report/get_worklog.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass +from bs4 import BeautifulSoup +from utils import get_report, ReportTypeEnum, PeriodTypeEnum, NotFoundReport + + +@dataclass +class Worklog: + actually: str # Отработано фактически (чч:мм:сс) + logged: str # Зафиксировано трудозатрат (чч:мм) + logged_percent: int # Процент зафиксированного времени + + @classmethod + def parse_from(cls, data: tuple[str, str, str]) -> "Worklog": + try: + logged_percent: int = int(data[2].replace("%", "")) + except Exception: + raise NotFoundReport() + + return cls( + actually=data[0], + logged=data[1], + logged_percent=logged_percent, + ) + + +def get_worklog() -> Worklog: + report: str = get_report( + report_type=ReportTypeEnum.WORKLOG, + period_type=PeriodTypeEnum.MONTH, + ) + + soup = BeautifulSoup(report, "html.parser") + current_user_tr = soup.select_one("table > tbody > tr.current") + if not current_user_tr: + raise NotFoundReport() + + td_list = current_user_tr.select("td") + return Worklog.parse_from( + ( + # Отработано фактически (чч:мм:сс) + td_list[1].get_text(strip=True), + # Зафиксировано трудозатрат (чч:мм) + td_list[-2].get_text(strip=True), + # Процент зафиксированного времени + td_list[-1].get_text(strip=True), + ) + ) + + +if __name__ == "__main__": + print(get_worklog()) + """ + Worklog(actually='103:05:14', logged='66:35', logged_percent=65) + """ diff --git a/job_compassplus/current_job_report/main.py b/job_compassplus/current_job_report/main.py index 036ca8566..eccf2e245 100644 --- a/job_compassplus/current_job_report/main.py +++ b/job_compassplus/current_job_report/main.py @@ -1,41 +1,83 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt import sys import time import traceback -import os.path + +from pathlib import Path +from threading import Thread from PyQt5.QtWidgets import ( - QApplication, QWidget, QLabel, QToolButton, QPlainTextEdit, QVBoxLayout, QHBoxLayout, QSystemTrayIcon, - QMenu, QWidgetAction, QMessageBox + QApplication, + QWidget, + QLabel, + QToolButton, + QPlainTextEdit, + QVBoxLayout, + QHBoxLayout, + QSystemTrayIcon, + QMenu, + QWidgetAction, + QMessageBox, ) -from PyQt5.QtGui import QColor, QPainter, QIcon -from PyQt5.QtCore import Qt, pyqtSignal, QThread +from PyQt5.QtGui import QColor, QPainter, QIcon, QPixmap, QCursor +from PyQt5.QtCore import Qt, pyqtSignal, QThread, QRectF -from get_user_and_deviation_hours import ( - get_user_and_deviation_hours, get_quarter_user_and_deviation_hours, get_quarter_num, - NotFoundReport +from get_hours_worked import ( + get_user_and_deviation_hours, + get_quarter_user_and_deviation_hours, ) +from utils import NotFoundReport, get_quarter_roman + + +# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/405f08fcbf8b99ea64a58a73ee699cb1c0b5230e/qt__pyqt__pyside__pyqode/pyqt__QPainter__dynamic_draw_emoji_on_img/main.py#L44-L66 +def draw_text_to_bottom_right( + img: QPixmap, text: str, scale_text_from_img: float = 0.5 +): + p = QPainter(img) + + factor = (img.width() * scale_text_from_img) / p.fontMetrics().width(text) + if factor < 1 or factor > 1.25: + f = p.font() + point_size = f.pointSizeF() * factor + if point_size > 0: + f.setPointSizeF(point_size) + p.setFont(f) + + # Bottom + right + text_rect = p.fontMetrics().boundingRect(text) + rect = QRectF( + img.width() - text_rect.width(), + img.height() - text_rect.height(), + img.width(), + img.height(), + ) + + p.drawText(rect, text) + + p = None # NOTE: Иначе, почему-то будет ошибка # Для отлова всех исключений, которые в слотах Qt могут "затеряться" и привести к тихому падению -def log_uncaught_exceptions(ex_cls, ex, tb): - text = f'{ex_cls.__name__}: {ex}:\n' - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) - print('Error: ', text) - QMessageBox.critical(None, 'Error', text) + print("Error: ", text) + QMessageBox.critical(None, "Error", text) sys.exit(1) sys.excepthook = log_uncaught_exceptions -TRAY_ICON = os.path.join(os.path.dirname(__file__), 'favicon.ico') + +DIR: Path = Path(__file__).parent.resolve() +TRAY_ICON: str = str(DIR / "favicon.png") class CheckJobReportThread(QThread): @@ -43,19 +85,19 @@ class CheckJobReportThread(QThread): about_ok = pyqtSignal(bool) about_log = pyqtSignal(str) - def __init__(self): + def __init__(self) -> None: super().__init__() self.last_text = None self.ok = None - def do_run(self): + def do_run(self) -> None: def _get_title(deviation_hours): - ok = deviation_hours[0] != '-' - return 'Переработка' if ok else 'Недоработка' + ok = deviation_hours[0] != "-" + return "Переработка" if ok else "Недоработка" - today = DT.datetime.today().strftime('%d/%m/%Y %H:%M:%S') - self.about_log.emit(f'Check for {today}') + today = dt.datetime.today().strftime("%d.%m.%Y %H:%M:%S") + self.about_log.emit(f"Проверка за {today}") text = "" deviation_hours = None @@ -63,23 +105,18 @@ def _get_title(deviation_hours): try: name, deviation_hours = get_user_and_deviation_hours() - ok = deviation_hours[0] != '-' - text += name + '\n\n' + _get_title(deviation_hours) + ' ' + deviation_hours - - except NotFoundReport: - text = "Отчет на сегодня еще не готов." - ok = True + ok = deviation_hours[0] != "-" + text += name + "\n\n" + _get_title(deviation_hours) + " " + deviation_hours - try: _, quarter_deviation_hours = get_quarter_user_and_deviation_hours() - if quarter_deviation_hours.count(':') == 1: + if quarter_deviation_hours.count(":") == 1: quarter_deviation_hours += ":00" - text += "\n" + _get_title(quarter_deviation_hours) + ' за квартал ' + get_quarter_num() \ - + " " + quarter_deviation_hours + text += f"\n{_get_title(quarter_deviation_hours)} за квартал {get_quarter_roman()} {quarter_deviation_hours}" except NotFoundReport: - pass + text = "Отчет на сегодня еще не готов." + ok = True # Если часы за месяц не готовы, но часы за квартал есть if not deviation_hours and quarter_deviation_hours: @@ -94,36 +131,40 @@ def _get_title(deviation_hours): else: self.about_log.emit(" Ничего не изменилось\n") - if self.ok != ok: - self.ok = ok - self.about_ok.emit(self.ok) + self.ok = ok + self.about_ok.emit(self.ok) - def run(self): + def run(self) -> None: while True: try: - self.do_run() + # Между 08:00 и 20:00 + now_hour = dt.datetime.now().hour + if now_hour in range(8, 20 + 1): + self.do_run() time.sleep(3600) except Exception as e: - self.about_log.emit("Error: " + str(e)) + self.about_log.emit(f"Error: {e}") self.about_log.emit("Wait 60 secs") time.sleep(60) class JobReportWidget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.info = QLabel() - self.ok = None + self.info.setWordWrap(True) + + self.ok: bool | None = None self.quit_button = QToolButton() - self.quit_button.setText('Quit') + self.quit_button.setText("Quit") self.quit_button.setAutoRaise(True) self.quit_button.clicked.connect(QApplication.instance().quit) self.hide_button = QToolButton() - self.hide_button.setText('Hide') + self.hide_button.setText("Hide") self.hide_button.setAutoRaise(True) self.hide_button.clicked.connect(lambda x=None: self.parent().hide()) @@ -164,25 +205,36 @@ def __init__(self): self.setLayout(layout) self.thread = CheckJobReportThread() - self.thread.about_new_text.connect(self.info.setText) + self.thread.about_new_text.connect(self.set_text) self.thread.about_ok.connect(self._set_ok) self.thread.about_log.connect(self._add_log) self.thread.start() - button_refresh.clicked.connect(self.thread.do_run) + button_refresh.clicked.connect(self.refresh) + + def set_text(self, text: str) -> None: + print(text) + self.info.setText(text) + + def refresh(self) -> None: + # Выполнение метода в отдельном потоке, а не в GUI + Thread(target=self.thread.do_run, daemon=True).start() - def _set_ok(self, val): + def _set_ok(self, val: bool) -> None: self.ok = val self.update() - def _add_log(self, val): + def _add_log(self, val: str) -> None: print(val) self.log.appendPlainText(val) - def paintEvent(self, event): + def paintEvent(self, event) -> None: super().paintEvent(event) - color = QColor('#29AB87') if self.ok else QColor(255, 0, 0, 128) + if self.ok is None: + return + + color = QColor("#29AB87") if self.ok else QColor(255, 0, 0, 128) painter = QPainter(self) painter.setBrush(color) @@ -190,15 +242,27 @@ def paintEvent(self, event): painter.drawRect(self.rect()) -# TODO: Нарисовать график -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) app.setQuitOnLastWindowClosed(False) tray = QSystemTrayIcon(QIcon(TRAY_ICON)) + def _on_about_log_or_ok(value: str | bool) -> None: + if isinstance(value, str): + text = "🔄" + else: + text = "✅" if value else "⛔" + + img = QPixmap(TRAY_ICON) + draw_text_to_bottom_right(img, text, 0.65) + tray.setIcon(QIcon(img)) + job_report_widget = JobReportWidget() - job_report_widget.setFixedSize(230, 130) + job_report_widget.thread.about_log.connect(_on_about_log_or_ok) + job_report_widget.thread.about_ok.connect(_on_about_log_or_ok) + job_report_widget.refresh() + job_report_widget_action = QWidgetAction(job_report_widget) job_report_widget_action.setDefaultWidget(job_report_widget) @@ -206,9 +270,14 @@ def paintEvent(self, event): menu.addAction(job_report_widget_action) tray.setContextMenu(menu) - tray.activated.connect(lambda x: menu.exec(tray.geometry().center())) - - tray.setToolTip('Compass Plus. Рапорт учета рабочего времени') + tray.activated.connect( + lambda _: ( + tray.contextMenu().resize(job_report_widget.sizeHint()), + tray.contextMenu().popup(QCursor.pos()), + ) + ) + + tray.setToolTip("Compass Plus. Рапорт учета рабочего времени") tray.show() app.exec() diff --git a/job_compassplus/current_job_report/utils.py b/job_compassplus/current_job_report/utils.py new file mode 100644 index 000000000..ede069598 --- /dev/null +++ b/job_compassplus/current_job_report/utils.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum +import re +import sys + +from datetime import datetime +from pathlib import Path + +from bs4 import BeautifulSoup + +DIR = Path(__file__).resolve().parent +sys.path.append(str(DIR.parent)) +from root_config import JIRA_HOST as HOST +from root_common import session + +sys.path.append(str(DIR.parent.parent)) +from get_quarter import ( + get_quarter_num, + get_quarter_roman, # NOTE: оставить get_quarter_roman для main.py +) + + +@enum.unique +class PeriodTypeEnum(enum.IntEnum): + MONTH = 0 + QUARTER = 1 + PERIOD = 2 + + +@enum.unique +class ReportTypeEnum(enum.IntEnum): + WORKLOG = 5 # Отчет по зафиксированным трудозатратам + SUMMARY = 6 # Сводный отчет + + +class NotFoundReport(Exception): + pass + + +URL: str = f"{HOST}/pa-reports-new/report/" + + +def clear_hours(hours: str) -> str: + return re.sub(r"[^\d:-]", "", hours) + + +def _send_data(data: dict[str, str | int]) -> str: + # В какой-то момент адрес временно поменялся, тогда предварительный GET поможет получить актуальный адрес + rs = session.get(URL) + if not rs.ok: + raise NotFoundReport(f"HTTP status is {rs.status_code}") + + # Добавление полей, типа токенов, если явно не были заданы + soup = BeautifulSoup(rs.content, "html.parser") + for el in soup.select('input[name][value][type="hidden"]'): + name: str = el["name"] + if name not in data: + data[name] = el["value"] + + rs = session.post(rs.url, data=data) + if not rs.ok: + raise NotFoundReport(f"HTTP status is {rs.status_code}") + + return rs.text + + +def get_report(report_type: ReportTypeEnum, period_type: PeriodTypeEnum) -> str: + today = datetime.today() + + data = { + "reporttype": report_type.value, + "PeriodType": period_type.value, + "Month": today.month, + "Year": today.year, + "QuarterNum": get_quarter_num(today) - 1, # NOTE: I квартал в отчете это 0 + "FromMonth": 1, + "ToMonth": 12, + } + + match report_type: + case PeriodTypeEnum.QUARTER: + data["quarter"] = "quarter" + case PeriodTypeEnum.PERIOD: + data["total"] = "total" + + return _send_data(data) + + +if __name__ == "__main__": + dt = datetime.now() + print(dt, get_quarter_num(dt), get_quarter_roman(dt)) + + print() + + report: str = get_report( + report_type=ReportTypeEnum.SUMMARY, + period_type=PeriodTypeEnum.MONTH, + ) + soup = BeautifulSoup(report, "html.parser") + for tr in soup.select("table.report tr:has(> td)"): + print([td.get_text(strip=True) for td in tr.select("td")]) diff --git a/job_compassplus/cyr_lat_audit.py b/job_compassplus/cyr_lat_audit.py new file mode 100644 index 000000000..f85977092 --- /dev/null +++ b/job_compassplus/cyr_lat_audit.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import argparse +import re + +from collections import Counter +from datetime import datetime +from pathlib import Path + + +def check_mixed_layout(text: str) -> list[tuple[str, list[str]]]: + words: list[str] = re.findall(r"\b[a-zA-Zа-яА-ЯёЁ]+\b", text) + corrupted_words: list[tuple[str, list[str]]] = [] + + for word in words: + has_lat: bool = bool(re.search(r"[a-zA-Z]", word)) + has_cyr: bool = bool(re.search(r"[а-яА-ЯёЁ]", word)) + + if has_lat and has_cyr: + cyr_letters: list[str] = re.findall(r"[а-яА-ЯёЁ]", word) + corrupted_words.append((word, cyr_letters)) + + return corrupted_words + + +def process( + path: Path, + extensions: set[str], + ignore_words: set[str], + ignore_files: set[str], + ignore_dirs: set[str], +) -> None: + path = path.resolve() + + if not path.is_dir(): + raise Exception(f"Path {str(path)!r} must be a directory") + + total_files_count: int = 0 + checked_files_count: int = 0 + corrupted_files_count: int = 0 + corrupted_words_count: int = 0 + ignored_words_count: int = 0 + + suffix_total_counter: dict[str, int] = Counter() + suffix_checked_counter: dict[str, int] = Counter() + suffix_corrupted_files_counter: dict[str, int] = Counter() + suffix_corrupted_words_counter: dict[str, int] = Counter() + + start_dt: datetime = datetime.now() + + for f in path.rglob("*.*"): + if not f.is_file() or f.name.lower() in ignore_files: + continue + + has_any: bool = any( + parent_dir.name.lower() in ignore_dirs for parent_dir in f.parents + ) + if has_any: + continue + + total_files_count += 1 + suffix_total_counter[f.suffix] += 1 + + if f.suffix not in extensions: + continue + + try: + checked_files_count += 1 + suffix_checked_counter[f.suffix] += 1 + + text: str = f.read_text("utf-8") + corrupted_words: list[tuple[str, list[str]]] = check_mixed_layout(text) + if corrupted_words: + corrupted_files_count += 1 + suffix_corrupted_files_counter[f.suffix] += 1 + + valid_corrupted_words: list[tuple[str, list[str]]] = [] + for word, cyr_letters in corrupted_words: + if word.lower() in ignore_words: + ignored_words_count += 1 + else: + valid_corrupted_words.append((word, cyr_letters)) + + if valid_corrupted_words: + corrupted_words_count += len(valid_corrupted_words) + suffix_corrupted_words_counter[f.suffix] += len( + valid_corrupted_words + ) + + print(f"File: {str(f)!r}") + for word, cyr_letters in valid_corrupted_words: + print(f" Word: {word!r}, letters: {cyr_letters}") + print() + + except UnicodeError as e: + print(f"{f} skip, error: {e}") + + print(f"Elapsed: {datetime.now() - start_dt}") + print() + + print("=== SUMMARY STATISTICS ===") + print(f"Total files found: {total_files_count}") + print(f"Total checked files: {checked_files_count}") + print(f"Files with mixed layout: {corrupted_files_count}") + print(f"Words with mixed layout: {corrupted_words_count}") + print(f"Ignored words: {ignored_words_count}") + print() + print(f"All extensions in path: {dict(suffix_total_counter)}") + print(f"Checked extensions: {dict(suffix_checked_counter)}") + print(f"Corrupted files by suffix: {dict(suffix_corrupted_files_counter)}") + print(f"Corrupted words by suffix: {dict(suffix_corrupted_words_counter)}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Cyrillic and Latin alphabet audit", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "path", + metavar="/PATH/TO/DIRECTORY", + type=Path, + help="Path to directory", + ) + parser.add_argument( + "-e", + "--ext", + nargs="+", + default=[".xml", ".java", ".xsd"], + help="File extensions to check", + ) + parser.add_argument( + "-iw", + "--ignore-words", + nargs="+", + default=[], + help="Words to ignore (case-insensitive)", + ) + parser.add_argument( + "-if", + "--ignore-files", + nargs="+", + default=[], + help="Files to ignore (case-insensitive)", + ) + parser.add_argument( + "-id", + "--ignore-dirs", + nargs="+", + default=["build"], + help="Directories to ignore (case-insensitive)", + ) + args = parser.parse_args() + + target_extensions: set[str] = { + ex if ex.startswith(".") else f".{ex}" for ex in args.ext + } + target_ignore_words: set[str] = {word.lower() for word in args.ignore_words} + target_ignore_files: set[str] = {word.lower() for word in args.ignore_files} + target_ignore_dirs: set[str] = {word.lower() for word in args.ignore_dirs} + + process( + path=args.path, + extensions=target_extensions, + ignore_words=target_ignore_words, + ignore_files=target_ignore_files, + ignore_dirs=target_ignore_dirs, + ) diff --git a/job_compassplus/designer/common.py b/job_compassplus/designer/common.py new file mode 100644 index 000000000..ae63b4ff4 --- /dev/null +++ b/job_compassplus/designer/common.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re + +from dataclasses import dataclass, field +from pathlib import Path + + +PATTERN: re.Pattern = re.compile( + r"\* \[(?P\w+)\] (?P\d+) - (?P[^\n]+)\n\s*at\s*(?P[^\n]+)" +) + + +PATH_PROBLEMS_TAB: Path = Path(__file__).parent / "problems_tab.txt" + + +@dataclass +class Problem: + severity: str + code: int + error: str + at: str + maintainers: list[str] = field(default_factory=list) + + +def parse_text(text: str) -> list[Problem]: + items: list[Problem] = [] + + for m in PATTERN.finditer(text): + maintainers: list[str] = [] + for brackets in re.findall(r"\[(.*?)]", m.group()): + if "@" in brackets: + maintainers += [ + email.split("@")[0] + for email in brackets.split(", ") + ] + + items.append( + Problem( + severity=m.group("severity"), + code=int(m.group("code")), + error=m.group("error"), + at=m.group("at"), + maintainers=maintainers, + ) + ) + + return items + + +if __name__ == "__main__": + from pathlib import Path + + text = PATH_PROBLEMS_TAB.read_text("utf-8") + problems: list[Problem] = parse_text(text) + + print(f"Problems ({len(problems)}):") + for i, p in enumerate(problems, 1): + print(f"{i}. {p}") diff --git a/job_compassplus/designer/parse_problems_tab__code_by_number.py b/job_compassplus/designer/parse_problems_tab__code_by_number.py new file mode 100644 index 000000000..d28eb8535 --- /dev/null +++ b/job_compassplus/designer/parse_problems_tab__code_by_number.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import PATH_PROBLEMS_TAB, parse_text + + +text = PATH_PROBLEMS_TAB.read_text("utf-8") + +total: int = 0 +code_by_number: dict[int, int] = dict() + +for p in parse_text(text): + if p.code not in code_by_number: + code_by_number[p.code] = 0 + code_by_number[p.code] += 1 + + total += 1 + +print(f"Code ({len(code_by_number)}):") +for code, number in sorted(code_by_number.items(), key=lambda x: x[1], reverse=True): + print(f" {code}: {number}") +print(f"Total: {total}") +""" +Code (4): + 51: 4 + 137: 3 + 132: 2 + 999: 1 +Total: 10 +""" diff --git a/job_compassplus/designer/parse_problems_tab__maintainer_by_number.py b/job_compassplus/designer/parse_problems_tab__maintainer_by_number.py new file mode 100644 index 000000000..4fb387851 --- /dev/null +++ b/job_compassplus/designer/parse_problems_tab__maintainer_by_number.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from common import PATH_PROBLEMS_TAB, parse_text + + +text = PATH_PROBLEMS_TAB.read_text("utf-8") + +# TODO: +filter_by_codes: list[int] = [ + # 127, 137 +] + +user_by_number: dict[str, int] = dict() + +for p in parse_text(text): + if filter_by_codes and p.code not in filter_by_codes: + continue + + users = p.maintainers.copy() + if not users: + users.append("") + + for user in users: + if user not in user_by_number: + user_by_number[user] = 0 + user_by_number[user] += 1 + +print(f"Users ({len(user_by_number)}):") +for user, number in sorted(user_by_number.items(), key=lambda x: x[1], reverse=True): + print(f" {user!r}: {number}") +""" +Users (3): + 'bar': 7 + 'foo': 3 + '': 2 +""" diff --git a/job_compassplus/designer/parse_problems_tab__maintainer_by_number_details.py b/job_compassplus/designer/parse_problems_tab__maintainer_by_number_details.py new file mode 100644 index 000000000..893dc9167 --- /dev/null +++ b/job_compassplus/designer/parse_problems_tab__maintainer_by_number_details.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import defaultdict +from common import PATH_PROBLEMS_TAB, Problem, parse_text + + +text = PATH_PROBLEMS_TAB.read_text("utf-8") + +# TODO: +filter_by_codes: list[int] = [ + # 127, 137 +] +print(f"Filter by codes: {filter_by_codes}") + +user_by_number_details: dict[str, list[Problem]] = defaultdict(list) +code_by_number: dict[int, int] = defaultdict(int) + +for p in parse_text(text): + code: int = p.code + if filter_by_codes and code not in filter_by_codes: + continue + + code_by_number[code] += 1 + + users = p.maintainers.copy() + if not users: + users.append("") + + user_by_number_details[", ".join(users)].append(p) + +print(f"Total users: {len(user_by_number_details)}") +print(f"Stats (codes {len(code_by_number)}, total {sum(code_by_number.values())}):") +for code, number in sorted(code_by_number.items(), key=lambda x: x[1], reverse=True): + print(f" Code {code}: {number}") +print() + +INDENT_1: str = " " + +for user, all_problems in sorted( + user_by_number_details.items(), + key=lambda x: len(x[1]), + reverse=True, +): + print(f"{user!r} ({len(all_problems)}):") + + code_by_problems: dict[int, list[Problem]] = defaultdict(list) + for p in all_problems: + code_by_problems[p.code].append(p) + + for code, problems in code_by_problems.items(): + print(f"{INDENT_1}Code {code} ({len(problems)}):") + + for p in problems: + print(f"{INDENT_1 * 2}[{p.severity}] {p.error} at {p.at}") + + print() + +""" +Filter by codes: [] +Total users: 4 +Stats (codes 4, total 10): + Code 51: 4 + Code 137: 3 + Code 132: 2 + Code 999: 1 + +'bar' (5): + Code 137 (1): + [NOTE] XXX [bar@example.com] [Fix available] at ABC::Common::Foo::Bar + Code 51 (4): + [WARNING] The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] at ABC::Common::Foo:Model:Bar + [WARNING] The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] at ABC::Common::Foo:Model:Bar + [WARNING] The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] at ABC::Common::Foo:Model:Bar + [WARNING] The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] at ABC::Common::Foo:Model:Bar + +'' (2): + Code 132 (2): + [NOTE] Editor page does not contain property `XXX` from `overridden` base page (See RADIX-23441) [Link] at ABC::FOO::BAR:General + [NOTE] Editor page does not contain property `XXX` from `overridden` base page (See RADIX-23441) [Link] at ABC::FOO::BAR:General + +'foo, bar' (2): + Code 137 (1): + [NOTE] XXX [foo@example.com, bar@example.com] [Fix available] at ABC::Common::Foo::Bar + Code 999 (1): + [ERROR] XXX [foo@example.com, bar@example.com] at ABC::Common::Foo::Bar + +'foo' (1): + Code 137 (1): + [NOTE] XXX [foo@example.com] [Fix available] at ABC::Interfacing.Foo::Bar +""" diff --git a/job_compassplus/designer/problems_tab.txt b/job_compassplus/designer/problems_tab.txt new file mode 100644 index 000000000..5d2e25805 --- /dev/null +++ b/job_compassplus/designer/problems_tab.txt @@ -0,0 +1,29 @@ +Cursor Class 'ABC::Interfacing.Foo::Bar' +* [NOTE] 137 - XXX [foo@example.com] [Fix available] + at ABC::Interfacing.Foo::Bar + +Statement Class 'ABC::Common::Foo::Bar' +* [NOTE] 137 - XXX [bar@example.com] [Fix available] + at ABC::Common::Foo::Bar + +User Method 'ABC::Common::Foo:Model:Bar' +* [WARNING] 51 - The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] + at ABC::Common::Foo:Model:Bar +* [WARNING] 51 - The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] + at ABC::Common::Foo:Model:Bar +* [WARNING] 51 - The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] + at ABC::Common::Foo:Model:Bar +* [WARNING] 51 - The definition 'ABC::Common::Foo:Bar:ownerPid' is XXX (See RADIX-24309, RADIX-17843. Ignore werror.) [Link] [bar@example.com] + at ABC::Common::Foo:Model:Bar + +Editor Page 'ABC::FOO::BAR:General' +* [NOTE] 132 - Editor page does not contain property `XXX` from `overridden` base page (See RADIX-23441) [Link] + at ABC::FOO::BAR:General +* [NOTE] 132 - Editor page does not contain property `XXX` from `overridden` base page (See RADIX-23441) [Link] + at ABC::FOO::BAR:General + +Statement Class 'ABC::Common::Foo::Bar' +* [NOTE] 137 - XXX [foo@example.com, bar@example.com] [Fix available] + at ABC::Common::Foo::Bar +* [ERROR] 999 - XXX [foo@example.com, bar@example.com] + at ABC::Common::Foo::Bar diff --git a/job_compassplus/doc_vs_docx.py b/job_compassplus/doc_vs_docx.py new file mode 100644 index 000000000..d74a1bd0b --- /dev/null +++ b/job_compassplus/doc_vs_docx.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +# pip install python-docx==1.1.0 +from docx import Document + + +PATH_DIR: str = r"C:\DOC\Specifications\Interfaces" + + +docx_num = 0 +doc_num = 0 +for f in Path(PATH_DIR).glob("*.doc*"): + if f.name.startswith("~"): + continue + + try: + Document(f) + ok = True + docx_num += 1 + except: + ok = False + doc_num += 1 + + print(f"[{'+' if ok else '-'}]", f) + +print() +print("Total:", docx_num + doc_num) +print("Docx:", docx_num) +print("Doc:", doc_num) diff --git a/job_compassplus/find_duplicated_specs.py b/job_compassplus/find_duplicated_specs.py new file mode 100644 index 000000000..374129698 --- /dev/null +++ b/job_compassplus/find_duplicated_specs.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import defaultdict +from pathlib import Path +from zlib import crc32 + + +DIR = Path(r"C:\DOC\W4") + +crc_by_files: dict[int, list[Path]] = defaultdict(list) +for f in DIR.rglob("*"): + if not f.is_file(): + continue + + crc = crc32(f.read_bytes()) + crc_by_files[crc].append(f) + +for files in crc_by_files.values(): + if len(files) == 1: + continue + + print(*files, sep="\n") + print() diff --git a/job_compassplus/find_invalid_utf_8_files_java.py b/job_compassplus/find_invalid_utf_8_files_java.py new file mode 100644 index 000000000..3deca20d0 --- /dev/null +++ b/job_compassplus/find_invalid_utf_8_files_java.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + + +def process(path: str): + if isinstance(path, str): + path = Path(path) + + if not path.exists(): + raise Exception(f"Not exists: {path}") + + for file in path.rglob("*.java"): + try: + file.read_text("utf-8") + except UnicodeError as e: + print(f"{file}, error: {e}") + for line in file.read_bytes().splitlines(): + try: + str(line, "utf-8") + except UnicodeError: + print(f" Line: {line}") + + print() + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Find invalid utf-8 files java" + ) + parser.add_argument( + "path", + metavar="/PATH/TO", + help="Path to source tree", + ) + args = parser.parse_args() + + process(args.path) diff --git a/job_compassplus/jira_avatars/get_avatars.py b/job_compassplus/jira_avatars/get_avatars.py new file mode 100644 index 000000000..a714a0cde --- /dev/null +++ b/job_compassplus/jira_avatars/get_avatars.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys + +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Self + +DIR = Path(__file__).resolve().parent +ROOT_DIR = DIR.parent + +sys.path.append(str(ROOT_DIR)) +from root_config import JIRA_HOST +from root_common import session + + +# Examples: +""" +{ + "system": [ + {"id":"10122","isSystemAvatar":true,"isSelected":false,"isDeletable":false,"urls":{"48x48":"https://.../useravatar?avatarId=10122","24x24":"https://.../useravatar?size=small&avatarId=10122","16x16":"https://.../useravatar?size=xsmall&avatarId=10122","32x32":"https://.../useravatar?size=medium&avatarId=10122"},"selected":false}, + ... + {"id":"17241","isSystemAvatar":true,"isSelected":true,"isDeletable":false,"urls":{"48x48":"https://.../useravatar?avatarId=17241","24x24":"https://.../useravatar?size=small&avatarId=17241","16x16":"https://.../useravatar?size=xsmall&avatarId=17241","32x32":"https://.../useravatar?size=medium&avatarId=17241"},"selected":true}, + ... + {"id":"17252","isSystemAvatar":true,"isSelected":false,"isDeletable":false,"urls":{"48x48":"https://.../useravatar?avatarId=17252","24x24":"https://.../useravatar?size=small&avatarId=17252","16x16":"https://.../useravatar?size=xsmall&avatarId=17252","32x32":"https://.../useravatar?size=medium&avatarId=17252"},"selected":false}], + "custom": [] +} +""" + + +@dataclass(frozen=True) +class AvatarUrls: + size_48x48: str + size_32x32: str + size_24x24: str + size_16x16: str + + @classmethod + def from_dict(cls, data: dict[str, str]) -> Self: + return cls( + size_48x48=data.get("48x48"), + size_24x24=data.get("24x24"), + size_32x32=data.get("32x32"), + size_16x16=data.get("16x16"), + ) + + +@dataclass(frozen=True) +class JiraAvatar: + id: str + is_system_avatar: bool + is_selected: bool + is_deletable: bool + urls: AvatarUrls + selected: bool # NOTE: Дублирует isSelected + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Self: + return cls( + id=data["id"], + is_system_avatar=data["isSystemAvatar"], + is_selected=data["isSelected"], + is_deletable=data["isDeletable"], + urls=AvatarUrls.from_dict(data["urls"]), + selected=data["selected"], + ) + + +@dataclass(frozen=True) +class JiraAvatarResponse: + system: list[JiraAvatar] + custom: list[JiraAvatar] + + @classmethod + def from_dict(cls, data: dict[str, list[dict[str, Any]]]) -> Self: + def _get_avatars(key: str) -> list[JiraAvatar]: + return [JiraAvatar.from_dict(d) for d in data[key]] + + return cls( + system=_get_avatars("system"), + custom=_get_avatars("custom"), + ) + + +def get_avatars(username: str) -> JiraAvatarResponse: + url = f"{JIRA_HOST}/rest/api/latest/user/avatars?username={username}" + + rs = session.get(url) + rs.raise_for_status() + + return JiraAvatarResponse.from_dict(rs.json()) + + +if __name__ == "__main__": + rs: JiraAvatarResponse = get_avatars("ipetrash") + + print(f"System ({len(rs.system)}):") + for i, avatar in enumerate(rs.system, 1): + print(f" {i}. {avatar}") + + print() + + print(f"Custom ({len(rs.custom)}):") + for i, avatar in enumerate(rs.custom, 1): + print(f" {i}. {avatar}") diff --git a/job_compassplus/jira_dump_issues.py b/job_compassplus/jira_dump_issues.py new file mode 100644 index 000000000..13a3008dc --- /dev/null +++ b/job_compassplus/jira_dump_issues.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time +import sqlite3 + +from pathlib import Path +from typing import Any + +from root_config import JIRA_HOST +from root_common import session + + +DIR: Path = Path(__file__).resolve().parent +FILE_NAME_DB: Path = DIR / "jira_local.sqlite" + +URL_SEARCH = f"{JIRA_HOST}/rest/api/latest/search" + + +def init_sqlite(filename: Path) -> sqlite3.Connection: + conn = sqlite3.connect(filename) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS Task ( + id INTEGER PRIMARY KEY, + key TEXT, + summary TEXT, + description TEXT, + status TEXT, + issue_type TEXT, + created TEXT, + is_indexed INTEGER DEFAULT 0 + ) + """ + ) + conn.commit() + return conn + + +def get_last_id(conn: sqlite3.Connection, project: str) -> int: + res = conn.execute( + "SELECT MAX(id) FROM Task WHERE key LIKE ?", (f"{project}-%",) + ).fetchone() + return res[0] if res[0] else 0 + + +def sync_jira_to_sqlite(conn: sqlite3.Connection, projects: list[str]): + max_results: int = 100 + + for project in projects: + last_id: int = get_last_id(conn, project) + + while True: + print(f"[*] Поиск задач в Jira проекта {projects} от ID > {last_id}...") + + filter_id: str = f"AND id > {last_id}" if last_id > 0 else "" + jql: str = f"project = {project} {filter_id} ORDER BY id ASC" + + query: dict[str, Any] = { + "jql": jql, + "fields": "key,summary,description,status,issuetype,created", + "maxResults": max_results, + } + + rs = session.get(URL_SEARCH, params=query) + try: + rs.raise_for_status() + except Exception as e: + print(rs.json()) + raise e + + issues: list[dict] = rs.json().get("issues") + if not issues: + print("Больше нет задач") + break + + for issue in issues: + print(issue) + + issue_id: int = int(issue["id"]) + issue_key: str = issue["key"] + issue_summary: str = issue["fields"].get("summary", "") + issue_desc: str = str(issue["fields"].get("description")) + issue_status: str = issue["fields"]["status"]["name"] + issue_type: str = issue["fields"]["issuetype"]["name"] + issue_created: str = issue["fields"]["created"] + + conn.execute( + """ + INSERT OR IGNORE INTO Task + (id, key, summary, description, status, issue_type, created) + VALUES + (?, ?, ?, ?, ?, ?, ?) + """, + ( + issue_id, + issue_key, + issue_summary, + issue_desc, + issue_status, + issue_type, + issue_created, + ), + ) + + last_id = issue_id + + conn.commit() + + print(f"[+] Загружено {len(issues)} задач...") + + time.sleep(5) + + +if __name__ == "__main__": + # TODO: Возможность задавать аргументом + projects: list[str] = [ + # "OPTT", + "TXI", + "TWRBS", + "TXCORE", + "TXACQ", + ] + + # TODO: Возможность задавать аргументом путь к БД + # FILE_NAME_DB: Path = DIR / "jira_local_OPTT.sqlite" + + db_conn = init_sqlite(FILE_NAME_DB) + sync_jira_to_sqlite(db_conn, projects) diff --git a/job_compassplus/jira_get_current_user.py b/job_compassplus/jira_get_current_user.py new file mode 100644 index 000000000..32078bc7f --- /dev/null +++ b/job_compassplus/jira_get_current_user.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from typing import Any + +from root_config import JIRA_HOST +from root_common import session + + +URL_API_MYSELF = f"{JIRA_HOST}/rest/api/latest/myself" + + +def get_current_user() -> dict[str, Any]: + rs = session.get(URL_API_MYSELF) + rs.raise_for_status() + + return rs.json() + + +if __name__ == "__main__": + user = get_current_user() + print("Current user:", user) + print("Current user name:", user["name"]) + print() + + import json + print(f"Current user (pretty):\n{json.dumps(user, indent=4)}") diff --git a/job_compassplus/jira_get_total_resolved.py b/job_compassplus/jira_get_total_resolved.py new file mode 100644 index 000000000..87eb9fcaa --- /dev/null +++ b/job_compassplus/jira_get_total_resolved.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time +from dataclasses import dataclass + +from root_config import JIRA_HOST +from root_common import session + + +URL_SEARCH = f"{JIRA_HOST}/rest/api/latest/search" + +JQL_RESOLUTION_DATE = "assignee = currentUser() AND resolutiondate" +JQL_TOTAL = f"{JQL_RESOLUTION_DATE} IS NOT EMPTY" +JQL_LAST_WEEK = f"{JQL_RESOLUTION_DATE} >= startOfDay(-7)" +JQL_LAST_MONTH = f"{JQL_RESOLUTION_DATE} >= startOfMonth(-1)" +JQL_LAST_YEAR = f"{JQL_RESOLUTION_DATE} >= startOfMonth(-12)" + + +@dataclass +class Stats: + last_7_days: int + last_month: int + last_year: int + total: int + + +def get_total(jql: str) -> int: + query = { + "jql": jql, + "fields": "key", + "maxResults": 0, + } + + rs = session.get(URL_SEARCH, params=query) + rs.raise_for_status() + + return rs.json()["total"] + + +def get_stats(sleep: float = 0.5) -> Stats: + last_7_days = get_total(JQL_LAST_WEEK) + time.sleep(sleep) + + last_month = get_total(JQL_LAST_MONTH) + time.sleep(sleep) + + last_year = get_total(JQL_LAST_YEAR) + time.sleep(sleep) + + total = get_total(JQL_TOTAL) + + return Stats( + last_7_days=last_7_days, + last_month=last_month, + last_year=last_year, + total=total, + ) + + +if __name__ == "__main__": + print(get_stats()) diff --git a/job_compassplus/jira_search_by_avatar/main.py b/job_compassplus/jira_search_by_avatar/main.py new file mode 100644 index 000000000..a080f57e3 --- /dev/null +++ b/job_compassplus/jira_search_by_avatar/main.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +import time +from pathlib import Path + +DIR = Path(__file__).resolve().parent +ROOT_DIR = DIR.parent + +sys.path.append(str(ROOT_DIR)) +from root_config import JIRA_HOST +from root_common import session + + +URL_PROFILE_FORMAT = f"{JIRA_HOST}/secure/ViewProfile.jspa?name={{name}}" +URL_USER_SEARCH = f"{JIRA_HOST}/rest/api/latest/user/search" + + +def search_by_avatar(avatar_id: int) -> list[dict]: + start_at = 0 + max_results = 500 + + avatar_id = str(avatar_id) + + items = [] + while True: + params = { + "username": ".", + "startAt": start_at, + "maxResults": max_results, + } + + rs = session.get(URL_USER_SEARCH, params=params) + rs.raise_for_status() + + result = rs.json() + if not result: + break + + for user in result: + if avatar_id in user["avatarUrls"]["48x48"]: + items.append(user) + + time.sleep(5) + + start_at += max_results + + return items + + +if __name__ == "__main__": + avatar_id = 17250 # pirate + users = search_by_avatar(avatar_id) + print(f"Users ({len(users)}):") + for i, user in enumerate(users, 1): + display_name = user["displayName"] + user_url = URL_PROFILE_FORMAT.format(name=user["key"]) + print(f"{i}. '{display_name}': {user_url}") diff --git a/job_compassplus/jira_show_last_issue/main.py b/job_compassplus/jira_show_last_issue/main.py new file mode 100644 index 000000000..062ee8a21 --- /dev/null +++ b/job_compassplus/jira_show_last_issue/main.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +from pathlib import Path + +DIR = Path(__file__).resolve().parent +ROOT_DIR = DIR.parent +sys.path.append(str(ROOT_DIR)) +from root_config import JIRA_HOST +from root_common import session + + +URL_SEARCH = f"{JIRA_HOST}/rest/api/latest/search" + + +def get_last_issue_key(project: str) -> str | None: + query = { + "jql": f"project={project} ORDER BY created DESC", + "fields": "key", + "maxResults": 1, + } + + rs = session.get(URL_SEARCH, params=query) + if rs.status_code == 400: + return + + rs.raise_for_status() + + try: + return rs.json()["issues"][0]["key"] + except Exception: + return + + +if __name__ == "__main__": + import time + + for project in [ + "OPTT", + "NOT_FOUND", + "RADIX", + "TXI", + "TXACQ", + "TXCORE", + "TXISS", + "TXPG", + "TWO", + "FLORA", + ]: + last_issue_key: str | None = get_last_issue_key(project=project) + print(f"{project}: {last_issue_key if last_issue_key else '-'}", ) + time.sleep(0.5) diff --git a/job_compassplus/jira_show_last_issue/notify_me.py b/job_compassplus/jira_show_last_issue/notify_me.py new file mode 100644 index 000000000..35401d6d2 --- /dev/null +++ b/job_compassplus/jira_show_last_issue/notify_me.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time +from datetime import datetime + +from tkinter.messagebox import showinfo + +from main import get_last_issue_key + + +NEED_PROJECT: str = "OPTT" +NEED_ISSUE_ID: int = 999 + + +while True: + last_issue_key: str = get_last_issue_key(NEED_PROJECT) + print(f'[{datetime.now():%d/%m/%Y %H:%M:%S}] last_issue_key: {last_issue_key}') + + # NOTE: "OPTT-1234" -> 1234 + issue_id: int = int(last_issue_key.split("-")[-1]) + if issue_id >= NEED_ISSUE_ID: + showinfo(title="Информация", message=f"Появилась задача {last_issue_key}!") + + time.sleep(30) diff --git a/job_compassplus/jira_sprint_get_total_overtime_hours.py b/job_compassplus/jira_sprint_get_total_overtime_hours.py new file mode 100644 index 000000000..4c7b48ccd --- /dev/null +++ b/job_compassplus/jira_sprint_get_total_overtime_hours.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +import sys + +from dataclasses import dataclass +from datetime import datetime + +from root_config import JIRA_HOST +from root_common import session + + +URL_SEARCH = f"{JIRA_HOST}/rest/api/latest/search" + +FIELD_OVERTIME_HOURS = "customfield_13440" + +QUERY = { + "jql": ( + "assignee = currentUser()" + " AND project = Sprint AND type = Sub-task" + " AND created >= startOfYear() AND created <= endOfYear()" + " ORDER BY created DESC" + ), + "fields": f"key,created,{FIELD_OVERTIME_HOURS}", +} + + +@dataclass +class Sprint: + key: str + created: datetime + overtime_hours: int + + +default_handler = logging.StreamHandler(stream=sys.stdout) +default_handler.setFormatter( + logging.Formatter( + "[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s" + ) +) +logger = logging.getLogger("jira_sprint_get_total_overtime_hours") +logger.setLevel(logging.WARNING) +logger.addHandler(default_handler) + + +def get_sprints_with_overtime_hours() -> list[Sprint]: + logger.debug(f"Load: {URL_SEARCH}") + + rs = session.get(URL_SEARCH, params=QUERY) + logger.debug(f"Response: {rs}") + rs.raise_for_status() + + items: list[Sprint] = [] + + issues = rs.json()["issues"] + logger.info(f"Total issues: {len(issues)}") + + for issue in issues: + key = issue["key"] + created_str = issue["fields"]["created"] + + overtime_hours = issue["fields"][FIELD_OVERTIME_HOURS] + overtime_hours: int = int(overtime_hours) if overtime_hours else 0 + + logger.info(f"Issue: {key}, created_str: {created_str}, overtime hours: {overtime_hours}") + + items.append( + Sprint( + key=key, + created=datetime.strptime(created_str, "%Y-%m-%dT%H:%M:%S.%f%z"), + overtime_hours=overtime_hours, + ) + ) + + return items + + +if __name__ == "__main__": + # NOTE: Debug + # logger.setLevel(logging.DEBUG) + + total_overtime_hours = 0 + sprints = get_sprints_with_overtime_hours() + print(f"Sprints ({len(sprints)}):") + for i, sprint in enumerate(sprints, 1): + print(f" {i}. {sprint}") + total_overtime_hours += sprint.overtime_hours + + print() + print(f"Total overtime hours: {total_overtime_hours}") diff --git a/job_compassplus/job_report/main.py b/job_compassplus/job_report/main.py deleted file mode 100644 index c0835b90c..000000000 --- a/job_compassplus/job_report/main.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from itertools import chain -from utils import get_report_persons_info, get_person_info - - -report_dict = get_report_persons_info() - -# Вывести всех сотрудников, отсортировав их по количестве переработанных часов -person_list = set(chain(*report_dict.values())) - -# Проверка того, что сортировка работает (в принципе, думаю можно удалить) -assert sorted(person_list, key=lambda x: x.deviation_of_time) == \ - sorted(person_list, key=lambda x: x.deviation_of_time.total) - -sorted_person_list = sorted(person_list, key=lambda x: x.deviation_of_time, reverse=True) - -for i, person in enumerate(sorted_person_list, 1): - print(f'{i:>3}. {person.full_name} {person.deviation_of_time}') - -print() -person = get_person_info(second_name='Петраш', first_name='Илья', report_dict=report_dict) -if person: - print(f'#{sorted_person_list.index(person) + 1}. {person.full_name} {person.deviation_of_time}') diff --git a/job_compassplus/job_report/report_person.py b/job_compassplus/job_report/report_person.py deleted file mode 100644 index 29fa6946d..000000000 --- a/job_compassplus/job_report/report_person.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -from functools import total_ordering -from typing import List - - -class ReportPerson: - """Класс для описания сотрудника в отчете.""" - - def __init__(self, tags: List[str]): - # ФИО - self.second_name, self.first_name, self.middle_name = tags[0].split(maxsplit=2) - - # Невыходов на работу - self.absence_from_work = int(tags[1]) - - # По календарю (смен / ч:мин) - # Для точного значения посещенных дней, может быть указано как "3 = 4- (1 О)", поэтому - # отсекаем правую, от знака равно, сторону, удаляем пробелы и переводим в число - self.need_to_work_days = self.get_work_day(tags[2]) - self.need_to_work_on_time = self.get_work_time(tags[3]) - - # Фактически (смен / ч:мин) - self.worked_days = self.get_work_day(tags[4]) - self.worked_time = self.get_work_time(tags[5]) - - # Отклонение (смен / ч:мин) - self.deviation_of_day = self.get_work_day(tags[6]) - self.deviation_of_time = self.get_work_time(tags[7]) - - @property - def full_name(self): - return self.second_name + ' ' + self.first_name + ' ' + self.middle_name - - @staticmethod - def get_work_day(day_str): - return int(day_str) if '=' not in day_str else int(day_str.split('=')[0].strip()) - - @total_ordering - class Time: - """Простой класс для хранения даты работы.""" - - def __init__(self, time_str: str): - # TODO: supports self._seconds - self._hours, self._minutes, self._seconds = map(int, time_str.split(':')) - - @property - def total(self) -> int: - """Всего минут""" - - return self._hours * 60 + self._minutes - - def __repr__(self): - return "{:0>2}:{:0>2}".format(self._hours, self._minutes) - - def __eq__(self, other): - return self.total == other.total - - def __lt__(self, other): - return self.total < other.total - - @staticmethod - def get_work_time(time_str): - return ReportPerson.Time(time_str) - - def __hash__(self): - return hash(self.full_name) - - def __eq__(self, other): - return self.full_name == other.full_name - - def __repr__(self): - return ( - f"{self.full_name}. Невыходов на работу: {self.absence_from_work}. По календарю ({self.need_to_work_days} смен / {self.need_to_work_on_time} ч:мин). " - f"Фактически ({self.worked_days} смен / {self.worked_time} ч:мин) Отклонение ({self.deviation_of_day} смен / {self.deviation_of_time} ч:мин)" - ) diff --git a/job_compassplus/job_report/utils.py b/job_compassplus/job_report/utils.py deleted file mode 100644 index d5c5603ef..000000000 --- a/job_compassplus/job_report/utils.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import os.path -import sys - -from datetime import datetime -from collections import defaultdict -from itertools import chain -from pathlib import Path -from typing import Dict, Set - - -DIR = Path(__file__).resolve().parent - -sys.path.append(str(DIR.parent)) -from root_common import session -from current_job_report.get_user_and_deviation_hours import get_report_context - -from bs4 import BeautifulSoup - -from report_person import ReportPerson - - -LOGGING_DEBUG = False - - -if LOGGING_DEBUG: - import logging - logging.basicConfig(level=logging.DEBUG) - - -def get_report_persons_info() -> Dict[str, Set[ReportPerson]]: - today = datetime.today().strftime('%Y-%m-%d') - report_file_name = f'report_{today}.html' - - # Если кэш-файл отчета не существует, загружаем новые данные и сохраняем в кэш-файл - if not os.path.exists(report_file_name): - if LOGGING_DEBUG: - print(f'{report_file_name} not exist') - - context = get_report_context() - - with open(report_file_name, mode='w', encoding='utf-8') as f: - f.write(context) - else: - if LOGGING_DEBUG: - print(f'{report_file_name} exist') - - with open(report_file_name, encoding='utf-8') as f: - context = f.read() - - html = BeautifulSoup(context, 'lxml') - report = html.select('#report tbody tr') - - current_dep = None - report_dict = defaultdict(set) - - for row in report: - children = list(row.children) - if len(children) == 1 and children[0].name == 'th': - current_dep = children[0].text.strip() - continue - - if children[0].has_attr('class') and children[0].attrs['class'][0] == 'person': - person_tags = [children[0].text] + [i.text for i in row.nextSibling.select('td')[1:]] - if len(person_tags) != 8: - continue - - person = ReportPerson(person_tags) - report_dict[current_dep].add(person) - - return report_dict - - -def get_person_info(second_name, first_name=None, middle_name=None, report_dict=None): - if not report_dict: - report_dict = get_report_persons_info() - - # Вывести всех сотрудников, отсортировав их по количеству переработанных часов - for person in list(chain(*report_dict.values())): - found = person.second_name == second_name - - if first_name is not None: - found = found and person.first_name == first_name - - if middle_name is not None: - found = found and person.middle_name == middle_name - - if found: - return person - - -if __name__ == '__main__': - report_dict = get_report_persons_info() - print(len(report_dict)) - print(sum(map(len, report_dict.values()))) - print() - - for dep, persons in report_dict.items(): - print(f'{dep} ({len(persons)})') - for p in persons: - print(f' {p}') - - print() diff --git a/job_compassplus/merge_tc33a_files.py b/job_compassplus/merge_tc33a_files.py new file mode 100644 index 000000000..7c6fbe9fb --- /dev/null +++ b/job_compassplus/merge_tc33a_files.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from pathlib import Path + + +DIR = Path(__file__).parent.resolve() +path_output = DIR / "tc33a-all-files.txt" + +with open(path_output, "w", encoding="ascii") as f_out: + path_dir = Path(r"C:\DOC\Visa спецификации\Base II TC33A\TC33_Capture_Transaction") + for f in path_dir.rglob("*.*"): + if not f.is_file(): + print(f"[#] Не файл: {str(f)!r}") + continue + + if any( + name in str(f) + for name in [ + # NOTE: Замечены файлы с кривым форматом, типа дат 222222 и 123456. + # В задаче не было таких, вероятно кривой пример + "TWICH-4378", + # NOTE: Файл был дополнен, переделан и отдельно занесен в тест + "TC33_CaptureTran_All_CP_Test.ctf", + # NOTE: Была ошибка при парсинге, постфикс имени файла вызывает сомнения + "TC33_CaptureTran_All_CP_Test (No CP07, CP05 TCR7)_VISA_err.ctf", + ] + ): + print(f"[#] Пропуск файла: {str(f)!r}") + continue + + print(f) + print(f.relative_to(path_dir)) + + try: + text: str = f.read_text("ascii") + print("Длина:", len(text)) + + first_line_length = len(text.splitlines()[0]) + print("Длина первой строки:", first_line_length) + if first_line_length != 168: + print("[#] Не CTF, пропуск файла") + continue + + # NOTE: Кривой формат даты + if "_MC" in str(f) and "CP12" in text and "12345678 " in text: + print(f"[#] Замена кривой даты в {str(f)!r}") + text = text.replace("12345678 ", "1030140815") + + if "inctf TC33A.EPIN.txt" in str(f) and "262012" in text: + print(f"[#] Замена кривой даты в {str(f)!r}") + text = text.replace("262012", "261220") + + lines = text.splitlines() + for i, line in enumerate(lines): + if line.startswith("3301") and re.search("^3300.+?CP06", lines[i - 1]): + print( + f"[#] Комментирование строки CP06 TCR1 - не поддерживается запись" + ) + lines[i] = f"# NOTE: Not supported: {line}" + + elif line.startswith("3309170"): + print( + f"[#] Комментирование строки CP01 TCR9 - не поддерживается запись" + ) + lines[i] = f"# NOTE: Not supported: {line}" + + text = "\n".join(lines) + + f_out.write(f"# START FILE: {f.relative_to(path_dir)}\n") + f_out.write(text) + f_out.write(f"\n# END FILE: {f.relative_to(path_dir)}\n\n") + + except UnicodeDecodeError as e: + print("[#]", e) + + print() diff --git a/job_compassplus/mysite_compassplus_com/common.py b/job_compassplus/mysite_compassplus_com/common.py new file mode 100644 index 000000000..147655507 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/common.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +from pathlib import Path +from typing import Any + +from bs4 import Tag + +DIR = Path(__file__).resolve().parent +ROOT_DIR = DIR.parent + +sys.path.append(str(ROOT_DIR)) +from site_common import do_get, do_post +from root_config import JIRA_HOST + + +URL_BASE = "https://mysite.compassplus.com" +URL = f"{URL_BASE}/Person.aspx?accountname={{}}" + + +def get_profile_organization(full_username: str) -> dict[str, Any]: + url = f"{URL_BASE}/_vti_bin/SilverlightProfileService.json/GetUserSLProfileData" + rs = do_post(url, json={"AccountNames": [full_username]}) + return rs.json() + + +def is_active_profile(full_username: str) -> bool: + organization = get_profile_organization(full_username) + data = organization["d"][0] + # Если хотя бы один из них имеет значение, отличное от [] или None + return bool(data["Parent"] or data["Siblings"] or data["Children"]) + + +def get_jira_user_active(username: str) -> bool: + url = f"{JIRA_HOST}/rest/api/latest/user?username={username}" + return do_get(url).json()["active"] + + +def get_text(el: Tag) -> str: + return el.get_text(strip=True) + + +if __name__ == "__main__": + full_username = r"CP\ipetrash" + + rs = do_get(URL.format(full_username)) + print(rs) + + organization = get_profile_organization(full_username) + print(organization) + + data = organization["d"][0] + print("Parent:", data["Parent"]) + print("Siblings:", data["Siblings"]) + print("Children:", data["Children"]) + print() + + print("Is active (mysite):", is_active_profile(full_username)) + print("Is active (jira):", get_jira_user_active(full_username.rsplit("\\")[-1])) diff --git a/job_compassplus/mysite_compassplus_com/get_all_department_employees.py b/job_compassplus/mysite_compassplus_com/get_all_department_employees.py new file mode 100644 index 000000000..94cf26894 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_all_department_employees.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass +from bs4 import BeautifulSoup +from common import URL, do_get, get_text + + +@dataclass +class Employee: + full_name: str + user_name: str + position: str + url: str + + +def get_employees(boss_username: str) -> list[Employee]: + url: str = URL.format(boss_username) + print("Load", url) + + rs = do_get(url) + + root = BeautifulSoup(rs.content, "html.parser") + root_table = root.select_one("#ReportingHierarchy") + + items = [] + + for td in root_table.select(".ms-orgname"): + a = td.a + url = a["href"] + items.append( + Employee( + full_name=get_text(a), + # Example: "https://.../Person.aspx?accountname=CP%5Cipetrash" -> "ipetrash" + user_name=url.split("%5C")[-1], + url=url, + position=get_text(td.select_one(".ms_metadata")), + ) + ) + + return items + + +if __name__ == "__main__": + from base64 import b64decode + + for boss_username in [ + b64decode("Q1BcbnZheW5lcg==").decode("utf-8"), + b64decode("Q1BcYXZvc3RyaWtvdg==").decode("utf-8"), + b64decode("Q1BceXJlbWl6b3Y=").decode("utf-8"), + ]: + print(f"Boss: {boss_username!r}") + + employees = get_employees(boss_username) + print(f"Employees ({len(employees)}):") + for i, employee in enumerate(employees, 1): + print(f"{i}. {employee}") + """ + ... + 9. Employee(full_name='Ilya A. Petrash', user_name='ipetrash', position='Senior Software Engineer', url='https://mysite.compassplus.com:443/Person.aspx?accountname=CP%5Cipetrash') + ... + """ + print() + + usernames = sorted(x.user_name for x in employees) + print(usernames) + print(", ".join(usernames)) + + print("\n" + "-" * 100 + "\n") diff --git a/job_compassplus/mysite_compassplus_com/get_person_info.py b/job_compassplus/mysite_compassplus_com/get_person_info.py new file mode 100644 index 000000000..6a26aa2eb --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_person_info.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass +from urllib.parse import urljoin + +from bs4 import BeautifulSoup +from common import URL, do_get, get_text, is_active_profile, get_jira_user_active + + +@dataclass +class Person: + name: str + position: str + department: str + img_url: str + location: str + birthday: str + is_active: bool | None = None + + def download_img(self) -> bytes: + return do_get(self.img_url).content + + +def get_person_info(name: str, domain: str = "CP") -> Person | None: + full_username = rf"{domain}\{name}" + + url = URL.format(full_username) + rs = do_get(url) + soup = BeautifulSoup(rs.content, "html.parser") + + img_el = soup.select_one("#ctl00_PictureUrlImage") + if not img_el: # Сайт не умеет показывать 404 при отсутствующем пользователе + return + + default_value = "N/A" + + try: + position = get_text(soup.select_one("#ProfileViewer_ValueTitle")) + if not position: + position = default_value + except Exception: + position = default_value + + try: + department = get_text(soup.select_one("#ProfileViewer_ValueDepartment")) + if not department: + department = default_value + except Exception: + department = default_value + + try: + css_path = 'div[id *= "_ProfileViewer_SPS-Location"] > .ms-profile-detailsValue' + location = get_text(soup.select_one(css_path)) + if not location: + location = default_value + except Exception: + location = default_value + + try: + css_path = 'div[id *= "_ProfileViewer_SPS-Birthday"] > .ms-profile-detailsValue' + birthday = get_text(soup.select_one(css_path)) + if not birthday: + birthday = default_value + except Exception: + birthday = default_value + + try: + # NOTE: is_active_profile для mysite не подходит - не всегда оттуда убирают информацию + # В джире эта информация, похоже, всегда актуальная + is_active = get_jira_user_active(name) + except Exception: + try: + is_active = is_active_profile(full_username) + except Exception: + is_active = None + + return Person( + name=name, + position=position, + department=department, + img_url=urljoin(rs.url, img_el["src"]), + location=location, + birthday=birthday, + is_active=is_active, + ) + + +if __name__ == "__main__": + username = "ipetrash" + + info = get_person_info(username) + print(info) + # Person(name='ipetrash', position='Senior Software Engineer', department='TX SPD, Application Platforms Division', img_url='https://portal.compassplus.com/my/User%20Photos/Profile%20Pictures/ipetrash.jpg', location='Magnitogorsk', birthday='August 18', is_active=True) + + import json + from dataclasses import asdict + + print(json.dumps(asdict(info), ensure_ascii=False, indent=4)) + """ + { + "name": "ipetrash", + "position": "Senior Software Engineer", + "department": "TX SPD, Application Platforms Division", + "img_url": "https://portal.compassplus.com/my/User%20Photos/Profile%20Pictures/ipetrash.jpg", + "location": "Magnitogorsk", + "birthday": "August 18", + "is_active": true + } + """ diff --git a/job_compassplus/mysite_compassplus_com/get_profile_image.py b/job_compassplus/mysite_compassplus_com/get_profile_image.py new file mode 100644 index 000000000..01e0e9edf --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_image.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from urllib.parse import urljoin +from bs4 import BeautifulSoup +from common import URL, do_get + + +def get_profile_image(username: str, domain: str = "CP") -> bytes | None: + url = URL.format(fr"{domain}\{username}") + + rs = do_get(url) + + root = BeautifulSoup(rs.content, "html.parser") + img_el = root.select_one("#ctl00_PictureUrlImage") + if not img_el: # Сайт не умеет показывать 404 при отсутствующем пользователе + return + + img_url = urljoin(rs.url, img_el["src"]) + + rs = do_get(img_url) + return rs.content + + +if __name__ == "__main__": + username = "ipetrash" + + img_data = get_profile_image(username) + print(len(img_data), img_data[:20]) + + from pathlib import Path + img_path = Path(__file__).parent / f"{username}.jpg" + img_path.write_bytes(img_data) diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/config.py b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/config.py new file mode 100644 index 000000000..0031cc312 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/config.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + + +DIR = Path(__file__).resolve().parent + +DB_DIR_NAME: Path = DIR / "database" +DB_DIR_NAME.mkdir(parents=True, exist_ok=True) + +DB_FILE_NAME: Path = DB_DIR_NAME / "db.sqlite" + +DIR_DB_BACKUP: Path = DIR / "database-backup" +DIR_DB_BACKUP.mkdir(parents=True, exist_ok=True) + +MAX_LAST_CHECK_DATE_DAYS: int = 30 diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db.py b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db.py new file mode 100644 index 000000000..ca72e6789 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum +import time +import sys + +from datetime import date +from typing import Type, Iterable, Optional, Any + +# pip install peewee +from peewee import ( + Model, + TextField, + ForeignKeyField, + CharField, + DateField, + BlobField, + BooleanField, +) +from playhouse.shortcuts import model_to_dict +from playhouse.sqliteq import SqliteQueueDatabase + +from config import DIR, DB_FILE_NAME + +sys.path.append(str(DIR.parent.parent.parent)) +from shorten import shorten + + +class NotDefinedParameterException(ValueError): + def __init__(self, parameter_name: str) -> None: + self.parameter_name = parameter_name + text = f'Parameter "{self.parameter_name}" must be defined!' + + super().__init__(text) + + +# This working with multithreading +# SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqliteq +db = SqliteQueueDatabase( + DB_FILE_NAME, + pragmas={ + "foreign_keys": 1, + "journal_mode": "wal", # WAL-mode + "cache_size": -1024 * 64, # 64MB page-cache + }, + use_gevent=False, # Use the standard library "threading" module. + autostart=True, + queue_max_size=64, # Max. # of pending writes that can accumulate. + results_timeout=5.0, # Max. time to wait for query to be executed. +) + + +class BaseModel(Model): + class Meta: + database = db + + @classmethod + def get_inherited_models(cls) -> list[Type["BaseModel"]]: + return sorted(cls.__subclasses__(), key=lambda x: x.__name__) + + @classmethod + def print_count_of_tables(cls) -> None: + items = [] + for sub_cls in cls.get_inherited_models(): + name = sub_cls.__name__ + count = sub_cls.select().count() + items.append(f"{name}: {count}") + + print(", ".join(items)) + + @classmethod + def count(cls, filters: Iterable = None) -> int: + query = cls.select() + if filters: + query = query.filter(*filters) + return query.count() + + def to_dict(self) -> dict[str, Any]: + return model_to_dict(self, recurse=False) + + def __str__(self) -> str: + fields = [] + for k, field in self._meta.fields.items(): + v = getattr(self, k) + + if isinstance(field, (TextField, CharField)): + if isinstance(v, enum.Enum): + v = v.value + + if v: + v = repr(shorten(v, length=30)) + + if isinstance(field, BlobField) and v is not None: + v = f"<{len(v)} bytes>" + + elif isinstance(field, ForeignKeyField): + k = f"{k}_id" + if v: + v = v.id + + fields.append(f"{k}={v}") + + return self.__class__.__name__ + "(" + ", ".join(fields) + ")" + + +class Person(BaseModel): + name = TextField() + position = TextField() + department = TextField() + img = BlobField(default=True) + location = TextField() + birthday = TextField() + create_date = DateField(default=date.today) + last_check_date = DateField(default=date.today) + is_active = BooleanField(default=None, null=True) + prev_name = TextField(default=None, null=True) + + class Meta: + indexes = ( + # Уникальный индекс по name и дате создания + (("name", "create_date"), True), + ) + + @classmethod + def get_last_by_name(cls, name: str) -> Optional["Person"]: + items: list[Person] = cls.get_all(name) + return items[0] if items else None + + @classmethod + def get_all(cls, name: str) -> list["Person"]: + prev_names: set[str] = set() + + persons: set[Person] = set() + for p in cls.select().where(cls.name == name): + persons.add(p) + + if p.prev_name: + prev_names.add(p.prev_name) + + for prev_name in prev_names: + if prev_name in persons: + continue + + for p in Person.get_all(prev_name): + persons.add(p) + + return sorted(persons, key=lambda p: p.id, reverse=True) + + @classmethod + def get_all_name(cls) -> list[str]: + return [p.name for p in cls.select(cls.name).distinct()] + + +db.connect() +db.create_tables(BaseModel.get_inherited_models()) + + +# Задержка в 50мс, чтобы дать время на запуск SqliteQueueDatabase и создание таблиц +# Т.к. в SqliteQueueDatabase запросы на чтение выполняются сразу, а на запись попадают в очередь +time.sleep(0.050) + + +if __name__ == "__main__": + BaseModel.print_count_of_tables() + print() + + names: list[str] = Person.get_all_name() + persons: list[Person] = [ + Person.get_last_by_name(name) + for name in names + ] + print(f"Total: {len(persons)}") + print(f"Active: {len([p for p in persons if p.is_active])}") + print(f"Non active: {len([p for p in persons if not p.is_active])}") + + # Поиск людей с одинаковой картинкой в профиле + # from collections import defaultdict + # img_by_persons: defaultdict[bytes, set[str]] = defaultdict(set) + # + # for p in Person: + # img_by_persons[p.img].add(p.name) + # + # for img, persons in sorted(img_by_persons.items(), key=lambda x: len(x[1]), reverse=True): + # if len(persons) > 1: + # print(len(persons), persons) + # diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db_backup.py b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db_backup.py new file mode 100644 index 000000000..3b465039a --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db_backup.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sqlite3 +import traceback +import time +import zipfile + +from datetime import date +from pathlib import Path + +from config import DIR_DB_BACKUP +from db import db + + +# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/b144a9c8fa6e737ef177fe1f7ae07d61794fc037/sqlite3__examples/backup__examples/common.py#L15 +def create_zip_for_file( + file_name_zip: str | Path, + file_name: Path, + delete_file_name: bool = True, +): + with zipfile.ZipFile( + file_name_zip, mode="w", compression=zipfile.ZIP_DEFLATED + ) as f: + f.write(file_name, arcname=file_name.name) + + if delete_file_name: + file_name.unlink() + + +# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/bfac99ef66038562f16c94341535e0ce02f1dec2/sqlite3__examples/backup__examples/backup_via_api.py#L17 +def backup( + connect: sqlite3.Connection, + file_name: Path, + use_zip: bool = True, + delete_file_name_after_zip: bool = True, +) -> Path: + dst = sqlite3.connect(file_name) + connect.backup(dst) + dst.close() + + if use_zip: + file_name_zip = Path(f"{file_name}.zip") + create_zip_for_file( + file_name_zip, file_name, delete_file_name=delete_file_name_after_zip + ) + return file_name_zip + + return file_name + + +def do_backup_db() -> None: + prefix: str = "[do_backup_db]" + + print(f"{prefix} Start") + + while True: + print(f"{prefix} Check") + try: + file_name_backup = backup( + connect=db.connection(), + file_name=DIR_DB_BACKUP / f"{date.today().isoformat()}.sqlite", + ) + print(f"{prefix} Backup saved to {file_name_backup}") + + except Exception: + # Выводим ошибку в консоль + tb = traceback.format_exc() + print(f"{prefix} Error:\n{tb}") + + finally: + time.sleep(30 * 24 * 60 * 60) # Раз в 30 дней + + +if __name__ == "__main__": + do_backup_db() diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db_updater.py b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db_updater.py new file mode 100644 index 000000000..3fc3275cf --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/db_updater.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +import time +import traceback +from datetime import date, timedelta + +import db + +from config import DIR, MAX_LAST_CHECK_DATE_DAYS + +sys.path.append(str(DIR.parent)) +from get_person_info import Person, get_person_info, get_jira_user_active + + +def create_person_from_info(info: Person) -> db.Person: + return db.Person.create( + name=info.name, + position=info.position, + department=info.department, + img=info.download_img(), + location=info.location, + birthday=info.birthday, + is_active=info.is_active, + ) + + +def is_person_eq_info(person: db.Person, info: Person) -> bool: + if person.img != info.download_img(): + return False + + if person.department != info.department: + return False + + if person.position != info.position: + return False + + if person.location != info.location: + return False + + if person.birthday != info.birthday: + return False + + if person.is_active != info.is_active: + return False + + return True + + +def is_need_to_check(person: db.Person, d: date) -> bool: + return d > person.last_check_date + timedelta(days=MAX_LAST_CHECK_DATE_DAYS) + + +def add_or_get_db(name: str, forced: bool = False) -> db.Person | None: + person = db.Person.get_last_by_name(name) + + # Если нет, то создать + if not person: + info = get_person_info(name) + if not info: + return + + # Пусть первые пользователи всегда будут активными + info.is_active = True + + return create_person_from_info(info) + + # Проверить дату проверку + # Если с даты последней проверки прошло больше MAX_LAST_CHECK_DATE_DAYS дней, то + # нужно проверить изменения полей + today = date.today() + if is_need_to_check(person, d=today) or forced: + info = get_person_info(name) + if info: + if is_person_eq_info(person, info): + person.last_check_date = today + person.save() + else: + # Создание новой записи с актуальными полями + person = create_person_from_info(info) + + elif person.is_active: # Если пользователь был в БД и активным, а потом его удалили из mysite + # Создание новой записи + return db.Person.create( + name=person.name, + position=person.position, + department=person.department, + img=person.img, + location=person.location, + birthday=person.birthday, + is_active=False, + ) + + return person + + +def do_update_db(forced: bool = False) -> None: + prefix: str = "[do_update_db]" + + print(f"{prefix} Start") + + while True: + print(f"{prefix} Check all") + try: + # Запрос для получения ников + names: list[str] = db.Person.get_all_name() + for i, name in enumerate(names, 1): + print(f"{prefix} Check {name} ({i}/{len(names)})") + + person: db.Person | None = None + try: + person = add_or_get_db(name, forced=forced) + except Exception as e: + print(f"{prefix} Error: {e}") + finally: + # Оптимизация, чтобы не делать лишней задержку, если не было запроса по сети в add_or_get_db + # (это косвенно можно понять по last_check_date) + if not forced and person and not is_need_to_check(person, d=date.today()): + continue + + time.sleep(10) + + except Exception: + # Выводим ошибку в консоль + tb = traceback.format_exc() + print(f"{prefix} Error:\n{tb}") + + finally: + time.sleep(24 * 60 * 60) # Раз в сутки + + +if __name__ == "__main__": + user_name = "akrylov" + print(get_person_info(user_name)) + print(add_or_get_db(user_name, forced=True)) + + # do_update_db(forced=True) diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/.gitignore b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/.gitignore new file mode 100644 index 000000000..41bca1bd2 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/.gitignore @@ -0,0 +1,3 @@ +cert.pem +key.pem +keyStore.p12 \ No newline at end of file diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/create_p12_from_self_signed_cert.bat b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/create_p12_from_self_signed_cert.bat new file mode 100644 index 000000000..a540f6036 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/create_p12_from_self_signed_cert.bat @@ -0,0 +1 @@ +openssl pkcs12 -export -out keyStore.p12 -inkey key.pem -in cert.pem \ No newline at end of file diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/create_self_signed_cert.bat b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/create_self_signed_cert.bat new file mode 100644 index 000000000..4bbe769e3 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/create_self_signed_cert.bat @@ -0,0 +1 @@ +openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 1095 -config openssl.cnf \ No newline at end of file diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/openssl.cnf b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/openssl.cnf new file mode 100644 index 000000000..4d23e835a --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/for_https/openssl.cnf @@ -0,0 +1,72 @@ +# +# OpenSSL configuration file. +# + +# Establish working directory. + +dir = . + +[ ca ] +default_ca = CA_default + +[ CA_default ] +serial = $dir/serial +database = $dir/certindex.txt +new_certs_dir = $dir/certs +certificate = $dir/cacert.pem +private_key = $dir/private/cakey.pem +default_days = 365 +default_md = md5 +preserve = no +email_in_dn = no +nameopt = default_ca +certopt = default_ca +policy = policy_match + +[ policy_match ] +countryName = match +stateOrProvinceName = match +organizationName = match +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[ req ] +default_bits = 2048 # Size of keys +default_keyfile = key.pem # name of generated keys +default_md = md5 # message digest algorithm +string_mask = nombstr # permitted characters +distinguished_name = req_distinguished_name +req_extensions = v3_req + +[ req_distinguished_name ] +# Variable name Prompt string +#------------------------- ---------------------------------- +0.organizationName = Organization Name (company) +organizationalUnitName = Organizational Unit Name (department, division) +emailAddress = Email Address +emailAddress_max = 40 +localityName = Locality Name (city, district) +stateOrProvinceName = State or Province Name (full name) +countryName = Country Name (2 letter code) +countryName_min = 2 +countryName_max = 2 +commonName = Common Name (hostname, IP, or your name) +commonName_max = 64 + +# Default values for the above, for consistency and less typing. +# Variable name Value +#------------------------ ------------------------------ +0.organizationName_default = My Company +localityName_default = My Town +stateOrProvinceName_default = State or Providence +countryName_default = US + +[ v3_ca ] +basicConstraints = CA:TRUE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer:always + +[ v3_req ] +basicConstraints = CA:FALSE +subjectKeyIdentifier = hash diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/main.py b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/main.py new file mode 100644 index 000000000..b213e8a46 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/main.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import base64 +import os +from datetime import date, datetime + +# pip install flask==2.3.3 +from flask import Flask, Response, abort, jsonify +from flask.json.provider import DefaultJSONProvider + +# pip install flask-cors==4.0.0 +from flask_cors import CORS + +from requests.exceptions import RequestException + +import db +from db_updater import add_or_get_db + + +config = { + # "DEBUG": True, # some Flask specific configs + "CACHE_TYPE": "SimpleCache", # Flask-Caching related configs + "CACHE_DEFAULT_TIMEOUT": 300, +} + + +class UpdatedJSONProvider(DefaultJSONProvider): + sort_keys = False + + def default(self, o): + if isinstance(o, (date, datetime)): + return o.isoformat() + return super().default(o) + + +app = Flask(__name__) + +# Tell Flask to use the above defined config +app.config.from_mapping(config) + +app.json = UpdatedJSONProvider(app) + +CORS(app) + + +@app.errorhandler(RequestException) +def handle_requests_error(e: RequestException) -> Response: + return Response( + response=str(e), + status=e.response.status_code if e.response else 404, + ) + + +@app.route("/api/get_all_person_info/") +def api_get_all_person_info(username: str) -> tuple[Response, int]: + has_person: bool = db.Person.get_last_by_name(username) is not None + + # Проверка наличия и попытка добавить в базу для первого раза + person: db.Person = add_or_get_db(username) + if not person: + abort(404) + + img_by_idx: dict[str, int] = dict() + items: list[dict] = [] + + for i, person in enumerate(db.Person.get_all(username)): + data = person.to_dict() + + # NOTE: Картинки возвращаются в одном запросе + data_base64 = base64.b64encode(person.img).decode("utf-8") + img_base64 = f"data:image/jpg;base64,{data_base64}" + + # Оптимизация, чтобы не возвращать одинаковые картинки + if img_base64 not in img_by_idx: + img_by_idx[img_base64] = i + else: + img_base64 = f"={img_by_idx[img_base64]}" # Символа "=" нет в base64 + + data["img"] = img_base64 + + items.append(data) + + return jsonify(items), 200 if has_person else 201 + + +if __name__ == "__main__": + from threading import Thread + from db_backup import do_backup_db + from db_updater import do_update_db + + Thread(target=do_backup_db, daemon=True).start() + Thread(target=do_update_db, daemon=True).start() + + app.run( + host="0.0.0.0", + port=int(os.environ.get("FLASK_PORT", 50000)), + # TODO: for https + # ssl_context=("for_https/cert.pem", "for_https/key.pem"), + ) diff --git a/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/migrations/001.py b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/migrations/001.py new file mode 100644 index 000000000..c428fa821 --- /dev/null +++ b/job_compassplus/mysite_compassplus_com/get_profile_info_from_web/migrations/001.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#schema-migrations + + +from playhouse.migrate import SqliteDatabase, SqliteMigrator, migrate +from db import TextField, DB_FILE_NAME, Person + + +db = SqliteDatabase(DB_FILE_NAME) +migrator = SqliteMigrator(db) + + +with db.atomic(): + migrate( + migrator.add_column( + Person._meta.table_name, + "prev_name", + TextField(default=None, null=True), + ), + ) diff --git a/job_compassplus/outlook_jira_issues_autoread.py b/job_compassplus/outlook_jira_issues_autoread.py new file mode 100644 index 000000000..eeeecee44 --- /dev/null +++ b/job_compassplus/outlook_jira_issues_autoread.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import argparse +import email +import imaplib +import re +import time + +from dataclasses import dataclass +from datetime import datetime, timedelta +from email.header import decode_header +from email.utils import parseaddr +from typing import Any + +from root_config import JIRA_HOST +from root_common import session + + +PATTERN_JIRA: re.Pattern = re.compile(r"\((\w+-\d+)\)") + + +@dataclass +class IssueInfo: + key: str + is_done: bool + updated: datetime | None + resolution_date: datetime | None + + +def get_issue_info(issue_key: str) -> IssueInfo: + def _get_datetime(value: str) -> datetime | None: + if value: + return datetime.fromisoformat(value).replace(tzinfo=None) + + url_issue: str = f"{JIRA_HOST}/rest/api/latest/issue/{issue_key}" + + rs = session.get(url_issue) + rs.raise_for_status() + + rs_fields: dict[str, Any] = rs.json()["fields"] + + return IssueInfo( + key=issue_key, + is_done=rs_fields["status"]["statusCategory"]["key"] == "done", + updated=_get_datetime(rs_fields.get("updated")), + resolution_date=_get_datetime(rs_fields.get("resolutiondate")), + ) + + +def get_args(): + parser = argparse.ArgumentParser( + description="Скрипт для автоматического прочтения неактуальных писем из Jira в Outlook." + ) + + # Обязательные параметры + parser.add_argument( + "-u", + "--user", + required=True, + help="Логин (email) от почты", + ) + parser.add_argument( + "-p", + "--password", + required=True, + help="Пароль от почты (или пароль приложения)", + ) + parser.add_argument( + "-f", + "--folder", + default="INBOX", + help="Папка для поиска (например, INBOX или 'Jira/Issues', по умолчанию: %(default)s))", + ) + + # Опциональные параметры + parser.add_argument( + "--server", + default="mail.compassplus.com", + help="Адрес IMAP сервера (по умолчанию: %(default)s)", + ) + + parser.add_argument( + "--limit", + type=int, + default=None, + help="Ограничение количества обрабатываемых писем (для тестирования, например 100)", + ) + + # Флаг прочтения (action='store_true' делает его булевым: если указан - True, если нет - False) + parser.add_argument( + "--mark", + action="store_true", + help="Если указан, письма при совпадении условий будут отмечены как прочитанные на сервере", + ) + + return parser.parse_args() + + +def process( + user: str, + password: str, + server: str, + folder: str, + limit: int | None = None, + mark_as_read: bool = False, +) -> None: + print(f"Подключение к {server} для пользователя {user}...") + + mail = imaplib.IMAP4_SSL(server) + mail.login(user, password) + + mail.select(f'"{folder}"') + + # Письма от старых к новым + status, response = mail.search(None, "(UNSEEN)") + + issue_by_info: dict[str, Any] = dict() + + msg_ids = response[0].split() + if limit: + print(f"Лимит обработки: {limit} писем") + msg_ids = msg_ids[:limit] + + for num in msg_ids: + # BODY.PEEK[] - читаем, не меняя статус на "Прочитано" + status, data = mail.fetch(num, "(BODY.PEEK[HEADER])") + raw_email: bytes = data[0][1] + msg = email.message_from_bytes(raw_email) + + # Декодируем заголовок + decoded_parts = decode_header(msg["Subject"]) + + subject = "" + for content, encoding in decoded_parts: + if isinstance(content, bytes): + # Используем кодировку из письма, если её нет — пробуем utf-8 + enc = encoding if encoding else "utf-8" + try: + subject += content.decode(enc) + except (UnicodeDecodeError, LookupError): + # Если всё равно ошибка, декодируем с заменой битых символов + subject += content.decode("utf-8", errors="replace") + else: + # Если это уже строка (например, ASCII) + subject += content + + raw_from: str = msg.get("From") + name, email_address = parseaddr(raw_from) + + raw_date: str = msg.get("Date") + date_tuple = email.utils.parsedate_tz(raw_date) + local_date: datetime | None = None + if date_tuple: + local_date = datetime.fromtimestamp( + email.utils.mktime_tz(date_tuple) + ).replace(tzinfo=None) + else: + print(f"[#] Неправильная дата письма: {raw_date!r}") + + read_it: bool = False + + # NOTE: Можно оптимизировать и не ходить в API Jira, если письмо старше такой-то даты + # Но хочется посмотреть информацию по задачам из писем + m: re.Match | None = PATTERN_JIRA.search(subject) + if m: + issue_key: str = m.group(1) + print( + "[+]", + num, + f"{local_date.strftime('%d.%m.%Y %H:%M:%S') if local_date else None}", + repr(subject), + repr(name), + email_address, + issue_key, + ) + + issue: IssueInfo | None = issue_by_info.get(issue_key) + if not issue: + try: + issue = get_issue_info(issue_key) + issue_by_info[issue_key] = issue + except Exception as e: + print(f"[#] Не удалось получить информацию по задаче {issue_key!r}: {e}") + + time.sleep(1) + + print(f" Информация по задаче: {issue if issue else '<неизвестно>'}") + + if issue: + # Определяем запас времени (например, 10 минут) + buffer = timedelta(minutes=10) + + # Если письма приходили до даты решения задачи, то уже не актуальные + if ( + issue.is_done + and issue.resolution_date + and local_date + # Если дата решения задачи больше даты отправки письма + and (issue.resolution_date - buffer) > local_date + ): + print(" Письмо приходило раньше решения задачи") + read_it = True + + else: + print( + "[?]", + num, + f"{local_date.strftime('%d.%m.%Y %H:%M:%S') if local_date else None}", + repr(subject), + repr(name), + email_address, + ) + + # Если прошло больше 1 месяца + if local_date and (datetime.now() - timedelta(weeks=4)) > local_date: + print(" Прошло больше 1 месяца") + read_it = True + + if read_it: + status_msg = ( + "[ПРОЧИТАНО]" + if mark_as_read + else "[DRY-RUN: БЫЛО БЫ ПРОЧИТАНО С ФЛАГОМ --mark]" + ) + print(f" {status_msg}") + + if mark_as_read: + mail.store(num, "+FLAGS", r"\Seen") + + mail.logout() + + +if __name__ == "__main__": + args = get_args() + + process( + user=args.user, + password=args.password, + server=args.server, + folder=args.folder, + limit=args.limit, + mark_as_read=args.mark, + ) diff --git a/parse__radix__CacheContent_log/CacheContent_log.txt b/job_compassplus/parse__radix__CacheContent_log/CacheContent_log.txt similarity index 100% rename from parse__radix__CacheContent_log/CacheContent_log.txt rename to job_compassplus/parse__radix__CacheContent_log/CacheContent_log.txt diff --git a/job_compassplus/parse__radix__CacheContent_log/main.py b/job_compassplus/parse__radix__CacheContent_log/main.py new file mode 100644 index 000000000..d70a7bed9 --- /dev/null +++ b/job_compassplus/parse__radix__CacheContent_log/main.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +__author__ = "ipetrash" + + +import re + + +def get_cache_object_info(file_name) -> dict[str, dict[str, int]]: + # new_object: Tran[180409000010273756] + # existing_object: Hold[13708] + pattern_object = re.compile(r"^(\w+_object): (.+)\[.+$") + + object_by_number = dict() + + for line in open(file_name): + match = pattern_object.search(line) + if match: + type_cache = match[1] + name = match[2] + + if type_cache not in object_by_number: + object_by_number[type_cache] = dict() + + if name not in object_by_number[type_cache]: + object_by_number[type_cache][name] = 0 + + object_by_number[type_cache][name] += 1 + + return object_by_number + + +def get_cache_existing_object_list( + file_name, top_values=None +) -> list[tuple[str, int]]: + object_by_number = get_cache_object_info(file_name) + + existing_object_list = sorted( + object_by_number["existing_object"].items(), key=lambda x: x[1], reverse=True + ) + if top_values: + existing_object_list = existing_object_list[:top_values] + + rows = [(name, number) for name, number in existing_object_list] + return rows + + +if __name__ == "__main__": + file_name = "CacheContent_log.txt" + print(file_name) + + rows = get_cache_existing_object_list(file_name, top_values=5) + + # pip install tabulate + from tabulate import tabulate + print(tabulate(rows, headers=("NAME", "NUMBER"), tablefmt="grid")) diff --git a/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/common.py b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/common.py index 29ee1a04a..a653ed575 100644 --- a/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/common.py +++ b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/common.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import logging @@ -9,22 +9,23 @@ from logging.handlers import RotatingFileHandler from pathlib import Path -from typing import Dict DIR = Path(__file__).resolve().parent ROOT_DIR = DIR.parent -# For import ascii_table__simple_pretty__ljust.py -sys.path.append(str(ROOT_DIR.parent)) -from ascii_table__simple_pretty__ljust import pretty_table +# pip install tabulate +from tabulate import tabulate -def get_table(assigned_open_issues_per_project: Dict[str, int]) -> str: - data = [("PROJECT", 'Issues')] + list(assigned_open_issues_per_project.items()) - return pretty_table(data) +def get_table(assigned_open_issues_per_project: dict[str, int]) -> str: + return tabulate( + list(assigned_open_issues_per_project.items()), + headers=("PROJECT", "Issues"), + tablefmt="grid", + ) -def print_table(assigned_open_issues_per_project: Dict[str, int]): +def print_table(assigned_open_issues_per_project: dict[str, int]) -> None: print(get_table(assigned_open_issues_per_project)) # PROJECT | Issues # --------+------- @@ -33,14 +34,20 @@ def print_table(assigned_open_issues_per_project: Dict[str, int]): # zzz | 3 -def get_logger(name, file='log.txt', encoding='utf-8', log_stdout=True, log_file=True) -> logging.Logger: +def get_logger( + name, file="log.txt", encoding="utf-8", log_stdout=True, log_file=True +) -> logging.Logger: log = logging.getLogger(name) log.setLevel(logging.DEBUG) - formatter = logging.Formatter('[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s') + formatter = logging.Formatter( + "[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s" + ) if log_file: - fh = RotatingFileHandler(file, maxBytes=10000000, backupCount=5, encoding=encoding) + fh = RotatingFileHandler( + file, maxBytes=10_000_000, backupCount=5, encoding=encoding + ) fh.setFormatter(formatter) log.addHandler(fh) @@ -52,4 +59,4 @@ def get_logger(name, file='log.txt', encoding='utf-8', log_stdout=True, log_file return log -logger = get_logger('parse_jira_Assigned_Open_Issues_per_Project') +logger = get_logger("parse_jira_Assigned_Open_Issues_per_Project") diff --git a/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/db.py b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/db.py index ebaead338..5b6f7fe69 100644 --- a/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/db.py +++ b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/db.py @@ -1,17 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt import os import shutil import sys -from typing import Dict, Optional - -from peewee import SqliteDatabase, Model, TextField, CharField, ForeignKeyField, DateField, IntegerField +from peewee import ( + SqliteDatabase, + Model, + TextField, + CharField, + ForeignKeyField, + DateField, + IntegerField, +) from common import ROOT_DIR, DIR, print_table @@ -19,28 +25,29 @@ sys.path.append(str(ROOT_DIR.parent)) from shorten import shorten + # Absolute file name -DB_FILE_NAME = str(DIR / 'database.sqlite') +DB_FILE_NAME = str(DIR / "database.sqlite") -def db_create_backup(backup_dir=DIR / 'backup'): +def db_create_backup(backup_dir=DIR / "backup") -> None: os.makedirs(backup_dir, exist_ok=True) - file_name = str(DT.datetime.today().date()) + '.sqlite' + file_name = str(dt.datetime.today().date()) + ".sqlite" file_name = os.path.join(backup_dir, file_name) shutil.copy(DB_FILE_NAME, file_name) # Ensure foreign-key constraints are enforced. -db = SqliteDatabase(DB_FILE_NAME, pragmas={'foreign_keys': 1}) +db = SqliteDatabase(DB_FILE_NAME, pragmas={"foreign_keys": 1}) class BaseModel(Model): class Meta: database = db - def __str__(self): + def __str__(self) -> str: fields = [] for k, field in self._meta.fields.items(): v = getattr(self, k) @@ -50,26 +57,26 @@ def __str__(self): v = repr(shorten(v)) elif isinstance(field, ForeignKeyField): - k = f'{k}_id' + k = f"{k}_id" if v: v = v.id - fields.append(f'{k}={v}') + fields.append(f"{k}={v}") - return self.__class__.__name__ + '(' + ', '.join(fields) + ')' + return self.__class__.__name__ + "(" + ", ".join(fields) + ")" class Run(BaseModel): - date = DateField(default=DT.date.today) + date = DateField(default=dt.date.today) def get_total_issues(self) -> int: return sum(x.value for x in self.issue_numbers) - def get_project_by_issue_numbers(self) -> Dict[str, int]: + def get_project_by_issue_numbers(self) -> dict[str, int]: return {issue.project.name: issue.value for issue in self.issue_numbers} - def __str__(self): - return f'{self.__class__.__name__}(id={self.id}, date={self.date}, total_issues={self.get_total_issues()})' + def __str__(self) -> str: + return f"{self.__class__.__name__}(id={self.id}, date={self.date}, total_issues={self.get_total_issues()})" class Project(BaseModel): @@ -78,27 +85,19 @@ class Project(BaseModel): class IssueNumber(BaseModel): value = IntegerField() - run = ForeignKeyField(Run, backref='issue_numbers') - project = ForeignKeyField(Project, backref='issue_numbers') - + run = ForeignKeyField(Run, backref="issue_numbers") + project = ForeignKeyField(Project, backref="issue_numbers") -def add(assigned_open_issues_per_project: Dict[str, int]) -> Optional[bool]: - last_run = Run.select().order_by(Run.id.desc()).get() - if assigned_open_issues_per_project == last_run.get_project_by_issue_numbers(): - return - run, created = Run.get_or_create(date=DT.date.today()) +def add(assigned_open_issues_per_project: dict[str, int]) -> bool: + run, created = Run.get_or_create(date=dt.date.today()) if not created: return False for project_name, issue_numbers in assigned_open_issues_per_project.items(): project, _ = Project.get_or_create(name=project_name) - IssueNumber.create( - value=issue_numbers, - run=run, - project=project - ) + IssueNumber.create(value=issue_numbers, run=run, project=project) db_create_backup() @@ -109,13 +108,13 @@ def add(assigned_open_issues_per_project: Dict[str, int]) -> Optional[bool]: db.create_tables([Run, Project, IssueNumber]) -if __name__ == '__main__': +if __name__ == "__main__": projects = [p.name for p in Project.select()] print(f"Projects ({len(projects)}): {projects}\n") # Print last rows for run in Run.select().order_by(Run.id.desc()).limit(5): - print(run, '\n') + print(run) print_table(run.get_project_by_issue_numbers()) - print('\n' + '-' * 100 + '\n') + print("\n" + "-" * 100 + "\n") diff --git a/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/get_assigned_open_issues_per_project.py b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/get_assigned_open_issues_per_project.py index 0611899be..88dcfc1cd 100644 --- a/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/get_assigned_open_issues_per_project.py +++ b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/get_assigned_open_issues_per_project.py @@ -1,42 +1,44 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys -from typing import Dict from bs4 import BeautifulSoup from common import ROOT_DIR, print_table sys.path.append(str(ROOT_DIR)) +from root_config import JIRA_HOST from root_common import session -URL = 'https://jira.compassplus.ru/secure/ViewProfile.jspa?name=ipetrash' +URL = f"{JIRA_HOST}/secure/ViewProfile.jspa?name=ipetrash" -def get_assigned_open_issues_per_project() -> Dict[str, int]: +def get_assigned_open_issues_per_project() -> dict[str, int]: rs = session.get(URL) - root = BeautifulSoup(rs.content, 'html.parser') + rs.raise_for_status() + + root = BeautifulSoup(rs.content, "html.parser") data = dict() - for item in root.select('#assigned-and-open > .mod-content > .stat-list > li'): - name = item.select_one('a[title]').get_text(strip=True) - value = int(item.select_one('.stat').get_text(strip=True)) + for item in root.select("#assigned-and-open > .mod-content > .stat-list > li"): + name = item.select_one("a[title]").get_text(strip=True) + value = int(item.select_one(".stat").get_text(strip=True)) data[name] = value return data -def get_and_prints() -> Dict[str, int]: +def get_and_prints() -> dict[str, int]: assigned_open_issues_per_project = get_assigned_open_issues_per_project() # {'xxx': 1, 'yyy': 2, 'zzz': 3} - print('Total issues:', sum(assigned_open_issues_per_project.values())) + print("Total issues:", sum(assigned_open_issues_per_project.values())) print() print_table(assigned_open_issues_per_project) @@ -49,5 +51,5 @@ def get_and_prints() -> Dict[str, int]: return assigned_open_issues_per_project -if __name__ == '__main__': +if __name__ == "__main__": get_and_prints() diff --git a/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/gui.py b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/gui.py index d130abaeb..11dc03b4d 100644 --- a/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/gui.py +++ b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/gui.py @@ -1,22 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import calendar -import datetime as DT import math import sys import time import traceback -from typing import List, Dict +from datetime import datetime, date, timedelta from PyQt5.QtWidgets import ( - QApplication, QMessageBox, QMainWindow, QSystemTrayIcon, QTabWidget, QTableWidget, - QTableWidgetItem, QVBoxLayout, QWidget, QPushButton, QSplitter, QLabel, QGridLayout, - QHeaderView, QProgressBar, QMenu + QApplication, + QMessageBox, + QMainWindow, + QSystemTrayIcon, + QTabWidget, + QTableWidget, + QTableWidgetItem, + QVBoxLayout, + QWidget, + QPushButton, + QSplitter, + QLabel, + QGridLayout, + QHeaderView, + QProgressBar, + QMenu, + QComboBox, ) from PyQt5.QtGui import QIcon, QPainter, QCloseEvent from PyQt5.QtCore import QEvent, QTimer, Qt, QThread, pyqtSignal @@ -26,23 +39,44 @@ from get_assigned_open_issues_per_project import get_assigned_open_issues_per_project from db import Run -sys.path.append(str(ROOT_DIR.parent / 'qt__pyqt__pyside__pyqode')) +sys.path.append(str(ROOT_DIR.parent / "qt__pyqt__pyside__pyqode")) from chart_line__show_tooltip_on_series__QtChart import ChartViewToolTips -def log_uncaught_exceptions(ex_cls, ex, tb): - text = f'{ex_cls.__name__}: {ex}:\n' - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) sys.excepthook = log_uncaught_exceptions -WINDOW_TITLE = DIR.name +WINDOW_TITLE: str = DIR.name + +DATE_FORMAT: str = "%d.%m.%Y" +TIME_FORMAT: str = "%H:%M:%S" + + +def get_human_datetime(dt: datetime | None = None) -> str: + if not dt: + dt = datetime.now() + return dt.strftime(f"{DATE_FORMAT} {TIME_FORMAT}") + + +def get_human_date(d: datetime | date | None = None) -> str: + if not d: + d = date.today() + return d.strftime(DATE_FORMAT) + + +def get_human_time(dt: datetime | None = None) -> str: + if not dt: + dt = datetime.now() + return dt.strftime(TIME_FORMAT) def get_table_widget(header_labels: list) -> QTableWidget: @@ -61,9 +95,9 @@ class GetAssignedOpenIssuesPerProjectThread(QThread): about_items = pyqtSignal(dict) about_error = pyqtSignal(Exception) - def run(self): + def run(self) -> None: try: - items = get_assigned_open_issues_per_project() + items: dict[str, int] = get_assigned_open_issues_per_project() self.about_items.emit(items) # Даем время на отображение и анимацию прогресс-бара @@ -74,13 +108,15 @@ def run(self): class TableWidgetRun(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.table_run = get_table_widget(['DATE', 'TOTAL ISSUES']) - self.table_run.selectionModel().selectionChanged.connect(self._on_table_run_item_clicked) + self.table_run = get_table_widget(["DATE", "TOTAL ISSUES"]) + self.table_run.selectionModel().selectionChanged.connect( + self._on_table_run_item_clicked + ) - self.table_issues = get_table_widget(['PROJECT', 'NUMBER']) + self.table_issues = get_table_widget(["PROJECT", "NUMBER"]) self.table_run.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.table_issues.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) @@ -93,7 +129,7 @@ def __init__(self): self.setLayout(main_layout) - def refresh(self, items: List[Run]): + def refresh(self, items: list[Run]) -> None: # Удаление строк таблицы while self.table_run.rowCount(): self.table_run.removeRow(0) @@ -101,7 +137,7 @@ def refresh(self, items: List[Run]): for i, run in enumerate(items): self.table_run.setRowCount(self.table_run.rowCount() + 1) - item = QTableWidgetItem(run.date.strftime('%d/%m/%Y')) + item = QTableWidgetItem(get_human_date(run.date)) item.setData(Qt.UserRole, run.get_project_by_issue_numbers()) self.table_run.setItem(i, 0, item) @@ -112,7 +148,7 @@ def refresh(self, items: List[Run]): self.table_run.setFocus() self._on_table_run_item_clicked() - def _on_table_run_item_clicked(self): + def _on_table_run_item_clicked(self) -> None: # Удаление строк таблицы while self.table_issues.rowCount(): self.table_issues.removeRow(0) @@ -129,10 +165,10 @@ def _on_table_run_item_clicked(self): class CurrentAssignedOpenIssues(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.table = get_table_widget(['PROJECT', 'NUMBER']) + self.table = get_table_widget(["PROJECT", "NUMBER"]) self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.progress_bar = QProgressBar() @@ -156,7 +192,9 @@ def __init__(self): main_layout = QGridLayout() main_layout.addWidget(self.label_total, 0, 0, Qt.AlignLeft | Qt.AlignCenter) main_layout.addWidget(self.progress_bar, 0, 1) - main_layout.addWidget(self.label_last_refresh_date, 0, 2, Qt.AlignRight | Qt.AlignCenter) + main_layout.addWidget( + self.label_last_refresh_date, 0, 2, Qt.AlignRight | Qt.AlignCenter + ) main_layout.addWidget(self.table, 1, 0, 2, 0) self.setLayout(main_layout) @@ -167,97 +205,106 @@ def __init__(self): self.thread.about_items.connect(self._on_set_items) self.thread.about_error.connect(self._on_error) - self._update_total_issues('-') + self.current_items: dict[str, int] | None = None + self._update_total_issues("-") - def _update_total_issues(self, value): - self.label_total.setText(f'Total issues: {value}') + def _update_total_issues(self, value) -> None: + self.label_total.setText(f"Total issues: {value}") - def _on_set_items(self, items: Dict[str, int]): - self._update_total_issues(sum(items.values())) + def _on_set_items(self, items: dict[str, int]) -> None: + self.current_items = items + + self._update_total_issues(sum(self.current_items.values())) # Удаление строк таблицы while self.table.rowCount(): self.table.removeRow(0) - for i, (project_name, number) in enumerate(items.items()): + for i, (project_name, number) in enumerate(self.current_items.items()): self.table.setRowCount(self.table.rowCount() + 1) self.table.setItem(i, 0, QTableWidgetItem(project_name)) self.table.setItem(i, 1, QTableWidgetItem(str(number))) self.label_last_refresh_date.setText( - "Last refresh date: " + DT.datetime.now().strftime('%d/%m/%Y %H:%M:%S') + f"Last refresh date: {get_human_datetime()}" ) - def _on_error(self, e: Exception): + def _on_error(self, e: Exception) -> None: tb_str = "".join(traceback.format_tb(e.__traceback__)) print(tb_str) - QMessageBox.warning(self, 'ERROR', str(e)) + QMessageBox.warning(self, "ERROR", str(e)) - def refresh(self): - if self.thread.isRunning(): - return + def refresh(self) -> None: + self.current_items = None self.thread.start() class MyChartViewToolTips(ChartViewToolTips): - def __init__(self, timestamp_by_run: dict): + def __init__(self, timestamp_by_info: dict[int, dict[str, int]]) -> None: super().__init__() - self._callout_font_family = 'Courier' - self.timestamp_by_run = timestamp_by_run + self._callout_font_family = "Courier" + self.timestamp_by_info: dict[int, dict[str, int]] = timestamp_by_info - def show_series_tooltip(self, point, state: bool): + def show_series_tooltip(self, point, state: bool) -> None: # value -> pos point = self.chart().mapToPosition(point) if not self._tooltip: self._tooltip = self._add_Callout() - if state: - distance = 25 - - for series in self.chart().series(): - for p_value in series.pointsVector(): - p = self.chart().mapToPosition(p_value) + if not state: + self._tooltip.hide() + return - current_distance = math.sqrt( - (p.x() - point.x()) * (p.x() - point.x()) - + (p.y() - point.y()) * (p.y() - point.y()) + distance = 25 + + for series in self.chart().series(): + for p_value in series.pointsVector(): + p = self.chart().mapToPosition(p_value) + + current_distance = math.sqrt( + (p.x() - point.x()) * (p.x() - point.x()) + + (p.y() - point.y()) * (p.y() - point.y()) + ) + + if current_distance < distance: + time_ms = int(p_value.x()) + info: dict[str, int] = self.timestamp_by_info[time_ms] + table = get_table(info) + text = ( + f"{get_human_date(date.fromtimestamp(time_ms / 1000))}" + "\n\n" + f"Total issues: {sum(info.values())}" + "\n" + f"{table}" ) - if current_distance < distance: - date_value = int(p_value.x()) - run = self.timestamp_by_run[date_value] - table = get_table(run.get_project_by_issue_numbers()) - text = f"Total issues: {run.get_total_issues()}\n\n{table}" - - self._tooltip.setText(text) - self._tooltip.setAnchor(p_value) - self._tooltip.setZValue(11) - self._tooltip.updateGeometry() - self._tooltip.show() - else: - self._tooltip.hide() + self._tooltip.setText(text) + self._tooltip.setAnchor(p_value) + self._tooltip.setZValue(11) + self._tooltip.updateGeometry() + self._tooltip.show() class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle(WINDOW_TITLE) - file_name = str(DIR / 'favicon.ico') + file_name = str(DIR / "favicon.ico") icon = QIcon(file_name) self.setWindowIcon(icon) - self.timestamp_by_run = dict() + self.timestamp_by_info: dict[int, dict[str, int]] = dict() menu = QMenu() - menu.addAction('Show / hide', (lambda: self.setVisible(not self.isVisible()))) + menu.addAction("Show / hide", (lambda: self.setVisible(not self.isVisible()))) menu.addSeparator() - menu.addAction('Quit', QApplication.instance().quit) + menu.addAction("Quit", QApplication.instance().quit) self.tray = QSystemTrayIcon(icon) self.tray.setContextMenu(menu) @@ -265,20 +312,31 @@ def __init__(self): self.tray.activated.connect(self._on_tray_activated) self.tray.show() - self.chart_view = MyChartViewToolTips(self.timestamp_by_run) + self.chart_view = MyChartViewToolTips(self.timestamp_by_info) self.chart_view.setRenderHint(QPainter.Antialiasing) + self.cb_chart_filter = QComboBox() + self.cb_chart_filter.addItem("", userData=0) + self.cb_chart_filter.activated.connect(self.refresh) + layout_chart_view = QVBoxLayout(self.chart_view) + layout_chart_view.addWidget(self.cb_chart_filter) + layout_chart_view.setAlignment( + self.cb_chart_filter, Qt.AlignTop | Qt.AlignRight + ) + self.table_run = TableWidgetRun() self.table_run.layout().setContentsMargins(0, 0, 0, 0) self.current_assigned_open_issues = CurrentAssignedOpenIssues() self.tab_widget = QTabWidget() - self.tab_widget.addTab(self.chart_view, 'CHART') - self.tab_widget.addTab(self.table_run, 'TABLE RUN') - self.tab_widget.addTab(self.current_assigned_open_issues, 'Current Assigned Open Issues') + self.tab_widget.addTab(self.chart_view, "CHART") + self.tab_widget.addTab(self.table_run, "TABLE RUN") + self.tab_widget.addTab( + self.current_assigned_open_issues, "Current Assigned Open Issues" + ) - self.pb_refresh = QPushButton('REFRESH') + self.pb_refresh = QPushButton("REFRESH") self.pb_refresh.clicked.connect(self.refresh) main_layout = QVBoxLayout() @@ -291,30 +349,57 @@ def __init__(self): self.setCentralWidget(central_widget) @staticmethod - def _get_timegm(date: DT.date) -> int: + def _get_timegm(date: date) -> int: return calendar.timegm(date.timetuple()) * 1000 - def _get_datetime(self, date: DT.date, delta: DT.timedelta = None) -> DT.datetime: - dt = DT.datetime.combine(date, DT.datetime.min.time()) + def _get_datetime(self, date: date, delta: timedelta = None) -> datetime: + dt = datetime.combine(date, datetime.min.time()) if delta: dt += delta return dt - def _fill_chart(self, items: List[Run]): + def _fill_chart_filter(self, items: list[Run]) -> None: + years: list[int] = sorted({run.date.year for run in items}) + filter_years: list[int] = [ + self.cb_chart_filter.itemData(i) + for i in range(self.cb_chart_filter.count()) + ] + for year in years: + if year not in filter_years: + self.cb_chart_filter.addItem(f"{year}", userData=year) + + def _fill_chart(self, items: list[Run]) -> None: + # Фильтрация данных из графика + year: int = self.cb_chart_filter.currentData() + if year: + items = [run for run in items if run.date.year == year] + series = QLineSeries() series.setPointsVisible(True) series.setPointLabelsVisible(True) series.setPointLabelsFormat("@yPoint") series.hovered.connect(self.chart_view.show_series_tooltip) - self.timestamp_by_run.clear() + self.timestamp_by_info.clear() + issues_number = [] for run in items: date_value = self._get_timegm(run.date) total_issues = run.get_total_issues() series.append(date_value, total_issues) + issues_number.append(total_issues) + + self.timestamp_by_info[date_value] = run.get_project_by_issue_numbers() + + now_date_timestamp = self._get_timegm(date.today()) + if now_date_timestamp not in self.timestamp_by_info: + # Использование данных из соседнего виджета + self.current_assigned_open_issues.refresh() + while not self.current_assigned_open_issues.current_items: + QApplication.instance().processEvents() - self.timestamp_by_run[date_value] = run + self.timestamp_by_info[now_date_timestamp] = self.current_assigned_open_issues.current_items + series.append(now_date_timestamp, sum(self.current_assigned_open_issues.current_items.values())) chart = QChart() chart.setTheme(QChart.ChartThemeDark) @@ -327,37 +412,44 @@ def _fill_chart(self, items: List[Run]): chart.setBackgroundRoundness(0) axisX = QDateTimeAxis() - # TODO: Не уверен, что это нужно... - # axisX.setRange( - # self._get_datetime(items[0].date, DT.timedelta(weeks=-1)), - # self._get_datetime(items[-1].date, DT.timedelta(weeks=1)) - # ) + if items: + axisX.setRange( + self._get_datetime(items[0].date, timedelta(days=-30)), + self._get_datetime(items[-1].date, timedelta(days=30)), + ) axisX.setFormat("dd/MM/yyyy") - axisX.setTitleText('Date') + axisX.setTitleText("Date") chart.addAxis(axisX, Qt.AlignBottom) series.attachAxis(axisX) axisY = QValueAxis() - axisY.setLabelFormat('%d') - axisY.setTitleText('Total issues') + if issues_number: + axisY.setRange(min(issues_number) * 0.8, max(issues_number) * 1.2) + axisY.setLabelFormat("%d") + axisY.setTitleText("Total issues") chart.addAxis(axisY, Qt.AlignLeft) series.attachAxis(axisY) self.chart_view.clear_all_tooltips() self.chart_view.setChart(chart) - def refresh(self): + def refresh(self) -> None: self.current_assigned_open_issues.refresh() items = list(Run.select()) + + self._fill_chart_filter(items) + self._fill_chart(items) items.reverse() self.table_run.refresh(items) - self.setWindowTitle(f"{WINDOW_TITLE}. Last refresh date: {DT.datetime.now():%d/%m/%Y %H:%M:%S}") + self.setWindowTitle( + f"{WINDOW_TITLE}. Last refresh date: {get_human_datetime()}" + ) - def _on_tray_activated(self, reason: QSystemTrayIcon.ActivationReason): + def _on_tray_activated(self, reason: QSystemTrayIcon.ActivationReason) -> None: # Если запрошено меню if reason == QSystemTrayIcon.Context: return @@ -368,19 +460,19 @@ def _on_tray_activated(self, reason: QSystemTrayIcon.ActivationReason): self.showNormal() self.activateWindow() - def changeEvent(self, event: QEvent): + def changeEvent(self, event: QEvent) -> None: if event.type() == QEvent.WindowStateChange: # Если окно свернули if self.isMinimized(): # Прячем окно с панели задач QTimer.singleShot(0, self.hide) - def closeEvent(self, event: QCloseEvent): + def closeEvent(self, event: QCloseEvent) -> None: self.hide() event.ignore() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) app.setQuitOnLastWindowClosed(False) diff --git a/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/main.py b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/main.py index f56199ca5..79bb07dcd 100644 --- a/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/main.py +++ b/job_compassplus/parse_jira_Assigned_Open_Issues_per_Project/main.py @@ -1,59 +1,68 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import sys import time +import sys # pip install schedule import schedule +# pip install simple-wait +from simple_wait import wait + import db -from common import ROOT_DIR, get_table, logger +from common import get_table, logger from get_assigned_open_issues_per_project import get_assigned_open_issues_per_project -sys.path.append(str(ROOT_DIR.parent / 'wait')) -from wait import wait + +IS_SINGLE: bool = "--single" in sys.argv def run(): + attempts_for_single: int = 5 + while True: try: - logger.info(f'Начало') + logger.info(f"Начало") assigned_open_issues_per_project = get_assigned_open_issues_per_project() logger.info( - 'Всего задач: %s\n\n%s\n', + "Всего задач: %s\n\n%s\n", sum(assigned_open_issues_per_project.values()), - get_table(assigned_open_issues_per_project) + get_table(assigned_open_issues_per_project), ) ok = db.add(assigned_open_issues_per_project) - if ok is None: - logger.info("Количество открытых задач в проектах не поменялось. Пропускаю...") - elif ok: + if ok: logger.info("Добавляю запись") else: logger.info("Сегодня запись уже была добавлена. Пропускаю...") - logger.info('\n' + '-' * 100 + '\n') + logger.info("\n" + "-" * 100 + "\n") break - except Exception: - logger.exception('Ошибка:') + except Exception as e: + if IS_SINGLE: + attempts_for_single -= 1 + if attempts_for_single <= 0: + raise e + + logger.exception("Ошибка:") - logger.info('Через 15 минут попробую снова...') + logger.info("Через 15 минут попробую снова...") wait(minutes=15) -if __name__ == '__main__': +if __name__ == "__main__": + if IS_SINGLE: + run() + sys.exit() + # Каждую неделю, в субботу, в 12:00 - schedule\ - .every().week\ - .saturday.at("12:00")\ - .do(run) + schedule.every().week.saturday.at("12:00").do(run) while True: schedule.run_pending() diff --git a/job_compassplus/parse_total_efforts.py b/job_compassplus/parse_total_efforts.py new file mode 100644 index 000000000..cf660badc --- /dev/null +++ b/job_compassplus/parse_total_efforts.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re + + +PATTERN_ARG: re.Pattern = re.compile(r"<(\w+)>") +PATTERN_RESULT: re.Pattern = re.compile(r"<=([\w +]+)>") + +DEFAULT_ARG_VALUE: str = "NaN" + +SAMPLE_TEMPLATE = """ +*Итоговая трудоемкость в человеко-днях:* <=a+b+c+d+e> ч/д, в том числе: +|Аналитика (проектирование/техническая спецификация)| ч/д| +|Разработка (программирование)| ч/д| +|Рецензирование решения и кода| ч/д| +|Тестирование (только силами разработчика)| ч/д| +|Резерв на непредвиденные работы и риски| ч/д| + +*Total efforts in man-days:* <= a+b+c+d+e> m/d, including: +|Analysis (design and tech specs)| m/d| +|Implementation (including coding)| m/d| +|Code and solution review| m/d| +|Testing (only by the developer)| m/d| +|Reserve for potential gaps and risks| m/d| +""".strip() + + +def get_args(template: str) -> list[str]: + args: list[str] = [] + for m in PATTERN_ARG.finditer(template): + value: str = m.group(1) + if value not in args: + args.append(value) + + return args + + +def process(template: str, arg_by_value: dict[str, str]) -> str: + def _get_norm_str_float(value: str) -> str: + return DEFAULT_ARG_VALUE if value == "nan" else value.removesuffix(".0") + + def _get_value_from_arg( + arg: str, + default_value: str = DEFAULT_ARG_VALUE, + get_float: bool = True, + ) -> str: + value = arg_by_value.get(arg) + if value: + value = value.strip() + value = _get_norm_str_float(value) + + # Проверка валидности значения + try: + float(value) + except Exception: + value = None + + if not value: + value = default_value + + if get_float: + value = f"float({value!r})" + + return value + + text: str = PATTERN_ARG.sub( + lambda m: _get_value_from_arg( + arg=m.group(1), + get_float=False, + ), + template, + ) + + def _process_result(m: re.Match) -> str: + template_expr: str = m.group(1) + + expr: str = re.sub( + r"\w+", + lambda m: _get_value_from_arg(arg=m.group()), + template_expr, + ) + + try: + value = eval(expr) + except SyntaxError: + value = float(DEFAULT_ARG_VALUE) + + result: str = f"{value:.1f}" + return _get_norm_str_float(result) + + return PATTERN_RESULT.sub(_process_result, text) + + +if __name__ == "__main__": + args: list[str] = get_args(SAMPLE_TEMPLATE) + print(args) + assert args == ["a", "b", "c", "d", "e"] + + print() + + arg_by_value: dict[str, str] = dict( + a="1.1", + b="0.1", + c="0.5", + d="12", + e="1.0", + ) + text = process(SAMPLE_TEMPLATE, arg_by_value) + print(text) + assert text == """ +*Итоговая трудоемкость в человеко-днях:* 14.7 ч/д, в том числе: +|Аналитика (проектирование/техническая спецификация)|1.1 ч/д| +|Разработка (программирование)|0.1 ч/д| +|Рецензирование решения и кода|0.5 ч/д| +|Тестирование (только силами разработчика)|12 ч/д| +|Резерв на непредвиденные работы и риски|1 ч/д| + +*Total efforts in man-days:* 14.7 m/d, including: +|Analysis (design and tech specs)|1.1 m/d| +|Implementation (including coding)|0.1 m/d| +|Code and solution review|0.5 m/d| +|Testing (only by the developer)|12 m/d| +|Reserve for potential gaps and risks|1 m/d| + """.strip() + + print("\n" + "-" * 10 + "\n") + + arg_by_value["e"] = "1.3" + text = process(SAMPLE_TEMPLATE, arg_by_value) + print(text) + assert text == """ +*Итоговая трудоемкость в человеко-днях:* 15 ч/д, в том числе: +|Аналитика (проектирование/техническая спецификация)|1.1 ч/д| +|Разработка (программирование)|0.1 ч/д| +|Рецензирование решения и кода|0.5 ч/д| +|Тестирование (только силами разработчика)|12 ч/д| +|Резерв на непредвиденные работы и риски|1.3 ч/д| + +*Total efforts in man-days:* 15 m/d, including: +|Analysis (design and tech specs)|1.1 m/d| +|Implementation (including coding)|0.1 m/d| +|Code and solution review|0.5 m/d| +|Testing (only by the developer)|12 m/d| +|Reserve for potential gaps and risks|1.3 m/d| + """.strip() + + print("\n" + "-" * 10 + "\n") + + arg_by_value: dict[str, str] = dict( + a="", + b="b", + c=" 3 ", + d="0", + ) + text = process(SAMPLE_TEMPLATE, arg_by_value) + print(text) + assert text == """ +*Итоговая трудоемкость в человеко-днях:* NaN ч/д, в том числе: +|Аналитика (проектирование/техническая спецификация)|NaN ч/д| +|Разработка (программирование)|NaN ч/д| +|Рецензирование решения и кода|3 ч/д| +|Тестирование (только силами разработчика)|0 ч/д| +|Резерв на непредвиденные работы и риски|NaN ч/д| + +*Total efforts in man-days:* NaN m/d, including: +|Analysis (design and tech specs)|NaN m/d| +|Implementation (including coding)|NaN m/d| +|Code and solution review|3 m/d| +|Testing (only by the developer)|0 m/d| +|Reserve for potential gaps and risks|NaN m/d| + """.strip() diff --git a/job_compassplus/portal_compassplus_com/employees_gui/config.py b/job_compassplus/portal_compassplus_com/employees_gui/config.py new file mode 100644 index 000000000..f6e4fb044 --- /dev/null +++ b/job_compassplus/portal_compassplus_com/employees_gui/config.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +URL = "https://portal.compassplus.com/Employees/Pages/OfficeReferenceBook.aspx" +URL_GET_EMPLOYEES_LIST = ( + "https://portal.compassplus.com/_layouts/15/tbi/employees.ashx?" + "p=50&c=1&s={}&fl=MPhotoUrl;NameLink;JobTitle;Department;WorkPhone" +) + +URL_GET_EMPLOYEE_INFO = ( + "https://portal.compassplus.com/_layouts/15/tbi/ui.ashx?u={}" + "&ctrl=TBI.SharePoint.Employees.WebParts/EmployeeFlyout" +) + +SETTINGS_FILE_NAME = "settings" + +PERSON_PLACEHOLDER_PHOTO = "iVBORw0KGgoAAAANSUhEUgAAAMgAAACWCAYAAACb3McZAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAkbSURBVHhe7Z0LUxNJFIX3//8bRPDBW1FUFA0g+AAEBN+KBQHxgeOe1LBbu7nTmSQzSd/Od6q+Klezydx0n8x09+3bf42NjWUAYINBAAJgEIAAGAQgAAYBCIBBAAJgEIAAGAQgAAYBCIBBAAJgEIAAGAQgAAYBCIBBAAJgEIAAGAQgAAYBCIBBAAJgEIAAGAQgAAYBCIBBAAJgEIAAGAQgAAYBCIBBBsjU1FS2srKSbW9vZ+/fv8+Ojo5afP/+/Z8/v3v3rvXvep1eb70PDA4MUjPXr1/PXrx4kZ2enma9SP/fy5cvs9nZWfP9oV4wSE3Mz89n+/v72cXFRd7V+9eXL1+ypaUl8/OgHjBIxczNzWUfP37Mu3Q9klFkQOvzoVowSEWMj49nz58/r/SOEdLv37+zra2t7OrVq+b1QDVgkAq4ceNG61d9GNLnapxjXRf0Dwbpk4WFhez8/DzvrsPR2dlZ69HOuj7oDwzSB7dv385+/vyZd9PhStfBAL56MEiP3Lp1K/v161fePeOQxiWPHj0yrxd6A4P0wPT0dGtxL0ZpkuDu3bvmdUP3YJAumZiYyE5OTvLuGKd0Z2NhsRowSJfs7u7m3TBuHR8fMwVcARikC5aXl/Pu50N7e3tmHFAeDFISPVp9+/Yt73o+pEE7K+79gUFKooRDj/r8+bMZD5QDg5Tg2rVr0ax39KIHDx6YcUFnMEgJvN49LtVsNlu5YlZsEAaDdEBjD893j0utrq6a8UEYDNKBx48f513MtzTta8UHYTBIB4aVpVuHFhcXzRihGAwS4ObNm3nXSkPa4WjFCcVgkACNRiPvWmlI+WNXrlwxYwUbDBLg7du3eddKR0rRt2IFGwwSINaM3X707NkzM1awwSAFpDb+uJQKSljxgg0GKeD+/ft5l0pLP378MOMFGwxSwPr6et6l0pOKTFgxQzsYpACV/0xVd+7cMWOGdjBIAYeHh3l3Sk+q+2vFDO1gkAJURDpVra2tmTFDOxikAO2jSFWbm5tmzNAOBikgZYMofd+KGdrBIAXo/I5UpeMUrJihHQxSQMqDdBXZtmKGdjBIAcp8TVUbGxtmzNAOBilAv7Kp6smTJ2bM0A4GKUA1blMVpUnLg0EKUHHqVDUzM2PGDO1gkAJUtlOF11KT6vayaao8GCSAjmVOTdpjb8UKNhgkQIoJi4rJihVsMEiAe/fu5d0qHSkmK1awwSABNA5Jadutxh+Tk5NmrGCDQTqQ0oKhilBYMUIxGKQDKU33UsS6ezBICVKYzaImVm9gkBLol9e7KPfTGxikJJ73h6g6vc44seKCMBikJDrKzOvKOuntvYNBuuD169d5l/MjnauoM06seKAzGKQLVE/K22E6ykq2YoFyYJAuUckcLzo4ODBjgPJgkB7w8Kh1dnbGwLwCMEgPKAUl5pOnlFKiBU7r2qE7MEiP6Nf55OQk75LxSDNtKrxtXTN0DwbpAw3aYzKJzEFZ0WrBIH2iO0kMqSgXFxfZ8vKyeY3QOxikAjQmGebA/fz8nKPVagKDVIjK6eiAmkFKJ0Zx3kd9YJCK0SPXq1evak9LkRGpb1U/GKQmZmdna6nvq9QRnX7FzsDBgEFqRusRe3t7faeofP36tZU2wp6OwYJBCtBzvY4q02OMsmHVOZXRa722DBrILy0ttSqra5ExZBg9nuk1qkCiz52enjbfs1sWFhZa8ezs7GS7u7utuDQtrLud9XrAIP9BFT+0b1sr0UXSukdVCYAyoTrt/7Fe2w8aF3WaZdOOQ+2/13fAXepfRt4gU1NTrQNlNFXajT59+tQ6S916z5jQ3afZbOZXXU76Lp4+fUou19+MpEH0C6m7gDp5P9JMUsyFEBRjP2Mf3Uk1I6cfEev9R4GRM4g69PHxcd4FqpGe5zXGsD5vGGiGq+qFS71fVWMhT4yMQdS4/d4xQtLYZG5uzvzsQaKJAE0F1yGls+iOMkqPXiNhEM3UDGInoB5JVldXzWuoG22r1Z1sENKAflTOGEnaIBprDKMyombCBvkru7i4mJ2enuafPjg1Gg3zelIiWYOMj49nb968yZty8NKvbFXTwUXIhIO6axRJs13WtaVCkgbRnUO/4jFIC35Vz3RdTk0POjHSkhY1U64Yn6RBtFIcm7RHfGtrq6dndxleq/qqjhhjATsZNdUZruQMok1DsUuPX0pT1+OROr3SP7Sf43IlXf+tv9e/6w6k2aPYpanzFBMokzKInsm7XRFH1SnF4xWSMojm6NFwVffExKBJxiAauNa9SQl1lqabNYNotZFHkjGI9lygOLS2tma2kUeSMIgGh6EUdTRYKe3GaiePJGEQreiiuJRKlZUkDBJzGdBRlR55rbbyhnuDKIMWxadUzkR0bxDtq0ZxKoUC2u4NUuceD9SfNjY2zDbzhGuD6Bbu7cSnUZLqglnt5gnXBtHiIIpXGodY7eYJ1wbRORgobnnP8nVtEG3WQXHr4cOHZtt5wbVBhr2bDnWW9zPaXRuEBcL4pW3PVtt5wbVB2PsRv7SRymo7L7g1iAq1ofil3ZBW+3nBrUFUkRz5kOcTsNwaRJU0kA95TjlxaxBVMEQ+5Pn0XbcG0UE0yIc87zB0a5DDw8P860exy/NaiFuDsAbiR6o2Y7WhB9wahDUQPzo4ODDb0AMuDaKyMpT48aMPHz6Y7egBlwYhzd2Xjo6OzHb0gEuDqGIG8iMVk7Pa0QMuDaLylsiPtOvTakcPuDTI+vp6/tUjL4rpkNNucGmQ7e3t/GtHXuQ1H8ulQVgk9Kf5+XmzLWPHpUFiPGUJhaUTsqy2jB2XBmk2m/nXjrzIa8KiS4NQyd2fdKyc1Zax484gOmYN+dPm5qbZnrHjziAUq/YpnfBrtWfsuDOIjlFG/rS/v2+2Z+y4M8jKykr+lSNP8noCrjuDqGI48idV4bfaM3bcGWRnZyf/ypEnea2P5c4gqtSH/Ekb3Kz2jB13BmGrrU95LSDnziCsovvVxMSE2aYx484grKL7lceMXlcGmZyczL9q5FEzMzNmu8aMK4PoC0Z+5bEEqSuDsBfdtzymvLsyiFKmkV/pTEmrXWPGlUFU4xX5lYptWO0aM64MohqvyK8ajYbZrvEylv0BRxk2BoRZQcsAAAAASUVORK5CYII=" diff --git a/job_compassplus/portal_compassplus_com/employees_gui/db.py b/job_compassplus/portal_compassplus_com/employees_gui/db.py new file mode 100644 index 000000000..7634add2a --- /dev/null +++ b/job_compassplus/portal_compassplus_com/employees_gui/db.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import base64 +import os + +from urllib.parse import urljoin + +from lxml import etree + +from sqlalchemy import Column, String, create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +import config + + +# TODO: добавить модуль logging + + +Base = declarative_base() + + +class Employee(Base): + """ + Класс описывает сотрудника. + + """ + + __tablename__ = "Employees" + + id = Column(String, primary_key=True) + url = Column(String) + name = Column(String) + short_name = Column(String) + birthday = Column(String) + job = Column(String) + department = Column(String) + photo = Column(String) + work_phone = Column(String) + mobile_phone = Column(String) + email = Column(String) + + @staticmethod + def parse(xml, session): + """Функция парсит строку xml с информацией о сотруднике и возвращает заполненный объект Employee.""" + + root = etree.HTML(xml) + + employee = Employee() + + # Если одно из них не будет определено, прекратить парсинг + try: + employee.id = root.xpath('//node()[@class="lookup-item"]/@id')[0].replace( + "user-", "" + ) + employee.short_name = root.xpath('//node()[@class="lookup-item"]/@value')[0] + employee.name = root.xpath('//node()[@class="lookup-item"]/@name')[0] + employee.url = root.xpath('//node()[@class="employee-name"]//a/@href')[0] + except IndexError as e: + print("error", "employee.id/short_name/name/url", e) + raise Exception("Bad parsing!") + + try: + # Загрузка страницы пользователя и вытаскивание дня его рождения + rs = session.get(employee.url) + user_root = etree.HTML(rs.text) + rs = user_root.xpath( + '//node()[contains(@id, "Birthday")]' + '/node()[@class="ms-tableCell ms-profile-detailsValue"]/text()' + ) + employee.birthday = rs[0] + except Exception as e: + print("error", "employee.birthday", e, employee.url) + employee.birthday = "" + + try: + photo_url = root.xpath('//node()[@class="employee-photo"]/img/@src')[0] + + # Относительный адрес делаем абсолютным + if photo_url.startswith("/"): + photo_url = urljoin(config.URL, photo_url) + + rs = session.get(photo_url) + if rs.ok: + employee.photo = base64.b64encode(rs.content).decode() + + except Exception as e: + print("warn", "employee.photo", e) + employee.photo = "" + + try: + employee.job = root.xpath( + '//node()[@class="employee-jobtitle"]/span[2]/text()' + )[0].strip() + except IndexError as e: + print("warn", "employee.job", e) + employee.job = "" + + try: + employee.department = root.xpath( + '//node()[@class="employee-department"]/span[2]/text()' + )[0].strip() + except IndexError as e: + print("warn", "employee.department", e) + employee.department = "" + + try: + employee.work_phone = "" + employee.mobile_phone = "" + + for div_phone in root.xpath('//node()[@class="employee-workphone"]'): + title, text = [el.text.strip() for el in div_phone.getchildren()] + if "Work" in title: + employee.work_phone = text + + elif "Mobile" in title: + employee.mobile_phone = text + + except Exception as e: + print("warn", "employee.work_phone/mobile_phone", e) + pass + + try: + employee.email = root.xpath( + '//node()[@class="employee-email"]/a/span/text()' + )[0].strip() + except IndexError as e: + print("warn", "employee.email", e) + employee.email = "" + + return employee + + def __str__(self) -> str: + return f'' + + def __repr__(self) -> str: + return self.__str__() + + +def get_session(): + DIR = os.path.dirname(__file__) + DB_FILE_NAME = "sqlite:///" + os.path.join(DIR, "database") + # DB_FILE_NAME = 'sqlite:///:memory:' + + # Создаем базу, включаем логирование и автообновление подключения каждые 2 часа (7200 секунд) + engine = create_engine( + DB_FILE_NAME, + # echo=True, + pool_recycle=7200, + ) + + Base.metadata.create_all(engine) + + Session = sessionmaker(bind=engine) + return Session() + + +db_session = get_session() + + +def exists(employee_id) -> bool: + return ( + db_session.query(Employee).filter(Employee.id == employee_id).scalar() + is not None + ) diff --git a/job_compassplus/portal_compassplus_com/employees_gui/main.py b/job_compassplus/portal_compassplus_com/employees_gui/main.py new file mode 100644 index 000000000..d77012be3 --- /dev/null +++ b/job_compassplus/portal_compassplus_com/employees_gui/main.py @@ -0,0 +1,454 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import base64 +import sys +import traceback + +import requests + +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from PyQt5.QtCore import * + +from requests_ntlm2 import HttpNtlmAuth + +from sqlalchemy import or_ + +import config +from db import * + + +# Для отлова всех исключений, которые в слотах Qt могут "затеряться" и привести к тихому падению +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) + + print("Error: ", text) + QMessageBox.critical(None, "Error", text) + sys.exit(1) + + +sys.excepthook = log_uncaught_exceptions + + +def get_url(page): + return config.URL_GET_EMPLOYEES_LIST.format((page - 1) * 50) + + +# # TODO: показывать короткое имя пользователя: ipetrash, ypaliy и т.п. +# +# if __name__ == '__main__': +# fill_db() + + +def pixmap_from_base64(base64_text): + pixmap = QPixmap() + pixmap.loadFromData(base64.b64decode(base64_text)) + pixmap = pixmap.scaledToWidth(192, Qt.SmoothTransformation) + + return pixmap + + +class EmployeeInfo(QWidget): + def __init__(self) -> None: + super().__init__() + + self.photo = QLabel() + self.name = QLabel() + self.short_name = QLabel() + self.job = QLabel() + self.department = QLabel() + # TODO: показывать также сколько осталось до др + self.birthday = QLabel() + + self.url = QLabel() + self.url.setOpenExternalLinks(True) + + self.work_phone = QLabel() + self.mobile_phone = QLabel() + self.id = QLabel() + + self.email = QLabel() + self.email.setOpenExternalLinks(True) + + form_layout = QFormLayout() + form_layout.addRow("Name:", self.name) + form_layout.addRow("Short Name:", self.short_name) + form_layout.addRow("Job:", self.job) + form_layout.addRow("Department:", self.department) + form_layout.addRow("Birthday:", self.birthday) + form_layout.addRow("Url:", self.url) + form_layout.addRow("Work Phone:", self.work_phone) + form_layout.addRow("Mobile Phone:", self.mobile_phone) + form_layout.addRow("Id:", self.id) + form_layout.addRow("Email:", self.email) + + layout = QVBoxLayout() + layout.addWidget(self.photo) + layout.addLayout(form_layout) + layout.addStretch() + + scroll_area_widget = QWidget() + scroll_area_widget.setLayout(layout) + + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setWidget(scroll_area_widget) + + layout = QVBoxLayout() + layout.addWidget(scroll_area) + self.setLayout(layout) + + # Все QLabel на форме умеет поддержку выделения и при наличии ссылок на них можно кликать + for label in self.findChildren(QLabel): + label.setTextInteractionFlags(Qt.TextBrowserInteraction) + + def set_employee(self, employee) -> None: + if not employee: + self.photo.setPixmap(pixmap_from_base64(config.PERSON_PLACEHOLDER_PHOTO)) + + self.name.setText("None") + self.short_name.setText("None") + self.job.setText("None") + self.department.setText("None") + self.birthday.setText("None") + self.url.setText("None") + self.work_phone.setText("None") + self.mobile_phone.setText("None") + self.id.setText("None") + self.email.setText("None") + return + + self.photo.setPixmap(pixmap_from_base64(employee.photo)) + + self.name.setText(employee.name) + self.short_name.setText(employee.short_name) + self.job.setText(employee.job) + self.department.setText(employee.department) + self.birthday.setText(employee.birthday) + self.url.setText(f'{employee.url}') + self.work_phone.setText(employee.work_phone) + self.mobile_phone.setText(employee.mobile_phone) + self.id.setText(employee.id) + self.email.setText(f'{employee.email}') + + +# TODO: ввод с клавы при фокусе на таблицу меняет редактор фильтра +class MainWindow(QMainWindow): + def __init__(self) -> None: + super().__init__() + + self.setWindowTitle("Compass Plus Employees") + self.setContextMenuPolicy(Qt.NoContextMenu) + + # TODO: окно с информацией о выделенном сотруднике умеет показывать его переработку/недоработку и прочее + self.filter_line_edit = QLineEdit() + # При изменении окна происходит вызов run_filter и отображение/скрытие кнопки очистки текста + self.filter_line_edit.textChanged.connect(self.run_filter) + self.filter_line_edit.installEventFilter(self) + + try: + # Добавление в редактор фильтра кнопки очищения содержимого + clear_icon = self.style().standardIcon(QStyle.SP_LineEditClearButton) + + clear_action = self.filter_line_edit.addAction( + clear_icon, QLineEdit.TrailingPosition + ) + clear_action.setVisible(len(self.filter_line_edit.text()) > 0) + clear_action.triggered.connect(self.filter_line_edit.clear) + + self.filter_line_edit.textChanged.connect( + lambda text: clear_action.setVisible(len(text) > 0) + ) + + # Если SP_LineEditClearButton не найден + except AttributeError: + # TODO: сделать реализацию для Qt4 + # clear_icon = pixmap_from_base64(config.LINE_EDIT_CLEAR_BUTTON_32x32) + pass + + self.employees_table = QTableWidget() + self.employees_table.setSelectionBehavior(QTableWidget.SelectRows) + self.employees_table.setSelectionMode(QTableWidget.SingleSelection) + self.employees_table.currentItemChanged.connect( + lambda item, _: self._item_click(item) + ) + + layout_filter = QHBoxLayout() + layout_filter.addWidget(QLabel("Filter:")) + layout_filter.addWidget(self.filter_line_edit) + + layout = QVBoxLayout() + layout.addLayout(layout_filter) + layout.addWidget(self.employees_table) + + central_widget = QWidget() + central_widget.setLayout(layout) + self.setCentralWidget(central_widget) + + employee_info_dock_widget = QDockWidget("Employee Info") + employee_info_dock_widget.setObjectName("employee_info_dock_widget") + employee_info_dock_widget.setFeatures(QDockWidget.NoDockWidgetFeatures) + + self.employee_info = EmployeeInfo() + employee_info_dock_widget.setWidget(self.employee_info) + self.addDockWidget(Qt.RightDockWidgetArea, employee_info_dock_widget) + + tool_bar = self.addToolBar("General") + tool_bar.setObjectName("General") + action_refill = tool_bar.addAction("Parse and refill") + action_refill.setToolTip( + "Clear database, parse site with employees, and fill database" + ) + action_refill.setStatusTip(action_refill.toolTip()) + action_refill.triggered.connect(self.refill) + + self.setStatusBar(QStatusBar()) + + def refill(self) -> None: + # TODO: можно в отдельный класс вынести + dialog = QDialog() + dialog.setWindowTitle("Auth and refill database") + + info = QLabel() + info.setText( + """When you click on OK, you will be cleansing a database of employees, +start parsing for the collection of employees and populate the database.""" + ) + + login = QLineEdit("CP\\") + password = QLineEdit() + password.setEchoMode(QLineEdit.Password) + + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + button_box.accepted.connect(dialog.accept) + button_box.rejected.connect(dialog.reject) + + form_layout = QFormLayout() + form_layout.addRow("Login:", login) + form_layout.addRow("Password:", password) + + layout = QVBoxLayout() + layout.addWidget(info) + layout.addLayout(form_layout) + layout.addStretch() + layout.addWidget(button_box) + + dialog.setLayout(layout) + if not dialog.exec(): + return + + login, password = login.text(), password.text() + + # Сначала пытаемся авторизоваться + session = requests.Session() + session.auth = HttpNtlmAuth(login, password, session) + + # Авторизация + rs = session.get(config.URL) + if not rs.ok: + QMessageBox.information(self, "Info", "Failed to login") + print("Не удалось авторизоваться") + print(f"rs.status_code = {rs.status_code}") + print(f"rs.headers = {rs.headers}") + return + + # TODO: move to db.py + # Очищение базы данных + for i in db_session.query(Employee): + db_session.delete(i) + db_session.commit() + + self.run_filter() + self.fill_db(session) + self.run_filter() + + def fill_db(self, session) -> None: + page = 1 + + rs = session.get(get_url(page)) + data = rs.json() + + max_page = data["Pages"] + + # TODO: наверное тоже нужно в QProgressDialog обернуть, хоть сбор и быстрый + employee_list = list() + employee_list += data["Properties"] + + while page < max_page: + page += 1 + + rs = session.get(get_url(page)) + data = rs.json() + + employee_list += data["Properties"] + + # Для отображения диалога парсинга и заполнения базы + progress = QProgressDialog( + "Operation in progress...", "Cancel", 0, len(employee_list), self + ) + progress.setWindowTitle("Parsing") + progress.setWindowModality(Qt.WindowModal) + + for i, row in enumerate(employee_list, 1): + progress.setValue(i) + + if progress.wasCanceled(): + break + + employee_id = row["Id"] + + if exists(employee_id): + print(f"Employee with id = {employee_id} already exist.") + continue + + rs = session.get(config.URL_GET_EMPLOYEE_INFO.format(employee_id)) + if not rs.ok: + print( + f"Request getting employee info (id={employee_id}) not ok." + ) + print(f"rs.status_code = {rs.status_code}") + print(f"rs.headers = {rs.headers}") + continue + + employee = Employee.parse(rs.text, session) + print(i, employee) + + db_session.add(employee) + + db_session.commit() + + progress.setValue(len(employee_list)) + + def _item_click(self, item) -> None: + employee = None + + if item and self.employees_table.rowCount() > 0: + item = self.employees_table.item(item.row(), 0) + employee = item.data(Qt.UserRole) + + self.employee_info.set_employee(employee) + + def run_filter(self) -> None: + # TODO: лучше использовать модель + # TODO: лучше использовать стандартный фильтр qt + # TODO: поиграться с делегатами для красивого отображения описания + ссылки на гист + + self.employees_table.clear() + self._item_click(None) + + # TODO: db.py + filter_text = self.filter_line_edit.text() + filter_text = f"%{filter_text}%" + sql_filter = or_( + Employee.name.like(filter_text), + Employee.short_name.like(filter_text), + Employee.job.like(filter_text), + Employee.department.like(filter_text), + Employee.birthday.like(filter_text), + Employee.work_phone.like(filter_text), + Employee.mobile_phone.like(filter_text), + Employee.id.like(filter_text), + Employee.email.like(filter_text), + ) + + query = db_session.query(Employee).filter(sql_filter) + rows = query.count() + + self.employees_table.setRowCount(rows) + + headers = [ + "Name", + "Short Name", + "Job", + "Department", + "Birthday", + "Url", + "Work Phone", + "Mobile Phone", + "Id", + "Email", + "Photo", + ] + self.employees_table.setColumnCount(len(headers)) + self.employees_table.setHorizontalHeaderLabels(headers) + + row = 0 + for employee in query: + self.employees_table.setItem(row, 0, QTableWidgetItem(employee.name)) + self.employees_table.setItem(row, 1, QTableWidgetItem(employee.short_name)) + self.employees_table.setItem(row, 2, QTableWidgetItem(employee.job)) + self.employees_table.setItem(row, 3, QTableWidgetItem(employee.department)) + self.employees_table.setItem(row, 4, QTableWidgetItem(employee.birthday)) + self.employees_table.setItem(row, 5, QTableWidgetItem(employee.url)) + self.employees_table.setItem(row, 6, QTableWidgetItem(employee.work_phone)) + self.employees_table.setItem( + row, 7, QTableWidgetItem(employee.mobile_phone) + ) + self.employees_table.setItem(row, 8, QTableWidgetItem(employee.id)) + self.employees_table.setItem(row, 9, QTableWidgetItem(employee.email)) + self.employees_table.setItem(row, 10, QTableWidgetItem(employee.photo)) + + self.employees_table.item(row, 0).setData(Qt.UserRole, employee) + + row += 1 + + # Запрет редактирования ячеек таблицы + for row in range(self.employees_table.rowCount()): + for column in range(self.employees_table.columnCount()): + self.employees_table.item(row, column).setFlags( + Qt.ItemIsSelectable | Qt.ItemIsEnabled + ) + + # Показываем информацию о первом сотруднике + if self.employees_table.rowCount() > 0: + item = self.employees_table.item(0, 0) + self.employees_table.setCurrentItem(item) + + def read_settings(self) -> None: + ini = QSettings(config.SETTINGS_FILE_NAME, QSettings.IniFormat) + + state = ini.value("MainWindow_State") + if state: + self.restoreState(state) + + geometry = ini.value("MainWindow_Geometry") + if geometry: + self.restoreGeometry(geometry) + + def write_settings(self) -> None: + ini = QSettings(config.SETTINGS_FILE_NAME, QSettings.IniFormat) + ini.setValue("MainWindow_State", self.saveState()) + ini.setValue("MainWindow_Geometry", self.saveGeometry()) + + def eventFilter(self, object, event): + # В окне вводе при клике на стрелку вниз фокус переходит в таблицу + if object == self.filter_line_edit: + if event.type() == QEvent.KeyRelease and event.key() == Qt.Key_Down: + self.employees_table.setFocus() + return False + + return super().eventFilter(object, event) + + def closeEvent(self, _) -> None: + self.write_settings() + + QApplication.instance().quit() + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.resize(1000, 750) + mw.read_settings() + mw.show() + mw.run_filter() + # TODO: + mw.employees_table.setFocus() + + app.exec_() diff --git a/job_compassplus/portal_compassplus_com/get_vacation_forecast.py b/job_compassplus/portal_compassplus_com/get_vacation_forecast.py new file mode 100644 index 000000000..dd2f35a2f --- /dev/null +++ b/job_compassplus/portal_compassplus_com/get_vacation_forecast.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +from pathlib import Path + +DIR = Path(__file__).resolve().parent +ROOT_DIR = DIR.parent + +sys.path.append(str(ROOT_DIR)) +from site_common import do_get, do_post + + +def get_vacation_forecast() -> int: + url = "https://helpdesk.compassluxe.com/pa-reports-new/vacation/VacationForecast" + rs = do_get(url) + data: dict[str, int] = rs.json() + return data["id"] + + +if __name__ == "__main__": + vacation_forecast: int = get_vacation_forecast() + print(f"Vacations:", vacation_forecast) diff --git a/job_compassplus/portal_compassplus_com/get_vacations.py b/job_compassplus/portal_compassplus_com/get_vacations.py new file mode 100644 index 000000000..25ed9e379 --- /dev/null +++ b/job_compassplus/portal_compassplus_com/get_vacations.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys + +from dataclasses import dataclass +from datetime import datetime, date, timezone +from pathlib import Path +from typing import Any + +DIR = Path(__file__).resolve().parent +ROOT_DIR = DIR.parent + +sys.path.append(str(ROOT_DIR)) +from site_common import do_get, do_post + + +@dataclass +class Vacation: + id: int + subject: str + subject_email: str + start_date: date + end_date: date + deputy: str + + @classmethod + def parse_from_dict(cls, data: dict[str, Any]) -> "Vacation": + def utc2local(dt_utc: datetime) -> datetime: + epoch = dt_utc.timestamp() + dt_local = datetime.fromtimestamp(epoch).replace(tzinfo=None) + + offset = dt_local - dt_utc.replace(tzinfo=None) + return (dt_utc + offset).replace(tzinfo=None) + + def parse_datetime(date_time_str: str) -> datetime: + # NOTE: Разбор даты из UTC "2024-08-18T19:00:00Z" в локальную дату + return utc2local(datetime.strptime(date_time_str, "%Y-%m-%dT%H:%M:%S%z")) + + return cls( + id=data["Id"], + subject=data["Subject"], + subject_email=data["EmailEmployee"], + start_date=parse_datetime(data["StartDate"]), + end_date=parse_datetime(data["EndDate"]), + deputy=data["Location"], + ) + + +def get_vacations() -> list[Vacation]: + url = "https://portal.compassplus.com/_api/web/lists/GetByTitle('Employee%20Vacations')/items" + rs = do_get( + url, + headers={ + # NOTE: С такими заголовками сервер вернет JSON, а не XML + "Accept": "application/json;odata.metadata=minimal", + "odata-version": "4.0", + }, + params={ + "$select": "Id,EndDate,Subject,Location,StartDate,EmailEmployee", + "$top": "5000", + "$orderby": "Subject asc", + "$filter": f"(EndDate ge '{datetime.now(timezone.utc).date()}T00:00:00')", + }, + ) + return [Vacation.parse_from_dict(value) for value in rs.json()["value"]] + + +if __name__ == "__main__": + vacations = get_vacations() + print(f"Vacations ({len(vacations)}):") + for i, vacation in enumerate(vacations, 1): + print(f" {i}. {vacation}") diff --git a/job_compassplus/portal_compassplus_com/notify_about_new_employees.py b/job_compassplus/portal_compassplus_com/notify_about_new_employees.py new file mode 100644 index 000000000..737926e0e --- /dev/null +++ b/job_compassplus/portal_compassplus_com/notify_about_new_employees.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os +import traceback +import time + +from urllib.parse import urljoin + +import requests +from playwright.sync_api import sync_playwright, Page + + +URL_PORTAL: str = os.getenv("URL_PORTAL") +URL_NOTIFY: str = os.getenv("URL_NOTIFY") + +if not URL_PORTAL: + raise Exception("URL_PORTAL environment is not set!") + +if not URL_NOTIFY: + raise Exception("URL_NOTIFY environment variable is not set!") + + +def parse(page: Page, url_portal: str = URL_PORTAL, url_notify: str = URL_NOTIFY) -> None: + print(f"Opening page: {url_portal}") + page.goto(url_portal) + + css_selector = "a#NameFieldLink[href]" + + items = page.locator(css_selector).all() + print("Users:", len(items)) + + for user_el in items: + url_mysite: str = user_el.get_attribute("href") + username: str = url_mysite.rsplit("\\")[-1] + + full_name: str = user_el.text_content().strip() + + print(f"Check {full_name!r} ({username})") + + url_check: str = urljoin(url_notify, username) + print(f"Sending a notification: {url_check}") + + rs = requests.get(url_check) + rs.raise_for_status() + + # 201 вернется, если в ходе запроса был добавлен пользователь + print(f"[{'+' if rs.status_code == 201 else '='}] {full_name!r} ({username})") + print() + + +with sync_playwright() as p: + print("Launching a browser") + browser = p.firefox.launch() + + try: + page = browser.new_page() + + max_attempts = 3 + attempt = 0 + while True: + try: + attempt += 1 + parse(page) + break + except Exception as e: + print(f"#{attempt}.\n{traceback.format_exc()}") + + if attempt >= max_attempts: + raise e + + time.sleep(30) + + finally: + browser.close() diff --git a/job_compassplus/print_connections__radixware_explorer.py b/job_compassplus/print_connections__radixware_explorer.py new file mode 100644 index 000000000..d20b36e50 --- /dev/null +++ b/job_compassplus/print_connections__radixware_explorer.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os + +from bs4 import BeautifulSoup + +# pip install tabulate +from tabulate import tabulate + + +FILE_NAME = r"%APPDATA%\radixware.org\explorer\connections.xml" +ABS_FILE_NAME = os.path.expandvars(FILE_NAME) + + +with open(ABS_FILE_NAME, "rb") as f: + root = BeautifulSoup(f.read(), "html.parser") + + +def get_tag_text(tag): + return "" if tag is None else tag.text + + +headers = [ + "NAME", + "ID", + "COMMENT", + "USERNAME", + "STATIONNAME", + "INITIALADDRESS", + "LANGUAGE", + "COUNTRY", + "EXPLORERROOTID", + "TRACELEVEL", +] +rows = [] + +for connection in root.select("connection"): + rows.append( + [ + connection["name"], + connection["id"], + get_tag_text(connection.comment), + get_tag_text(connection.username), + get_tag_text(connection.stationname), + get_tag_text(connection.initialaddress), + get_tag_text(connection.language), + get_tag_text(connection.country), + get_tag_text(connection.explorerrootid), + get_tag_text(connection.tracelevel), + ] + ) + + +print(tabulate(rows, headers=headers, tablefmt="grid")) diff --git a/job_compassplus/print_statistics_by_img.py b/job_compassplus/print_statistics_by_img.py new file mode 100644 index 000000000..c07c1779b --- /dev/null +++ b/job_compassplus/print_statistics_by_img.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import Counter, defaultdict +from hashlib import sha1 +from pathlib import Path + + +def process(path: Path) -> None: + files: list[Path] = [ + p + for p in path.glob("*/ads/*/img/*") + if p.suffix.lower() in [".svg", ".png", ".img", ".gif", ".jpg"] + ] + + suffix_by_number = Counter(p.suffix.lower() for p in files) + print(f"Suffixes ({len(suffix_by_number)}):") + for suffix, number in suffix_by_number.items(): + print(f" {suffix}: {number}") + + print() + + duplicated: dict[str, list[Path]] = defaultdict(list) + for p in files: + hash_file = sha1(p.read_bytes()).hexdigest() + duplicated[hash_file].append(p) + + duplicated = {k: v for k, v in duplicated.items() if len(v) > 1} + + print(f"Duplicates ({len(duplicated)}):") + for hash_file, items in sorted(duplicated.items(), key=lambda x: len(x[1]), reverse=True): + print(f" Hash {hash_file} ({len(items)}):") + for p in items: + print(f" {p}") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Search in locale" + ) + parser.add_argument( + "path_trunk", + type=Path, + metavar="/PATH/TO/TRUNK", + help="Path to TX source tree (the directory containing branch.xml)", + ) + args = parser.parse_args() + + process(args.path_trunk) diff --git a/job_compassplus/root_common.py b/job_compassplus/root_common.py index 86187f561..bad178e83 100644 --- a/job_compassplus/root_common.py +++ b/job_compassplus/root_common.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import ssl @@ -14,21 +14,30 @@ class TLSAdapter(requests.adapters.HTTPAdapter): def init_poolmanager(self, *args, **kwargs): ctx = ssl.create_default_context() - ctx.set_ciphers('DEFAULT@SECLEVEL=1') - kwargs['ssl_context'] = ctx + ctx.set_ciphers("DEFAULT@SECLEVEL=1") + kwargs["ssl_context"] = ctx return super(TLSAdapter, self).init_poolmanager(*args, **kwargs) -USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0' +USER_AGENT = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0" +) session = requests.session() session.cert = str(PATH_CERT) -session.mount('https://', TLSAdapter()) -session.headers['User-Agent'] = USER_AGENT +session.mount("https://", TLSAdapter()) +session.headers["User-Agent"] = USER_AGENT -if __name__ == '__main__': +if __name__ == "__main__": + from root_config import JIRA_HOST + # Check - rs = session.get('https://jira.compassplus.ru/pa-reports/') + rs = session.get(f"{JIRA_HOST}/pa-reports/") + print(rs) + rs.raise_for_status() + + rs = session.get(f"{JIRA_HOST}/secure/ViewProfile.jspa?name=ipetrash") print(rs) + rs.raise_for_status() diff --git a/job_compassplus/root_config.py b/job_compassplus/root_config.py index d410c0c75..658a260af 100644 --- a/job_compassplus/root_config.py +++ b/job_compassplus/root_config.py @@ -1,34 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pathlib import Path +JIRA_HOST = "https://helpdesk.compassluxe.com" + # NOTE: Get : openssl pkcs12 -nodes -out ipetrash.pem -in ipetrash.p12 -NAME_CERT = 'ipetrash.pem' +NAME_CERT = "ipetrash.pem" ROOT_DIR = Path(__file__).resolve().parent PATH_LOCAL_CERT = ROOT_DIR / NAME_CERT -DIR_CURRENT_KEYS = Path(r'C:\keys\bin\current') +DIR_CURRENT_KEYS = Path(r"C:\keys\bin\current") PATH_COMMON_CERT = DIR_CURRENT_KEYS / NAME_CERT -if not DIR_CURRENT_KEYS.exists(): - print(f"[#] {DIR_CURRENT_KEYS} don't exists!") - if PATH_LOCAL_CERT.exists(): PATH_CERT = PATH_LOCAL_CERT -elif PATH_COMMON_CERT: +elif PATH_COMMON_CERT.exists(): PATH_CERT = PATH_COMMON_CERT else: - raise Exception(f'File {NAME_CERT} not found in: {PATH_LOCAL_CERT}, {PATH_COMMON_CERT}!') + raise Exception( + f"File {NAME_CERT} not found in: {PATH_LOCAL_CERT}, {PATH_COMMON_CERT}!" + ) -if __name__ == '__main__': - print(f'ROOT_DIR: {ROOT_DIR}') - print(f'PATH_LOCAL_CERT: {PATH_LOCAL_CERT} (exists: {PATH_LOCAL_CERT.exists()})') - print(f'PATH_COMMON_CERT: {PATH_COMMON_CERT} (exists: {PATH_COMMON_CERT.exists()})') - print(f'PATH_CERT: {PATH_CERT} (exists: {PATH_CERT.exists()})') +if __name__ == "__main__": + print(f"ROOT_DIR: {ROOT_DIR}") + print(f"PATH_LOCAL_CERT: {PATH_LOCAL_CERT} (exists: {PATH_LOCAL_CERT.exists()})") + print(f"PATH_COMMON_CERT: {PATH_COMMON_CERT} (exists: {PATH_COMMON_CERT.exists()})") + print(f"PATH_CERT: {PATH_CERT} (exists: {PATH_CERT.exists()})") diff --git a/job_compassplus/site_common.py b/job_compassplus/site_common.py new file mode 100644 index 000000000..84c281b99 --- /dev/null +++ b/job_compassplus/site_common.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time + +# pip install requests_ntlm==1.3.0 +from requests_ntlm import HttpNtlmAuth + +from requests import Response +from requests.exceptions import RequestException + +from root_common import session +from site_config_token import USERNAME, PASSWORD + + +def do_request(method, url: str, *args, **kwargs) -> Response: + attempts = 0 + max_attempts = 5 + + while True: + attempts += 1 + try: + rs = method( + url, + auth=HttpNtlmAuth(USERNAME, PASSWORD), + *args, + **kwargs + ) + + # Через какое-то отваливается доступ, почистим куки и заново авторизуемся + if rs.status_code == 401: + session.cookies.clear() + + rs.raise_for_status() + + return rs + + except RequestException as e: + if attempts >= max_attempts: + raise e + + time.sleep(5) # 5 seconds + + +def do_get(url: str, *args, **kwargs) -> Response: + return do_request(session.get, url, *args, **kwargs) + + +def do_post(url: str, *args, **kwargs) -> Response: + return do_request(session.post, url, *args, **kwargs) + + +if __name__ == "__main__": + rs = do_get("https://mysite.compassplus.com:443/Person.aspx?accountname=CP%5Cipetrash") + print(rs) + + rs = do_get("https://portal.compassplus.com/Pages/default.aspx") + print(rs) diff --git a/job_compassplus/site_config_token.py b/job_compassplus/site_config_token.py new file mode 100644 index 000000000..328724b8b --- /dev/null +++ b/job_compassplus/site_config_token.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os +import sys + +from pathlib import Path + + +DIR = Path(__file__).resolve().parent + +TOKEN_FILE_NAME = DIR / "TOKEN.txt" +SEP = "|" + +try: + TOKEN = os.environ.get("TOKEN") + if not TOKEN: + TOKEN = TOKEN_FILE_NAME.read_text("utf-8").strip() + if not TOKEN: + raise Exception("TOKEN пустой!") + + USERNAME, PASSWORD = TOKEN.split(SEP) + +except: + print( + f"Нужно в {TOKEN_FILE_NAME.name} или в переменную окружения " + f"TOKEN добавить логин/пароль (формат <домен>\\<логин>{SEP}<пароль>)" + ) + TOKEN_FILE_NAME.touch() + sys.exit() diff --git a/job_compassplus/statistics_by_names/print_statistic_all_names.py b/job_compassplus/statistics_by_names/print_statistic_all_names.py deleted file mode 100644 index e794d1f8a..000000000 --- a/job_compassplus/statistics_by_names/print_statistic_all_names.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import sys -from pathlib import Path - -from bs4 import BeautifulSoup - -DIR = Path(__file__).resolve().parent -sys.path.append(str(DIR.parent)) -from current_job_report.get_user_and_deviation_hours import get_report_context - - -def get_all_names(split_name=False): - """ - split_name = False: ["<Фамилия> <Имя> <Отчество>", ...] - split_name = True: [["<Фамилия>", "<Имя>", "<Отчество>"], ...] - - """ - - text = get_report_context() - root = BeautifulSoup(text, 'html.parser') - - # Имена описаны как "<Фамилия> <Имя> <Отчество>" - items = sorted({' '.join(report.text.split()) for report in root.select('#report .person')}) - - if split_name: - return [x.split(maxsplit=2) for x in items] - - return items - - -if __name__ == '__main__': - # Имена описаны как "<Фамилия> <Имя> <Отчество>" - name_list = get_all_names() - - total = len(name_list) - print('Total:', total) - - print_line_format = '{:%s}. {}' % len(str(total)) - - for i, name in enumerate(name_list, 1): - print(print_line_format.format(i, name)) diff --git a/job_compassplus/statistics_by_names/print_statistic_find_namesake.py b/job_compassplus/statistics_by_names/print_statistic_find_namesake.py deleted file mode 100644 index b6df885db..000000000 --- a/job_compassplus/statistics_by_names/print_statistic_find_namesake.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Скрипт для поиска однофамильцев. - -""" - - -from collections import defaultdict - -# pip install pymorphy2 -import pymorphy2 - -from print_statistic_all_names import get_all_names - - -name_list = get_all_names() -total = len(name_list) -print('Total:', total) -print() - - -morph = pymorphy2.MorphAnalyzer() - - -def get_normal_form(word): - return morph.parse(word)[0].normal_form - - -second_name_by_full_name_list = defaultdict(list) - -for name in name_list: - second_name = name.split()[1] - normal_form = get_normal_form(second_name) - - second_name_by_full_name_list[normal_form].append(name) - -# Фильтр по однофамильцам -second_name_by_full_name_list = filter(lambda x: len(x[1]) > 1, second_name_by_full_name_list.items()) - -# Сортировка по количеству фамилий -second_name_by_full_name_list = sorted(second_name_by_full_name_list, key=lambda x: len(x[1]), reverse=True) - -# Вывод итоговых данных -for second_name, full_name_list in second_name_by_full_name_list: - print(f'{second_name.upper()} ({len(full_name_list)}): {sorted(full_name_list)}') diff --git a/job_compassplus/statistics_by_names/print_statistic_number_male_and_female.py b/job_compassplus/statistics_by_names/print_statistic_number_male_and_female.py deleted file mode 100644 index ca0d8a115..000000000 --- a/job_compassplus/statistics_by_names/print_statistic_number_male_and_female.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Скрипт для подсчета мужчин и женщин по именам. - -""" - - -# pip install pymorphy2 -import pymorphy2 - -from print_statistic_all_names import get_all_names - - -name_list = get_all_names(split_name=True) -total = len(name_list) -print('Total:', total) -print() - -morph = pymorphy2.MorphAnalyzer() - -masc_name_list = [] -femn_name_list = [] - -for _, first_name, _ in name_list: - parsed_word_list = morph.parse(first_name) - parsed_word = None - - if len(parsed_word_list) > 1: - parsed_word_list_filtered = list(filter(lambda x: x.normal_form.lower() == first_name.lower(), parsed_word_list)) - if not parsed_word_list_filtered: - # Алена -> Алёна - parsed_word_list_filtered = list(filter(lambda x: x.normal_form.lower() == first_name.lower().replace('е', 'ё'), parsed_word_list)) - if not parsed_word_list_filtered: - print('Error by parsing name:', first_name.upper(), parsed_word_list) - continue - - parsed_word = parsed_word_list_filtered[0] - else: - parsed_word = parsed_word_list_filtered[0] - else: - parsed_word = parsed_word_list[0] - - gender = parsed_word.tag.gender - - if gender == 'masc': - masc_name_list.append(first_name) - else: - femn_name_list.append(first_name) - -# Сортировка списка имен -masc_name_list.sort() -femn_name_list.sort() - -print(f'Masc ({len(masc_name_list)}): {masc_name_list}') -print(f'Femn ({len(femn_name_list)}): {femn_name_list}') diff --git a/job_compassplus/statistics_by_names/print_statistic_unique_first_name.py b/job_compassplus/statistics_by_names/print_statistic_unique_first_name.py deleted file mode 100644 index 41c549fc6..000000000 --- a/job_compassplus/statistics_by_names/print_statistic_unique_first_name.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -Скрипт для вывода имен без повторений. - -""" - - -from print_statistic_all_names import get_all_names - - -first_name_list = [name[1] for name in get_all_names(split_name=True)] -print('Total:', len(first_name_list)) - -unique_first_name_list = list(set(first_name_list)) -print(f'Total unique ({len(unique_first_name_list)}): {unique_first_name_list}') diff --git a/job_compassplus/svn/find_release_version.py b/job_compassplus/svn/find_release_version.py new file mode 100644 index 000000000..cf783e534 --- /dev/null +++ b/job_compassplus/svn/find_release_version.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import subprocess +import xml.etree.ElementTree as ET + +from datetime import date, timedelta + +from get_last_release_version import get_last_release_version, URL_DEFAULT_SVN_PATH + + +def find_release_version( + text: str, + version: str, + last_days: int = 30, + url_svn_path: str = URL_DEFAULT_SVN_PATH, +) -> str: + url = f"{url_svn_path}/{version}" + + end_date = date.today() - timedelta(days=last_days) + + data: bytes = subprocess.check_output( + [ + "svn", + "log", + # "--verbose", + "--xml", + "--search", + text, + "--revision", + # Если в паре значений первым идет большее значение, то поиск будет идти от большего к меньшему + f"HEAD:{{{end_date}}}", + url, + ] + ) + root = ET.fromstring(data) + + last_revision = None + for logentry_el in root.findall(".//logentry"): + last_revision = logentry_el.attrib["revision"] + break + + if not last_revision: + raise Exception("Не удалось найти ревизию!") + + last_release_version: str = get_last_release_version( + version=version, + start_revision=last_revision, + last_days=last_days, + url_svn_path=url_svn_path, + ) + + # Первый коммит, который искали попал уже в следующую версию, поэтому + # нужно добавить 1 к последней версии: + # 3.2.35.10.10 -> 3.2.35.10.11 + last_release_version = re.sub( + r"\.(\d+)$", # Последнее число в версии + lambda m: f".{int(m.group(1)) + 1}", # Увеличение числа на 1 + last_release_version + ) + + return last_release_version + + +if __name__ == '__main__': + text = "TXI-8197" + print(find_release_version(text=text, version="3.2.35.10")) + # 3.2.35.10.11 + + print(find_release_version(text=text, version="3.2.34.10")) + # 3.2.34.10.18 diff --git a/job_compassplus/svn/get_last_release_version.py b/job_compassplus/svn/get_last_release_version.py new file mode 100644 index 000000000..886ec6a6b --- /dev/null +++ b/job_compassplus/svn/get_last_release_version.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import subprocess +import xml.etree.ElementTree as ET + +from datetime import date, timedelta + + +TEXT_RELEASE_VERSION = "Release version " +PATTERN_RELEASE_VERSION = re.compile(rf"{TEXT_RELEASE_VERSION}([\d.]+) ") + +URL_DEFAULT_SVN_PATH = "svn+cplus://svn2.compassplus.ru/twrbs/trunk/dev" + + +def get_last_release_version( + version: str, + start_revision: str = "HEAD", + last_days: int = 30, + url_svn_path: str = URL_DEFAULT_SVN_PATH, +) -> str: + url = f"{url_svn_path}/{version}" + + end_date = date.today() - timedelta(days=last_days) + + data: bytes = subprocess.check_output( + [ + "svn", + "log", + "--xml", + "--search", + TEXT_RELEASE_VERSION, + "--revision", + # Если в паре значений первым идет большее значение, то поиск будет идти от большего к меньшему + f"{start_revision}:{{{end_date}}}", + url, + ] + ) + root = ET.fromstring(data) + + last_release_version_msg = None + for logentry_el in root.findall(".//logentry"): + last_release_version_msg = logentry_el.find("msg").text + break + + if not last_release_version_msg: + raise Exception("Не удалось найти коммит релиза!") + + m = PATTERN_RELEASE_VERSION.search(last_release_version_msg) + if not m: + raise Exception( + f"Не удалось вытащить версию релиза из {last_release_version_msg!r}" + ) + + return m.group(1) + + +if __name__ == "__main__": + print(get_last_release_version(version="trunk", last_days=60)) + # 3.2.36.10 + + print(get_last_release_version(version="3.2.35.10")) + # 3.2.35.10.10 + + print(get_last_release_version(version="3.2.34.10")) + # 3.2.34.10.17 diff --git a/job_compassplus/svn/search_by_versions.py b/job_compassplus/svn/search_by_versions.py new file mode 100644 index 000000000..a225af75b --- /dev/null +++ b/job_compassplus/svn/search_by_versions.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import subprocess +import xml.etree.ElementTree as ET + +from datetime import date, timedelta + + +PATTERN_VERSION = re.compile(r"/dev/(.+?)/") +URL_DEFAULT_SVN_PATH = "svn+cplus://svn2.compassplus.ru/twrbs/trunk/dev" + + +def search( + text: str, + last_days: int = 30, + url_svn_path: str = URL_DEFAULT_SVN_PATH, +) -> list[str]: + start_date = date.today() - timedelta(days=last_days) + + data: bytes = subprocess.check_output( + [ + "svn", + "log", + "--verbose", + "--xml", + "--search", + text, + "--revision", + # Порядок имеет значение - выдача ревизий тут будет от меньшей к большей + f"{{{start_date}}}:HEAD", + url_svn_path, + ] + ) + + root = ET.fromstring(data) + + versions = [] + for logentry_el in root.findall(".//logentry"): + for path_el in logentry_el.findall("./paths/path"): + if m := PATTERN_VERSION.search(path_el.text): + version = m.group(1) + if version not in versions: + versions.append(version) + + return versions + + +if __name__ == "__main__": + versions: list[str] = search(text="TXI-8210") + print(versions) + # ['trunk', '3.2.36.10', '3.2.35.10'] + + versions: list[str] = search( + text="OPTT-441", + last_days=365, + url_svn_path="svn+cplus://svn2.compassplus.ru/twrbs/csm/optt", + ) + print(versions) + # ['trunk', '2.1.12.1'] diff --git a/job_compassplus/tx_parse_xml/acl__add_attr__IsPresentable=false.py b/job_compassplus/tx_parse_xml/acl__add_attr__IsPresentable=false.py new file mode 100644 index 000000000..dd5f1b993 --- /dev/null +++ b/job_compassplus/tx_parse_xml/acl__add_attr__IsPresentable=false.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from bs4 import BeautifulSoup + + +FILE_NAME_ACL = Path( + r"C:\DEV__TX\trunk_tx\com.tranzaxis\ads\Interfacing.W4\src\aclTJQ2GCOP7NFZPERU23FQA76GXQ.xml" +) + +# NOTE: +PROP_IDS = "prd45TLQNSG4ZHKBNA47FXMPPU2DA prdSMVLMMHGBNFZDPKTLREQIMZQQI prdO2NOBXHOF5GQ7GZNU72X7AHINA prdUCW5JGR3ERH57BHQZW6A5ICD3Y prdOBM2S7OY3NEGNKNH2ICNFYBBJM prd44YUSXQGX5EWRDH5KNLZW3HQY4 prdK6OZ5JDUPFHDFEOYPUV743YE4Q prdZBIQDTOORBBJ5LCGRJIZUTOR2M prdPIVNTR5EERAHLGOWNB2H3OM674 prdZ77U22XJOZA75HI3VOPQMRGEMQ prd7HEZYF7WEBE33BTYCW5JZBR7TM prdKDRVNDR2WZHDZNAWVUFTNQDMXM prdWW4PDFFYP5DHVNGYY5PFUDZKSU prdH273DPBJCJH5RIO7XOEDA264K4 prdLOZ3LGQ6O5DCJMCJKCDCNHCDTY prdXIJRCNSMUVB5NM7XWSEKNIB7VM prdM3TWEJNXGZHJPF2RYCSOVO5DCY prdU2ZTHQSTMFGSHHDZMAN4HHVEQY prd67BZTCTYZ5B5VJ2XHTEMF4GPTU prdU4ZPUTPJOVEAXB2STEOR75N4PM prdOSEMPZYRR5HMVG7QPW6HQJJUNQ prdE3XVETDHQVAG7F4AI2XMHTTNAE prdDSS3XKFQ7VFV3FTNVY72ZPFXNI prdMVSBYDI2MZA2ZJN7E5NIYKFOS4 prd3YGIB5LSA5CY5L6Z5SPX2IDHNU prdVBGG254XNNDFXIKVTOGGGC7RCE prdFJWE4R7BFRC7JDREKN45LKJE6Y prdDLGUFA7MFJEOXK2TDN5NEXK6JY prdJII6QJNJVFHZ5NWE6NPJJYJNAY prdMJQG63HJ4JAZ5KWGNUJASMEE6M prdDEIUSASLFJDV3P7FEEBJ3JC4IY prdIFMA4QRICFDD3GVNV4ERFEXIKQ prdGK3RLNHSJVD5DBZOU4MF3IZQHU prdDKTJ7TT7W5G4RGOLSQGRXDSDIA prd5CVKNF4PPNGAJDAWKIOJRSGGL4".split() + +root_acl = BeautifulSoup(open(FILE_NAME_ACL, "rb"), "xml") + +for prop_id in PROP_IDS: + prop_el = root_acl.select_one(f'[Id="{prop_id}"]') + + if not prop_el.Presentation.has_attr("IsPresentable"): + prop_el.Presentation["IsPresentable"] = "false" + +with open(FILE_NAME_ACL, "w", encoding="utf-8") as f: + f.write(str(root_acl)) diff --git a/job_compassplus/tx_parse_xml/acl__generate_new_Property.py b/job_compassplus/tx_parse_xml/acl__generate_new_Property.py new file mode 100644 index 000000000..cbaad3548 --- /dev/null +++ b/job_compassplus/tx_parse_xml/acl__generate_new_Property.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import uuid + +from pathlib import Path + +from bs4 import BeautifulSoup + + +FILE_NAME_ACL = Path( + r"C:\DEV__TX\trunk_tx\com.tranzaxis\ads\Interfacing.W4\src\aclTJQ2GCOP7NFZPERU23FQA76GXQ.xml" +) +FILE_NAME_ACL_LOCALE = ( + FILE_NAME_ACL.parent.parent / "locale" / "en" / ("mlb" + FILE_NAME_ACL.name) +) + +root_acl = BeautifulSoup(open(FILE_NAME_ACL, "rb"), "xml") +root_acl_locale = BeautifulSoup(open(FILE_NAME_ACL_LOCALE, "rb"), "xml") + +# NOTE: +PROP_IDS = "prd45TLQNSG4ZHKBNA47FXMPPU2DA prdSMVLMMHGBNFZDPKTLREQIMZQQI prdO2NOBXHOF5GQ7GZNU72X7AHINA prdUCW5JGR3ERH57BHQZW6A5ICD3Y prdOBM2S7OY3NEGNKNH2ICNFYBBJM prd44YUSXQGX5EWRDH5KNLZW3HQY4 prdK6OZ5JDUPFHDFEOYPUV743YE4Q prdZBIQDTOORBBJ5LCGRJIZUTOR2M prdPIVNTR5EERAHLGOWNB2H3OM674 prdZ77U22XJOZA75HI3VOPQMRGEMQ prd7HEZYF7WEBE33BTYCW5JZBR7TM prdKDRVNDR2WZHDZNAWVUFTNQDMXM prdWW4PDFFYP5DHVNGYY5PFUDZKSU prdH273DPBJCJH5RIO7XOEDA264K4 prdLOZ3LGQ6O5DCJMCJKCDCNHCDTY prdXIJRCNSMUVB5NM7XWSEKNIB7VM prdM3TWEJNXGZHJPF2RYCSOVO5DCY prdU2ZTHQSTMFGSHHDZMAN4HHVEQY prd67BZTCTYZ5B5VJ2XHTEMF4GPTU prdU4ZPUTPJOVEAXB2STEOR75N4PM prdOSEMPZYRR5HMVG7QPW6HQJJUNQ prdE3XVETDHQVAG7F4AI2XMHTTNAE prdDSS3XKFQ7VFV3FTNVY72ZPFXNI prdMVSBYDI2MZA2ZJN7E5NIYKFOS4 prd3YGIB5LSA5CY5L6Z5SPX2IDHNU prdVBGG254XNNDFXIKVTOGGGC7RCE prdFJWE4R7BFRC7JDREKN45LKJE6Y prdDLGUFA7MFJEOXK2TDN5NEXK6JY prdJII6QJNJVFHZ5NWE6NPJJYJNAY prdMJQG63HJ4JAZ5KWGNUJASMEE6M prdDEIUSASLFJDV3P7FEEBJ3JC4IY prdIFMA4QRICFDD3GVNV4ERFEXIKQ prdGK3RLNHSJVD5DBZOU4MF3IZQHU prdDKTJ7TT7W5G4RGOLSQGRXDSDIA prd5CVKNF4PPNGAJDAWKIOJRSGGL4".split() +items = [] + +new_prop_ids = [] + +with open("new_props.txt", "w", encoding="utf-8") as f: + for prop_id in PROP_IDS: + prop_el = root_acl.select_one(f'[Id="{prop_id}"]') + prop_name = prop_el["Name"] + + title_id = prop_el.Presentation["TitleId"] + title = root_acl_locale.select_one(f'[Id="{title_id}"]').Value.text + + # print(name, title) + prop_el["Id"] = f"prd{uuid.uuid4().hex.upper()[:26]}" + prop_el["Name"] = f"{prop_name}_Title_{title}" + + new_prop_ids.append(prop_el["Id"]) + + new_src = f"""\ + + + return + + + + {prop_name} + + + + ; + + \ + """ + + new_prop_el_str = re.sub( + ".+?", new_src, str(prop_el), flags=re.DOTALL + ) + print(new_prop_el_str, file=f) + + f.write("\n\n") + f.write('new_prop_ids: "' + " ".join(new_prop_ids) + '"') diff --git a/job_compassplus/tx_parse_xml/acl__generate_new_case_of_switch__with_XSD.py b/job_compassplus/tx_parse_xml/acl__generate_new_case_of_switch__with_XSD.py new file mode 100644 index 000000000..cd308284d --- /dev/null +++ b/job_compassplus/tx_parse_xml/acl__generate_new_case_of_switch__with_XSD.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# Example: +# case "{OPERATION}": { +# Crypto::CryptoWsdl:{OPERATION}Document doc = Crypto::CryptoWsdl:{OPERATION}Document.Factory.newInstance(); +# doc.ensure{OPERATION}().ensure{OPERATION}Rq(); +# return doc; +# } +TEMPLATE_CASE = """\ + + case "{OPERATION}": { + + + + + Crypto::CryptoWsdl:{OPERATION}Document + + + + doc = + + + + Crypto::CryptoWsdl:{OPERATION}Document + + + + .Factory.newInstance(); + doc.ensure{OPERATION}().ensure{OPERATION}Rq(); + return doc; + } + +""" + +# Example: +# switch (operationName) { +# {SWITCHES} +# +# default: { +# return null; +# } +# } +TEMPLATE = """ + + + switch (operationName) { +{SWITCHES} + + default: { + return null; + } +} + + +""" + +ITEMS = [ + "CalcArpc", + "CalcArqc", + "CalcCsc", + "CalcCvv", + "CalcEmvMac", + "CalcHmac", + "CalcKeyCheck", + "CalcMac", + "CalcMacDukpt", + "CalcPinOffset", + "CalcPvv", + "CheckArpc", + "CheckArqc", + "CheckCapToken", + "CheckCsc", + "CheckCvc3", + "CheckCvv", + "CheckDcsc", + "CheckDcvv", + "CheckHmac", + "CheckKey", + "CheckMac", + "CheckMacDukpt", + "CheckPinOffset", + "CheckPvv", + "CheckRsaKey", + "DecryptDukpt", + "DesCrypt", + "DesRecrypt", + "EncryptDukpt", + "ExportEmvPinBlock", + "ExportHmacKey", + "ExportKey", + "ExportPinBlock", + "ExportRsaKey", + "GenHmacKey", + "GenKey", + "GenRsaKey", + "GeneratePin", + "GetHsmInfo", + "ImportClearPin", + "ImportHmacKey", + "ImportKey", + "ImportPinBlock", + "ImportPinBlockDukpt", + "ImportPinOffset", + "ImportRsaKey", + "JwtDecode", + "JwtEncode", + "MigrateKey", + "RebuildPinBlock", + "RklGenerateAndEncryptKey", + "RklImportPublicKey", + "RklImportRootPublicKey", + "RsaSign", + "RsaVerify", + "TdsSign", + "TranslateKey", +] + +cases = [TEMPLATE_CASE.replace("{OPERATION}", operation) for operation in ITEMS] +text = TEMPLATE.replace("{SWITCHES}", "\n".join(cases)) + +with open("switch_case.xml", "w", encoding="utf-8") as f: + print(text) + print(text, file=f) diff --git a/job_compassplus/tx_parse_xml/acl__prop_to_title.py b/job_compassplus/tx_parse_xml/acl__prop_to_title.py new file mode 100644 index 000000000..dbba66698 --- /dev/null +++ b/job_compassplus/tx_parse_xml/acl__prop_to_title.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from bs4 import BeautifulSoup + + +FILE_NAME_ACL = Path( + r"C:\DEV__TX\trunk_tx\com.tranzaxis\ads\Interfacing.W4\src\aclTJQ2GCOP7NFZPERU23FQA76GXQ.xml" +) +FILE_NAME_ACL_LOCALE = ( + FILE_NAME_ACL.parent.parent / "locale" / "en" / ("mlb" + FILE_NAME_ACL.name) +) + +root_acl = BeautifulSoup(open(FILE_NAME_ACL, "rb"), "html.parser") +root_acl_locale = BeautifulSoup(open(FILE_NAME_ACL_LOCALE, "rb"), "html.parser") + +# NOTE: +PROP_IDS = "prd45TLQNSG4ZHKBNA47FXMPPU2DA prdSMVLMMHGBNFZDPKTLREQIMZQQI prdO2NOBXHOF5GQ7GZNU72X7AHINA prdUCW5JGR3ERH57BHQZW6A5ICD3Y prdOBM2S7OY3NEGNKNH2ICNFYBBJM prd44YUSXQGX5EWRDH5KNLZW3HQY4 prdK6OZ5JDUPFHDFEOYPUV743YE4Q prdZBIQDTOORBBJ5LCGRJIZUTOR2M prdPIVNTR5EERAHLGOWNB2H3OM674 prdZ77U22XJOZA75HI3VOPQMRGEMQ prd7HEZYF7WEBE33BTYCW5JZBR7TM prdKDRVNDR2WZHDZNAWVUFTNQDMXM prdWW4PDFFYP5DHVNGYY5PFUDZKSU prdH273DPBJCJH5RIO7XOEDA264K4 prdLOZ3LGQ6O5DCJMCJKCDCNHCDTY prdXIJRCNSMUVB5NM7XWSEKNIB7VM prdM3TWEJNXGZHJPF2RYCSOVO5DCY prdU2ZTHQSTMFGSHHDZMAN4HHVEQY prd67BZTCTYZ5B5VJ2XHTEMF4GPTU prdU4ZPUTPJOVEAXB2STEOR75N4PM prdOSEMPZYRR5HMVG7QPW6HQJJUNQ prdE3XVETDHQVAG7F4AI2XMHTTNAE prdDSS3XKFQ7VFV3FTNVY72ZPFXNI prdMVSBYDI2MZA2ZJN7E5NIYKFOS4 prd3YGIB5LSA5CY5L6Z5SPX2IDHNU prdVBGG254XNNDFXIKVTOGGGC7RCE prdFJWE4R7BFRC7JDREKN45LKJE6Y prdDLGUFA7MFJEOXK2TDN5NEXK6JY prdJII6QJNJVFHZ5NWE6NPJJYJNAY prdMJQG63HJ4JAZ5KWGNUJASMEE6M prdDEIUSASLFJDV3P7FEEBJ3JC4IY prdIFMA4QRICFDD3GVNV4ERFEXIKQ prdGK3RLNHSJVD5DBZOU4MF3IZQHU prdDKTJ7TT7W5G4RGOLSQGRXDSDIA prd5CVKNF4PPNGAJDAWKIOJRSGGL4".split() + +items = [] + +for prop_id in PROP_IDS: + prop_el = root_acl.select_one("#" + prop_id) + name = prop_el["name"] + + title_id = prop_el.presentation["titleid"] + title = root_acl_locale.select_one("#" + title_id).value.text + + items.append((name, title)) + +items.sort() + +for name, title in items: + print(name, title, sep="\t") diff --git a/job_compassplus/tx_parse_xml/acl_update_License_for_GetterSources.py b/job_compassplus/tx_parse_xml/acl_update_License_for_GetterSources.py new file mode 100644 index 000000000..e48cb39af --- /dev/null +++ b/job_compassplus/tx_parse_xml/acl_update_License_for_GetterSources.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from pathlib import Path + +from bs4 import BeautifulSoup + + +FILE_NAME_ACL = Path( + r"C:\DEV__TX\trunk_tx\com.tranzaxis\ads\Interfacing.W4\src\aclTJQ2GCOP7NFZPERU23FQA76GXQ.xml" +) +LICENSE_PATH = "com.tranzaxis/Interface/Online/W4/" + +TEMPLATE_THIS_PROP = """ + + + {prop_name} + + +""" +TEMPLATE = """ + + + + if ( + + {TEMPLATE_THIS_PROP} + + == null) { + try { + + + + + + + + + + + + + + + + + + {TEMPLATE_THIS_PROP} + + = true; + + } catch (java.lang.Exception e) { + + + {TEMPLATE_THIS_PROP} + + = false; + } +} + +return + + {TEMPLATE_THIS_PROP} + + ; + + + + +""" + +ITEMS = [...] + +root_acl = BeautifulSoup(open(FILE_NAME_ACL, "rb"), "xml") +root_acl_str = str(root_acl) + +for license_name, license_id, prop_name, prop_id in ITEMS: + print(license_name, license_id, prop_name, prop_id) + + new_getter_src = ( + TEMPLATE.replace("{TEMPLATE_THIS_PROP}", TEMPLATE_THIS_PROP) + .replace("{prop_id}", prop_id) + .replace("{prop_name}", prop_name) + .replace("{license_path}", LICENSE_PATH) + .replace("{license_name}", license_id) + .replace("{acl_id}", FILE_NAME_ACL.stem) + ) + + prop_el = root_acl.select_one(f'[Id="{prop_id}"]') + old_prop_el_str = str(prop_el) + new_prop_el_str = re.sub( + ".+?", + new_getter_src, + old_prop_el_str, + flags=re.DOTALL, + ) + root_acl_str = root_acl_str.replace(old_prop_el_str, new_prop_el_str) + +with open("new_" + FILE_NAME_ACL.name, "w", encoding="utf-8") as f: + f.write(root_acl_str) diff --git a/job_compassplus/tx_parse_xml/acl_update_name_props.py b/job_compassplus/tx_parse_xml/acl_update_name_props.py new file mode 100644 index 000000000..5c6d252d2 --- /dev/null +++ b/job_compassplus/tx_parse_xml/acl_update_name_props.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from bs4 import BeautifulSoup + + +FILE_NAME_ACL = Path( + r"C:\DEV__TX\trunk_tx\com.tranzaxis\ads\Interfacing.W4\src\aclTJQ2GCOP7NFZPERU23FQA76GXQ.xml" +) + +ITEMS = [...] + +root_acl = BeautifulSoup(open(FILE_NAME_ACL, "rb"), "xml") +root_acl_str = str(root_acl) + +for license_name, license_id, prop_name, prop_id in ITEMS: + print(license_name, license_id, prop_name, prop_id) + + prop_el = root_acl.select_one(f'[Id="{prop_id}"]') + old_prop_el_str = str(prop_el) + + prop_el["Name"] = f"{prop_name}_{license_id}" + new_prop_el_str = str(prop_el) + + root_acl_str = root_acl_str.replace(old_prop_el_str, new_prop_el_str) + +with open("new_" + FILE_NAME_ACL.name, "w", encoding="utf-8") as f: + f.write(root_acl_str) diff --git a/job_compassplus/tx_parse_xml/count_TODO_NOTE_FIXME_of_ADS_classes.py b/job_compassplus/tx_parse_xml/count_TODO_NOTE_FIXME_of_ADS_classes.py new file mode 100644 index 000000000..0fa9efc26 --- /dev/null +++ b/job_compassplus/tx_parse_xml/count_TODO_NOTE_FIXME_of_ADS_classes.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import xml.etree.ElementTree as ET + +from collections import defaultdict +from pathlib import Path +from xml.etree.ElementTree import ParseError + + +NS = dict( + xsc="http://schemas.radixware.org/xscml.xsd", +) + + +def process(path: str): + if isinstance(path, str): + path = Path(path) + + if not path.exists(): + raise Exception(f"Not exists: {path}") + + assignee_by_number: dict[str, int] = defaultdict(int) + type_by_number: dict[str, int] = defaultdict(int) + behavior_by_number: dict[str, int] = defaultdict(int) + + for layer_dir in path.glob("*"): + if not layer_dir.is_dir(): + continue + + layer_ads_dir: Path = layer_dir / "ads" + if not layer_ads_dir.is_dir(): + continue + + for class_path in layer_ads_dir.glob("*/src/*.xml"): + try: + model = ET.fromstring(class_path.read_bytes()) + except ParseError as e: + print(f"[#] Invalid XML by path {str(class_path)!r}\nError: {e}\n") + continue + + for task_el in model.findall(".//xsc:Task", namespaces=NS): + assignee: str = task_el.attrib["Assignee"].strip() + if not assignee: + assignee = "" + + assignee_by_number[assignee] += 1 + + task_type: str = task_el.attrib["Type"] + type_by_number[task_type] += 1 + + behavior: str = task_el.attrib["Behavior"] + behavior_by_number[behavior] += 1 + + indent1: str = " " + + print("Total:", sum(assignee_by_number.values())) + print() + + print(f"assignee_by_number ({len(assignee_by_number)}):") + for assignee, number in sorted( + assignee_by_number.items(), key=lambda x: x[1], reverse=True + ): + print(f"{indent1}{assignee}: {number}") + print() + + print(f"type_by_number ({len(type_by_number)}):") + for task_type, number in sorted( + type_by_number.items(), key=lambda x: x[1], reverse=True + ): + print(f"{indent1}{task_type}: {number}") + print() + + print(f"behavior_by_number ({len(behavior_by_number)}):") + for behavior, number in sorted( + behavior_by_number.items(), key=lambda x: x[1], reverse=True + ): + print(f"{indent1}{behavior}: {number}") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Count TODO, NOTE, FIXME of ADS classes" + ) + parser.add_argument( + "path_trunk", + metavar="/PATH/TO/TRUNK", + help="Path to source tree (the directory containing branch.xml)", + ) + args = parser.parse_args() + + process(args.path_trunk) diff --git a/job_compassplus/tx_parse_xml/counting_size_bin_of_ADS.py b/job_compassplus/tx_parse_xml/counting_size_bin_of_ADS.py new file mode 100644 index 000000000..bdf444a5c --- /dev/null +++ b/job_compassplus/tx_parse_xml/counting_size_bin_of_ADS.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from collections import Counter + +# pip install humanize +from humanize import naturalsize as sizeof_fmt + + +def process(path_layer: str): + if isinstance(path_layer, str): + path_layer = Path(path_layer) + + if not path_layer.exists(): + raise Exception(f"Not exists: {path_layer}") + + total_files: int = 0 + module_by_size = Counter() + + for path_bin in Path(path_layer).glob("ads/*/bin"): + module: str = path_bin.parent.name + sizes: list[int] = [f.stat().st_size for f in path_bin.glob("*.jar")] + if not sizes: + continue + + total_files += len(sizes) + module_by_size[module] = sum(sizes) + + print(f"Total modules: {len(module_by_size)}") + print(f"Total files: {total_files}") + print("Top10 modules by size:") + for module, size in module_by_size.most_common(10): + print(f" {module}: {sizeof_fmt(size)} ({size} bytes)") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Counting size bin of ADS modules" + ) + parser.add_argument( + "path_layer", + metavar="/PATH/TO/TRUNK/LAYER", + help="Path to layer (the directory containing layer.xml)", + ) + args = parser.parse_args() + + process(args.path_layer) diff --git a/job_compassplus/tx_parse_xml/export_ids.py b/job_compassplus/tx_parse_xml/export_ids.py new file mode 100644 index 000000000..e83ff4bb7 --- /dev/null +++ b/job_compassplus/tx_parse_xml/export_ids.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from pathlib import Path + + +PATTERN_ID: re.Pattern = re.compile(rb'\bId="([a-zA-Z]{3}\w+)"') + + +def process(path_layer: Path, path_extract: Path): + if not path_layer.exists(): + raise Exception(f"Not exists: {path_layer}") + + items: set[bytes] = set() + for f in path_layer.rglob("*.xml"): + # TODO: Добавить статистику: файлы, количество найденных + ids: list[bytes] = PATTERN_ID.findall(f.read_bytes()) + print(f"{f} ids: {len(ids)}") + items.update(ids) + + with open(path_extract, "wb") as f: + for value in items: + f.write(value + b"\n") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Exporting IDs to a file" + ) + parser.add_argument( + "path_layer", + metavar="/PATH/TO/TRUNK/LAYER", + help="Path to layer (the directory containing layer.xml)", + type=Path, + ) + parser.add_argument( + "path_extract", + help="Path to extract", + type=Path, + ) + args = parser.parse_args() + + process(args.path_layer, args.path_extract) diff --git a/job_compassplus/tx_parse_xml/gen_svn_ignore_layers.py b/job_compassplus/tx_parse_xml/gen_svn_ignore_layers.py new file mode 100644 index 000000000..9bc577412 --- /dev/null +++ b/job_compassplus/tx_parse_xml/gen_svn_ignore_layers.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import fnmatch +import argparse + +from pathlib import Path + + +def process( + root_path: Path, + include_glob: str, + exclude_patterns: list[str], + encoding: str = "utf-8", +) -> None: + file_exclude: Path = root_path / "svn_layers_exclude.bat" + file_restore: Path = root_path / "svn_layers_restore.bat" + + print(f"Scanning directory: {root_path.absolute()}") + + target_dirs: list[Path] = [p for p in root_path.glob(include_glob) if p.is_dir()] + if not target_dirs: + print("No matching directories found.") + return + + # Подготавливаем списки строк для записи + lines_exclude: list[str] = ["@echo off"] + lines_restore: list[str] = ["@echo off"] + + for p in target_dirs: + dir_name: str = p.name + is_excluded: bool = any( + fnmatch.fnmatch(dir_name, pat) for pat in exclude_patterns + ) + + prefix: str = "REM " if is_excluded else "" + lines_exclude.append(f"{prefix}svn update --set-depth exclude {dir_name}") + lines_restore.append(f"{prefix}svn update --set-depth infinity {dir_name}") + + file_exclude.write_text("\n".join(lines_exclude) + "\n", encoding=encoding) + file_restore.write_text("\n".join(lines_restore) + "\n", encoding=encoding) + + print(f"Done! Created '{file_exclude.name}' and '{file_restore.name}'") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Generates SVN batch scripts to manage folder depth (exclude/include).", + ) + parser.add_argument( + "path", + metavar="PROJECT_PATH", + type=Path, + help="Path to the project directory", + ) + parser.add_argument( + "--include", + default="com.tranzaxis.*", + help="Glob pattern for directories to process (default: %(default)s)", + ) + parser.add_argument( + "--excludes", + nargs="+", + default=["com.tranzaxis.experimental", "com.tranzaxis.demo"], + help="Globs for directories to comment out in exclude script (default: %(default)s)", + ) + + args = parser.parse_args() + + if not args.path.exists(): + parser.error(f"The path {args.path} does not exist.") + + process(args.path, args.include, args.excludes) diff --git a/job_compassplus/tx_parse_xml/generate_new_licenses_of_layer.py b/job_compassplus/tx_parse_xml/generate_new_licenses_of_layer.py new file mode 100644 index 000000000..334458c19 --- /dev/null +++ b/job_compassplus/tx_parse_xml/generate_new_licenses_of_layer.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +ITEMS = [ + "W000", + "W001", + "W002", + "W003", + "W004", + "W005", + "W006", + "W007", + "W008", + "W009", + "W010", + "W011", + "W012", + "W014", + "W015", + "W016", + "W017", + "W018", + "W019", + "W021", + "W022", + "W023", + "W024", + "W025", + "W026", + "W027", + "W028", + "W029", + "W032", + "W033", + "W034", + "W035", + "W036", + "W037", +] + +text = "" + +for name in ITEMS: + text += f"""\ + + + +""" + +print(text) diff --git a/job_compassplus/tx_parse_xml/get_layer_version.py b/job_compassplus/tx_parse_xml/get_layer_version.py new file mode 100644 index 000000000..920a34654 --- /dev/null +++ b/job_compassplus/tx_parse_xml/get_layer_version.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from pathlib import Path + + +def get_layer_version(path: Path) -> str: + path = path.resolve() + + if not path.is_dir(): + raise Exception(f"Path {str(path)!r} must be directory") + + path = path / "layer.xml" + if not path.exists(): + raise Exception(f"Path {str(path)!r} is not exists!") + + m = re.search(r'\bReleaseNumber="(.+?)"', path.read_text("utf-8")) + if not m: + raise Exception(f'Not found "ReleaseNumber" in path {str(path)!r}!') + + return m.group(1) + + +def process(path: Path) -> None: + print(get_layer_version(path)) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Print a layer version" + ) + parser.add_argument( + "path", + metavar="/PATH/TO/LAYER", + type=Path, + help="Path to layer (the directory containing layer.xml)", + ) + args = parser.parse_args() + + process(args.path) diff --git a/job_compassplus/tx_parse_xml/get_project_versions.py b/job_compassplus/tx_parse_xml/get_project_versions.py new file mode 100644 index 000000000..2612cc5c6 --- /dev/null +++ b/job_compassplus/tx_parse_xml/get_project_versions.py @@ -0,0 +1,329 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import subprocess + +from dataclasses import dataclass, field, fields +from pathlib import Path +from typing import Optional, Any + + +def get_diff_fields(obj1, obj2) -> dict[str, tuple[Any, Any]]: + """ + Compares two dataclass instances and returns a dictionary of differing fields. + The dictionary keys are the field names, and values are a tuple + (value_in_obj1, value_in_obj2). + """ + + if not isinstance(obj1, type(obj2)): + raise TypeError("Both objects must be instances of the same dataclass.") + + diffs: dict[str, tuple[Any, Any]] = dict() + for field_info in fields(obj1): + field_name: str = field_info.name + value1: Any = getattr(obj1, field_name) + value2: Any = getattr(obj2, field_name) + + if value1 != value2: + diffs[field_name] = value1, value2 + + return diffs + + +def parse_xml_attrs(text: str) -> dict[str, str]: + return dict(re.findall(r'(\w+)\s*=\s*"(.+?)"', text)) + + +def parse_line_xml_text(text: str, line_contains: list[str]) -> dict[str, str]: + for line in text.splitlines(): + if all(x in line for x in line_contains): + return parse_xml_attrs(line) + + return dict() + + +def get_remote_file_from_svn(file_name: Path) -> str: + try: + result: bytes = subprocess.check_output( + args=["svn", "info", file_name], + stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError as e: + # NOTE: svn: warning: W155010: The node 'xxx' was not found. + if b"W155010" in e.stderr: + print(f"[#] Не найден {file_name} в SVN") + return "" + + raise e + + url_svn_file: str | None = None + for line in result.splitlines(): + parts: list[bytes] = line.split(b"URL:", maxsplit=1) + if len(parts) < 2: + continue + + url_svn_file = parts[1].strip().decode("ascii") + break + + if not url_svn_file: + raise Exception( + f"Не удалось получить URL файла {file_name} в репозитории SVN из результата svn info:\n{result!r}" + ) + + return subprocess.check_output( + args=["svn", "cat", url_svn_file], + encoding="utf-8", + ) + + +@dataclass +class BranchInfo: + # TODO: Не все поля были поддержаны + base_dev_uri: str + type: str + base_release: str | None + last_release: str | None + + @classmethod + def parse_from_dict(cls, d: dict[str, str]) -> "BranchInfo": + return cls( + base_dev_uri=d["BaseDevUri"], + type=d["Type"], + base_release=d.get("BaseRelease"), + last_release=d.get("LastRelease"), + ) + + @classmethod + def parse_from_text(cls, text: str) -> Optional["BranchInfo"]: + xml_attrs: dict[str, str] = parse_line_xml_text( + text=text, + line_contains=["Branch", "BaseDevUri", "Type"], + ) + if xml_attrs: + return cls.parse_from_dict(xml_attrs) + + +@dataclass +class LayerInfo: + # TODO: Не все поля были поддержаны + uri: str + name: str + release_number: str | None + base_layer_uris: list[str] = field(default_factory=list) + + @classmethod + def parse_from_dict(cls, d: dict[str, str]) -> "LayerInfo": + return cls( + uri=d["Uri"], + name=d["Name"], + release_number=d.get("ReleaseNumber"), + base_layer_uris=d.get("BaseLayerURIs", "").split(), + ) + + @classmethod + def parse_from_text(cls, text: str) -> Optional["LayerInfo"]: + xml_attrs: dict[str, str] = parse_line_xml_text( + text=text, + line_contains=["Layer", "Uri", "Name"], + ) + if xml_attrs: + return cls.parse_from_dict(xml_attrs) + + +@dataclass +class ProjectInfo: + branch: BranchInfo | None + layers: list[LayerInfo] + + +@dataclass +class TotalProjectInfo: + local: ProjectInfo + remote: ProjectInfo + + +def collect_all_layer_infos( + path_layer: Path, + layer_infos: list[LayerInfo], + remote: bool = False, +): + if not path_layer.exists(): + raise Exception(f"Путь {path_layer} не существует!") + + layer_info: LayerInfo | None = LayerInfo.parse_from_text( + get_remote_file_from_svn(path_layer) + if remote + else path_layer.read_text(encoding="utf-8") + ) + + if not layer_info: + text = f"Не удалось получить информацию из {path_layer}!" + if not remote: + raise Exception(f"{text} (local)") + + print(f"[#] {text} (remote)") + return + + if layer_info in layer_infos: + print(f"[#] Обнаружилось зацикливание с {layer_info}") + return + + layer_infos.append(layer_info) + + if not layer_info.base_layer_uris: + return + + for base_layer_uri in layer_info.base_layer_uris: + path_layer = path_layer.parent.parent / base_layer_uri / "layer.xml" + collect_all_layer_infos(path_layer, layer_infos, remote=remote) + + +def get_project_versions(path: Path) -> TotalProjectInfo: + path = path.resolve() + + if not path.is_dir(): + raise Exception(f"Путь {path} должен быть директорией") + + path_branch = path / "branch.xml" + if not path_branch.exists(): + raise Exception(f"Не существует: {path_branch}") + + branch_info_local: BranchInfo | None = BranchInfo.parse_from_text( + path_branch.read_text(encoding="utf-8") + ) + if not branch_info_local: + raise Exception( + f"Не удалось получить информацию из {branch_info_local}! (local)" + ) + + branch_info_remote: BranchInfo | None = BranchInfo.parse_from_text( + get_remote_file_from_svn(path_branch) + ) + if not branch_info_remote: + print(f"[#] Не удалось получить информацию из {branch_info_remote}! (remote)") + + path_base_dev_layer = path / branch_info_local.base_dev_uri / "layer.xml" + + layer_infos_local: list[LayerInfo] = [] + collect_all_layer_infos(path_base_dev_layer, layer_infos_local, remote=False) + + layer_infos_remote: list[LayerInfo] = [] + collect_all_layer_infos(path_base_dev_layer, layer_infos_remote, remote=True) + + return TotalProjectInfo( + local=ProjectInfo( + branch=branch_info_local, + layers=layer_infos_local, + ), + remote=ProjectInfo( + branch=branch_info_remote, + layers=layer_infos_remote, + ), + ) + + +def process(path: Path) -> None: + print(f"Информация по версиям из {path}") + + total_project_info: TotalProjectInfo = get_project_versions(path) + + if not total_project_info.local.branch: + print() + print("[#] Local branch: <нет информации>") + elif not total_project_info.remote.branch: + print() + print("[#] Remote branch: <нет информации>") + else: + diff_branches: dict[str, tuple[Any, Any]] = get_diff_fields( + total_project_info.local.branch, total_project_info.remote.branch + ) + if diff_branches: + print() + + print("Обнаружена разница в полях Branch между local и remote:") + for field_name, (value_local, value_remote) in diff_branches.items(): + print(f" {field_name}: {value_local} != {value_remote}") + + @dataclass + class DiffLayersResult: + local: LayerInfo | None = None + remote: LayerInfo | None = None + + uri_by_layers: dict[str, DiffLayersResult] = { + uri: DiffLayersResult() + for uri in set( + l.uri + for l in ( + total_project_info.local.layers + total_project_info.remote.layers + ) + ) + } + + for l in total_project_info.local.layers: + uri_by_layers[l.uri].local = l + for l in total_project_info.remote.layers: + uri_by_layers[l.uri].remote = l + + diff_layers_local: list[LayerInfo] = [] + diff_layers_remote: list[LayerInfo] = [] + for uri, layers in uri_by_layers.items(): + if layers.local and layers.remote: + diff_layers: dict[str, tuple[Any, Any]] = get_diff_fields( + layers.local, layers.remote + ) + if diff_layers: + print() + print(f"Обнаружена разница в полях Layer ({uri}) между local и remote:") + for field_name, (value_local, value_remote) in diff_layers.items(): + print(f" {field_name}: {value_local} != {value_remote}") + + elif layers.local: + diff_layers_remote.append(layers.local) + elif layers.remote: + diff_layers_local.append(layers.remote) + + if diff_layers_local: + print() + print(f"В Local нет слоев из Remote ({len(diff_layers_local)}):") + for l in diff_layers_local: + print(f" {l}") + + if diff_layers_remote: + print() + print(f"В Local нет слоев из Remote ({len(diff_layers_remote)}):") + for l in diff_layers_remote: + print(f" {l}") + + print() + print("Версии:") + + def _print_project_info(project_info: ProjectInfo, is_local: bool) -> None: + ind1: str = " " + print(ind1 + ("Local:" if is_local else "Remote:")) + print(f"{ind1 * 2}Branch: {project_info.branch}") + print(f"{ind1 * 2}Layers:") + for layer in project_info.layers: + print(f"{ind1 * 3}{layer}") + + _print_project_info(total_project_info.local, is_local=True) + print() + _print_project_info(total_project_info.remote, is_local=False) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Print a layer version") + parser.add_argument( + "path", + metavar="/PATH/TO/PROJECT", + type=Path, + help="Path to project (the directory containing branch.xml)", + ) + args = parser.parse_args() + + process(args.path) diff --git a/job_compassplus/tx_parse_xml/module_id_by_name.py b/job_compassplus/tx_parse_xml/module_id_by_name.py new file mode 100644 index 000000000..286ad1be2 --- /dev/null +++ b/job_compassplus/tx_parse_xml/module_id_by_name.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from show_maintainers import parse_xml_partially + + +def get_module_id_by_name(project_dir: str | Path) -> dict[str, str]: + if isinstance(project_dir, str): + project_dir = Path(project_dir) + + module_id_by_name: dict[str, str] = dict() + for layer_path in project_dir.glob("*/layer.xml"): + for path in layer_path.parent.glob("ads/*/module.xml"): + root_module = parse_xml_partially( + path, line_contains=["Module ", " Id", " Name"] + ) + module_id = root_module.module["id"] + if module_id not in module_id_by_name: + module_id_by_name[module_id] = root_module.module["name"] + + return module_id_by_name + + +if __name__ == "__main__": + project_dir = "C:/DEV__TX/trunk" + owner_by_modules = get_module_id_by_name(project_dir) + + for module_id, module_name in owner_by_modules.items(): + print(f"{module_id} = {module_name}") diff --git a/job_compassplus/tx_parse_xml/save_maintainers.py b/job_compassplus/tx_parse_xml/save_maintainers.py new file mode 100644 index 000000000..831180160 --- /dev/null +++ b/job_compassplus/tx_parse_xml/save_maintainers.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from show_maintainers import get_owner_by_modules + + +project_dir = "C:/DEV__TX/trunk_tx" + +with open("maintainers.txt", "w", encoding="utf-8") as f: + owner_by_modules = get_owner_by_modules(project_dir) + + lines = [] + for email in sorted(owner_by_modules): + modules = owner_by_modules[email] + modules.sort() + + lines.append(f"{email} ({len(modules)}):") + for module in modules: + lines.append(f" {module}") + lines.append("") + + text = "\n".join(lines).strip() + f.write(text) diff --git a/job_compassplus/tx_parse_xml/search_in_locale_files_of_ADS.py b/job_compassplus/tx_parse_xml/search_in_locale_files_of_ADS.py new file mode 100644 index 000000000..710166a3e --- /dev/null +++ b/job_compassplus/tx_parse_xml/search_in_locale_files_of_ADS.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import xml.etree.ElementTree as ET + +from pathlib import Path + + +NS = dict( + ads="http://schemas.radixware.org/adsdef.xsd", +) + + +def get_text(el: ET.Element) -> str: + if el is None: + return "" + return "".join(text for text in el.itertext()) + + +def process(path: str, regexp: str) -> None: + pattern = re.compile(regexp) + + for p in Path(path).glob("*/ads/*/locale/*/mlb*.xml"): + try: + mlb = ET.fromstring(p.read_bytes()) + except Exception: + # Ignore + continue + + items = [] + for string_el in mlb.findall(".//ads:String", namespaces=NS): + string_value_el = string_el.find("./ads:Value", namespaces=NS) + string_value = get_text(string_value_el) + + if pattern.search(string_value): + string_id = string_el.attrib["Id"] + string_lang = string_value_el.attrib["Language"] + + items.append(f"Found with id={string_id} language={string_lang}: {string_value!r}") + + if items: + items.sort() + print(*items, sep="\n") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Search in locale" + ) + parser.add_argument( + "path_trunk", + metavar="/PATH/TO/TRUNK", + help="Path to TX source tree (the directory containing branch.xml)", + ) + parser.add_argument( + "regexp", + help="Regular expression", + ) + args = parser.parse_args() + + process(args.path_trunk, args.regexp) diff --git a/job_compassplus/tx_parse_xml/show_SQL_of_ADS_classes.py b/job_compassplus/tx_parse_xml/show_SQL_of_ADS_classes.py new file mode 100644 index 000000000..f57c651b4 --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_SQL_of_ADS_classes.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import xml.etree.ElementTree as ET + +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class ADS: + title: str + props: dict[str, list[str]] + editors: dict[str, list[str]] + selectors: dict[str, list[str]] + filters: dict[str, list[str]] + sortings: dict[str, list[str]] + + def __bool__(self) -> bool: + return bool( + self.props + or self.editors + or self.selectors + or self.filters + or self.sortings + ) + + +NS = dict( + ads="http://schemas.radixware.org/adsdef.xsd", + xsc="http://schemas.radixware.org/xscml.xsd", +) + + +def has_text(el: ET.Element) -> bool: + return any(text.strip() for text in el.itertext()) + + +def get_xml_object_title(el: ET.Element) -> str: + el_id = el.attrib["Id"] + el_name = el.attrib["Name"] + return f"{el_name}({el_id})" + + +def get_databases_specific_sqml_expression(el: ET.Element, tag_name: str) -> list[str]: + items = [] + if not el: + return items + + specific_sqml_expression_el = el.findall(f"./ads:{tag_name}", namespaces=NS) + for sqml_el in specific_sqml_expression_el: + if sqml_el and has_text(sqml_el.find("./xsc:Content", namespaces=NS)): + database = sqml_el.find("./xsc:Database", namespaces=NS).text + items.append(database) + + return items + + +def get_exists_condition_names(condition_el: ET.Element) -> list[str]: + items = [] + if not condition_el: + return items + + condition_where_el = condition_el.find("./ads:ConditionWhere", namespaces=NS) + if condition_where_el and has_text(condition_where_el): + items.append("ConditionWhere") + + condition_from_el = condition_el.find("./ads:ConditionFrom", namespaces=NS) + if condition_from_el and has_text(condition_from_el): + items.append("ConditionFrom") + + for tag_name in ["SpecificConditionWhere", "SpecificConditionFrom"]: + for database in get_databases_specific_sqml_expression(condition_el, tag_name): + items.append(f"{tag_name}={database}") + + return items + + +def get_ads_filters(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + for filter_el in ads_el.findall( + "./ads:Presentations/ads:Filters/ads:Filter", namespaces=NS + ): + filter_title = get_xml_object_title(filter_el) + + condition_el = filter_el.find("./ads:Condition", namespaces=NS) + if condition_el and has_text(condition_el): + obj_by_items[filter_title].append("Condition") + + condition_from_el = filter_el.find("./ads:ConditionFrom", namespaces=NS) + if condition_from_el and has_text(condition_from_el): + obj_by_items[filter_title].append("ConditionFrom") + + hint_el = filter_el.find("./ads:Hint", namespaces=NS) + if hint_el and has_text(hint_el): + obj_by_items[filter_title].append("Hint") + + for tag_name in ["SpecificCondition", "SpecificConditionFrom", "SpecificHint"]: + for database in get_databases_specific_sqml_expression(filter_el, tag_name): + obj_by_items[filter_title].append(f"{tag_name}={database}") + + enabled_sorting_el = filter_el.find("./ads:EnabledSorting", namespaces=NS) + if enabled_sorting_el: + enabled_sorting_hint_el = enabled_sorting_el.find( + "./ads:Hint", namespaces=NS + ) + if enabled_sorting_hint_el and has_text(enabled_sorting_hint_el): + obj_by_items[filter_title].append("EnabledSorting/Hint") + + for database in get_databases_specific_sqml_expression( + enabled_sorting_el, "SpecificHint" + ): + obj_by_items[filter_title].append( + f"EnabledSorting/SpecificHint={database}" + ) + + return obj_by_items + + +def get_ads_props(ads_el: ET.Element) -> dict[str, list[str]]: + prop_by_items = defaultdict(list) + + for prop_el in ads_el.findall("./ads:Properties/ads:Property", namespaces=NS): + prop_title = get_xml_object_title(prop_el) + + sqml_expression_el = prop_el.find("./ads:SqmlExpression", namespaces=NS) + if sqml_expression_el and has_text(sqml_expression_el): + prop_by_items[prop_title].append("SqmlExpression") + + for database in get_databases_specific_sqml_expression( + prop_el, "SpecificSqmlExpression" + ): + prop_by_items[prop_title].append(f"SpecificSqmlExpression={database}") + + parent_select_condition_el = prop_el.find( + "./ads:Presentation/ads:ParentSelect/ads:ParentSelectCondition", + namespaces=NS, + ) + for condition_name in get_exists_condition_names(parent_select_condition_el): + prop_by_items[prop_title].append(f"ParentSelect/{condition_name}") + + return prop_by_items + + +def get_ads_editor_presentations(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + for presentation_el in ads_el.findall( + "./ads:Presentations/ads:EditorPresentations/ads:EditorPresentation", + namespaces=NS, + ): + editor_title = get_xml_object_title(presentation_el) + + for child_item_ref_el in presentation_el.findall( + "./ads:ChildExplorerItems/ads:Item/ads:ChildRef", namespaces=NS + ): + condition_el = child_item_ref_el.find("./ads:Condition", namespaces=NS) + child_sqml_items = get_exists_condition_names(condition_el) + if child_sqml_items: + child_ref_title = get_xml_object_title(child_item_ref_el) + obj_by_items[editor_title].append( + f'{child_ref_title}: {", ".join(child_sqml_items)}' + ) + + for child_item_entity_el in presentation_el.findall( + "./ads:ChildExplorerItems/ads:Item/ads:Entity", namespaces=NS + ): + condition_el = child_item_entity_el.find("./ads:Condition", namespaces=NS) + child_sqml_items = get_exists_condition_names(condition_el) + if child_sqml_items: + child_entity_title = get_xml_object_title(child_item_entity_el) + obj_by_items[editor_title].append( + f'{child_entity_title}: {", ".join(child_sqml_items)}' + ) + + return obj_by_items + + +def get_ads_selector_presentations(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + for presentation_el in ads_el.findall( + "./ads:Presentations/ads:SelectorPresentations/ads:SelectorPresentation", + namespaces=NS, + ): + selector_title = get_xml_object_title(presentation_el) + + condition_el = presentation_el.find("./ads:Condition", namespaces=NS) + for condition_name in get_exists_condition_names(condition_el): + obj_by_items[selector_title].append(condition_name) + + addons_el = presentation_el.find("./ads:Addons", namespaces=NS) + if addons_el: + default_hint_el = addons_el.find("./ads:DefaultHint", namespaces=NS) + if default_hint_el and has_text(default_hint_el): + obj_by_items[selector_title].append("Addons/DefaultHint") + + specific_hint_el = addons_el.find("./ads:SpecificHint", namespaces=NS) + for database in get_databases_specific_sqml_expression( + specific_hint_el, "SpecificDefaultHint" + ): + obj_by_items[selector_title].append( + f"Addons/SpecificDefaultHint={database}" + ) + + return obj_by_items + + +def get_ads_sortings(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + sorting_el = ads_el.find( + "./ads:Presentations/ads:Sortings/ads:Sorting", namespaces=NS + ) + if sorting_el: + sorting_title = get_xml_object_title(sorting_el) + + hint_el = sorting_el.find("./ads:Hint", namespaces=NS) + if hint_el and has_text(hint_el): + obj_by_items[sorting_title].append("Hint") + + specific_hint_el = sorting_el.find("./ads:SpecificHint", namespaces=NS) + for database in get_databases_specific_sqml_expression( + specific_hint_el, "SpecificHint" + ): + obj_by_items[sorting_title].append(f"SpecificHint={database}") + + return obj_by_items + + +def get_ads(model_path: Path) -> ADS | None: + model = ET.fromstring(model_path.read_text(encoding="utf-8")) + ads_el = model.find("./ads:AdsClassDefinition", namespaces=NS) + if not ads_el: + return + + return ADS( + title=get_xml_object_title(ads_el), + props=get_ads_props(ads_el), + editors=get_ads_editor_presentations(ads_el), + selectors=get_ads_selector_presentations(ads_el), + filters=get_ads_filters(ads_el), + sortings=get_ads_sortings(ads_el), + ) + + +def get_ads_list(branch_dir: Path | str) -> dict[str, list[ADS]]: + if isinstance(branch_dir, str): + branch_dir = Path(branch_dir) + + if not branch_dir.exists(): + raise Exception(f"Not exists: {branch_dir}") + + layer_module_by_ads_list = defaultdict(list) + + for layer_dir in branch_dir.glob("*"): + if not layer_dir.is_dir(): + continue + + layer_ads_dir = layer_dir / "ads" + if layer_ads_dir.is_dir(): + layer = layer_dir.name + for module_path in layer_ads_dir.glob("*/src"): + module = module_path.parent.name + layer_module = f"{layer}/{module}" + + for class_path in module_path.glob("*.xml"): + if ads := get_ads(class_path): + layer_module_by_ads_list[layer_module].append(ads) + + return layer_module_by_ads_list + + +def process(path: str) -> None: + indent1 = " " + indent2 = indent1 * 2 + indent3 = indent1 * 3 + indent4 = indent1 * 4 + for layer_module, ads_list in get_ads_list(path).items(): + print(layer_module) + + for ads in ads_list: + print(indent1 + ads.title) + + prop_by_sqmls = ads.props + if prop_by_sqmls: + print(indent2 + "Props:") + for prop, items in prop_by_sqmls.items(): + print(indent3 + f'{prop}: {", ".join(items)}') + + editor_by_sqmls = ads.editors + if editor_by_sqmls: + print(indent2 + "Editor presentations:") + for editor, items in editor_by_sqmls.items(): + print(indent3 + f"{editor}:") + for sqml in items: + print(indent4 + f"{sqml}") + + selector_by_sqmls = ads.selectors + if selector_by_sqmls: + print(indent2 + "Selector presentations:") + for selector, items in selector_by_sqmls.items(): + print(indent3 + f'{selector}: {", ".join(items)}') + + filter_by_sqmls = ads.filters + if filter_by_sqmls: + print(indent2 + "Filters:") + for filter_name, items in filter_by_sqmls.items(): + print(indent3 + f'{filter_name}: {", ".join(items)}') + + sorting_by_sqmls = ads.sortings + if sorting_by_sqmls: + print(indent2 + "Sortings:") + for sorting, items in sorting_by_sqmls.items(): + print(indent3 + f'{sorting}: {", ".join(items)}') + + print() + + print("\n" + "-" * 100 + "\n") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Shows ADS entities with SQL definitions" + ) + parser.add_argument( + "path_trunk", + metavar="/PATH/TO/TRUNK", + help="Path to TX source tree (the directory containing branch.xml)", + ) + args = parser.parse_args() + + process(args.path_trunk) diff --git a/job_compassplus/tx_parse_xml/show_ads_modules_without_maintainer.py b/job_compassplus/tx_parse_xml/show_ads_modules_without_maintainer.py new file mode 100644 index 000000000..11713a16f --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_ads_modules_without_maintainer.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import xml.etree.ElementTree as ET +from pathlib import Path + + +def get_module_ids_with_maintainter(path_branch: str | Path) -> list[str]: + ns = dict( + p="http://schemas.radixware.org/product.xsd", + ) + + ids: list[str] = [] + + branch = ET.fromstring(path_branch.read_bytes()) + for module_info_el in branch.findall(".//p:ModuleInfo", namespaces=ns): + module_id = module_info_el.find("./p:ModuleId", namespaces=ns).text + has_maintainters = module_info_el.find(".//p:OwnerEmail", namespaces=ns) is not None + if has_maintainters and module_id not in ids: + ids.append(module_id) + + return ids + + +def process(path_layer: str): + if isinstance(path_layer, str): + path_layer = Path(path_layer) + + if not path_layer.exists(): + raise Exception(f"Not exists: {path_layer}") + + path_branch = path_layer.parent / "branch.xml" + if not path_branch.exists(): + raise Exception(f"Not exists: {path_branch}") + + module_ids: list[str] = get_module_ids_with_maintainter(path_branch) + + deprecated_module_without_maintainters: list[tuple[str, str]] = [] + module_without_maintainters: list[tuple[str, str]] = [] + + for path_module in path_layer.glob("ads/*/module.xml"): + module_name = path_module.parent.name + + module_el = ET.fromstring(path_module.read_bytes()) + module_id = module_el.get("Id") + if module_id in module_ids: + continue + + is_deprecated = module_el.get("IsDeprecated") == "true" + if is_deprecated: + deprecated_module_without_maintainters.append((module_name, module_id)) + else: + module_without_maintainters.append((module_name, module_id)) + + print(f"Modules ({len(module_without_maintainters)}):") + for module_name, module_id in module_without_maintainters: + print(f" {module_name} ({module_id})") + + print() + + print(f"Deprecated modules ({len(deprecated_module_without_maintainters)}):") + for module_name, module_id in deprecated_module_without_maintainters: + print(f" {module_name} ({module_id})") + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Show ads modules without maintainer" + ) + parser.add_argument( + "path_layer", + metavar="/PATH/TO/TRUNK/LAYER", + help="Path to layer (the directory containing layer.xml)", + ) + args = parser.parse_args() + + process(args.path_layer) diff --git a/job_compassplus/tx_parse_xml/show_db_columns_tables_with_DefaultVal_as_Expression.py b/job_compassplus/tx_parse_xml/show_db_columns_tables_with_DefaultVal_as_Expression.py new file mode 100644 index 000000000..3789a9c9d --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_db_columns_tables_with_DefaultVal_as_Expression.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import xml.etree.ElementTree as ET + +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class Column: + id: str + name: str + default_value: str + + +def get_table_by_columns(model_path: Path) -> dict[str, list[Column]]: + ns = dict( + dds="http://schemas.radixware.org/ddsdef.xsd", + com="http://schemas.radixware.org/commondef.xsd", + ) + + table_by_columns = defaultdict(list) + model = ET.fromstring(model_path.read_text(encoding="utf-8")) + for table in model.findall(".//dds:Tables/dds:Table", namespaces=ns): + table_title = f"{table.attrib['Name']}({table.attrib['Id']})" + + for column in table.findall("./dds:Columns/dds:Column", namespaces=ns): + default_value = column.find( + "./dds:DefaultVal[@Type='Expression']/com:Value", namespaces=ns + ) + if default_value is not None: + table_by_columns[table_title].append( + Column( + id=column.attrib["Id"], + name=column.attrib["Name"], + default_value=default_value.text.strip(), + ) + ) + + return table_by_columns + + +def process(branch_dir: Path | str) -> dict[str, dict[str, list[Column]]]: + if isinstance(branch_dir, str): + branch_dir = Path(branch_dir) + + layer_module_by_tables = defaultdict(dict) + + for layer_dir in branch_dir.glob("*"): + if not layer_dir.is_dir(): + continue + + layer_dds_dir = layer_dir / "dds" + if layer_dds_dir.is_dir(): + layer = layer_dir.name + for model_xml in layer_dds_dir.glob("*/model.xml"): + module = model_xml.parent.name + + for table, columns in get_table_by_columns(model_xml).items(): + key = f"{layer}/{module}" + layer_module_by_tables[key][table] = columns + + return layer_module_by_tables + + +if __name__ == "__main__": + path = r"C:\DEV__OPTT\trunk_optt" + + for key, table_by_columns in process(path).items(): + print(f"{key} ({len(table_by_columns)})") + for i, (table, columns) in enumerate(table_by_columns.items(), 1): + print(f" {i}. {table}:") + for column in columns: + print(f" {column.name}({column.id}) = {column.default_value!r}") + print() + """ + com.optt/ProtocolSetup (7) + 1. Protocol(tblJBLJOL4TFBAHLBEWZMLSHXYGMM): + extGuid(colW44QAAJJP5ELXOLWL5KKHTLUKI) = 'sys_guid()' + 2. ProtocolField(tblE26CRSMVJZCJVF4ZMR56KUYTAM): + extGuid(colRO63O6OLMRBSNJRIBSSV3HRVOQ) = 'sys_guid()' + 3. ProtocolStateAttribute(tblFP3C5RQFDFFWJAJQMYVXSQ3SEY): + extGuid(col3MK2X5H3ZZBVVJB3AYDMMDCWHQ) = 'sys_guid()' + 4. MessageType(tbl2DX3TB7ZUZE4RG3B7BY6HS4EZI): + extGuid(col73V3MXLEKFD4BASZ2JXAEB4X4A) = 'sys_guid()' + 5. Rule(tblE4HU6SKERVDMHJBSB4D6SCPUVQ): + extGuid(colAN5WEW42GBATLDBNPREXMUY5PU) = 'sys_guid()' + 6. FieldValueGenerator(tblGU3P5FK7PZD4VLOS4XR5236L6Q): + extGuid(colBDVB7VL5ARAVRLSYA2J5YEMBFU) = 'sys_guid()' + 7. ProtocolVersion(tblQLF47J7P4JCY5E6JFJ62NJBTRQ): + extGuid(colJLFFQTXRX5FFJG6AG3AL4MFNDM) = 'sys_guid()' + """ diff --git a/job_compassplus/tx_parse_xml/show_db_custom_triggers.py b/job_compassplus/tx_parse_xml/show_db_custom_triggers.py new file mode 100644 index 000000000..c0e63725e --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_db_custom_triggers.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import xml.etree.ElementTree as ET + +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class Trigger: + table_name: str + name: str + db_name: str + + +def get_triggers(model_path: Path) -> list[Trigger]: + ns = dict( + dds="http://schemas.radixware.org/ddsdef.xsd", + ) + + items = [] + model = ET.fromstring(model_path.read_text(encoding="utf-8")) + for table in model.findall(".//dds:Tables/dds:Table", namespaces=ns): + for trigger in table.findall("./dds:Triggers/dds:Trigger", namespaces=ns): + if trigger.attrib.get( + "Type" + ): # При True - триггер был создан автоматически радиксом + continue + + items.append( + Trigger( + table_name=table.attrib["Name"], + name=trigger.attrib["Name"], + db_name=trigger.attrib["DbName"], + ) + ) + + return items + + +def process(branch_dir: Path | str) -> dict[str, list[Trigger]]: + if isinstance(branch_dir, str): + branch_dir = Path(branch_dir) + + layer_module_by_triggers = defaultdict(list) + + for layer_dir in branch_dir.glob("*"): + if not layer_dir.is_dir(): + continue + + layer_dds_dir = layer_dir / "dds" + if layer_dds_dir.is_dir(): + layer = layer_dir.name + for model_xml in layer_dds_dir.glob("*/model.xml"): + module = model_xml.parent.name + + for trigger in get_triggers(model_xml): + key = f"{layer}/{module}" + layer_module_by_triggers[key].append(trigger) + + return layer_module_by_triggers + + +if __name__ == "__main__": + path = r"C:\DEV__OPTT\trunk_optt" + + layer_module_by_triggers = process(path) + for key, triggers in layer_module_by_triggers.items(): + print(f"{key} ({len(triggers)})") + for i, trigger in enumerate(triggers, 1): + print(f" {i}. {trigger.table_name}. {trigger.name} ({trigger.db_name})") + print() diff --git a/job_compassplus/tx_parse_xml/show_db_tables_with_ExcludeTargetDatabases.py b/job_compassplus/tx_parse_xml/show_db_tables_with_ExcludeTargetDatabases.py new file mode 100644 index 000000000..d2fd6eb44 --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_db_tables_with_ExcludeTargetDatabases.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import xml.etree.ElementTree as ET + +from collections import defaultdict +from pathlib import Path + + +# NOTE: ExcludeTargetDatabases еще присутствует во множестве объектов таблицы: индексы, триггеры, столбцы и т.п. +# Но текущая реализация выводит только таблицы + + +def get_tables(model_path: Path) -> list[str]: + ns = dict( + dds="http://schemas.radixware.org/ddsdef.xsd", + ) + + items = [] + model = ET.fromstring(model_path.read_text(encoding="utf-8")) + for table in model.findall(".//dds:Tables/dds:Table", namespaces=ns): + if table.attrib["ExcludeTargetDatabases"]: + title = f"{table.attrib['Name']}({table.attrib['Id']})" + items.append(title) + + return items + + +def process(branch_dir: Path | str) -> dict[str, list[str]]: + if isinstance(branch_dir, str): + branch_dir = Path(branch_dir) + + layer_module_by_tables = defaultdict(list) + + for layer_dir in branch_dir.glob("*"): + if not layer_dir.is_dir(): + continue + + layer_dds_dir = layer_dir / "dds" + if layer_dds_dir.is_dir(): + layer = layer_dir.name + for model_xml in layer_dds_dir.glob("*/model.xml"): + module = model_xml.parent.name + + for table in get_tables(model_xml): + key = f"{layer}/{module}" + layer_module_by_tables[key].append(table) + + return layer_module_by_tables + + +if __name__ == "__main__": + path = r"C:\DEV__OPTT\trunk_optt" + + for key, tables in process(path).items(): + print(f"{key} ({len(tables)})") + for i, table in enumerate(tables, 1): + print(f" {i}. {table}") + print() diff --git a/job_compassplus/tx_parse_xml/show_db_views.py b/job_compassplus/tx_parse_xml/show_db_views.py new file mode 100644 index 000000000..46755d2cb --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_db_views.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import xml.etree.ElementTree as ET + +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class View: + id: str + name: str + db_name: str + + +def get_views(model_path: Path) -> list[View]: + ns = dict( + dds="http://schemas.radixware.org/ddsdef.xsd", + ) + + items = [] + model = ET.fromstring(model_path.read_text(encoding="utf-8")) + for view in model.findall(".//dds:Views/dds:View", namespaces=ns): + items.append( + View( + id=view.attrib["Id"], + name=view.attrib["Name"], + db_name=view.attrib["DbName"], + ) + ) + + return items + + +def process(branch_dir: Path | str) -> dict[str, list[View]]: + if isinstance(branch_dir, str): + branch_dir = Path(branch_dir) + + layer_module_by_triggers = defaultdict(list) + + for layer_dir in branch_dir.glob("*"): + if not layer_dir.is_dir(): + continue + + layer_dds_dir = layer_dir / "dds" + if layer_dds_dir.is_dir(): + layer = layer_dir.name + for model_xml in layer_dds_dir.glob("*/model.xml"): + module = model_xml.parent.name + + for view in get_views(model_xml): + key = f"{layer}/{module}" + layer_module_by_triggers[key].append(view) + + return layer_module_by_triggers + + +if __name__ == "__main__": + path = r"C:\DEV__OPTT\trunk_optt" + + layer_module_by_triggers = process(path) + for key, views in layer_module_by_triggers.items(): + print(f"{key} ({len(views)})") + for i, view in enumerate(views, 1): + print(f" {i}. {view.name} ({view.db_name}, {view.id})") + print() diff --git a/job_compassplus/tx_parse_xml/show_duplicates_SQL_of_ADS_classes.py b/job_compassplus/tx_parse_xml/show_duplicates_SQL_of_ADS_classes.py new file mode 100644 index 000000000..4cd63df91 --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_duplicates_SQL_of_ADS_classes.py @@ -0,0 +1,384 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +import xml.etree.ElementTree as ET + +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class ADS: + title: str + props: dict[str, list[str]] + editors: dict[str, list[str]] + selectors: dict[str, list[str]] + filters: dict[str, list[str]] + sortings: dict[str, list[str]] + + def __bool__(self) -> bool: + return bool( + self.props + or self.editors + or self.selectors + or self.filters + or self.sortings + ) + + +NS = dict( + ads="http://schemas.radixware.org/adsdef.xsd", + xsc="http://schemas.radixware.org/xscml.xsd", +) + + +def get_text(el: ET.Element) -> str: + return "".join(text.strip() for text in el.itertext()) + + +def get_xml_object_title(el: ET.Element) -> str: + el_id = el.attrib["Id"] + el_name = el.attrib["Name"] + return f"{el_name}({el_id})" + + +def get_databases_specific_sqml_expression( + el: ET.Element, tag_name: str +) -> list[tuple[str, str]]: + items = [] + if not el: + return items + + specific_sqml_expression_el = el.findall(f"./ads:{tag_name}", namespaces=NS) + for sqml_el in specific_sqml_expression_el: + if sqml_el: + content_el = sqml_el.find("./xsc:Content", namespaces=NS) + sqml_text = get_sqml_text(content_el) + if sqml_text: + database = sqml_el.find("./xsc:Database", namespaces=NS).text + items.append((database, sqml_text)) + + return items + + +def process_sqml_text(text: str) -> str: + return re.sub(r"\s", "", text).lower() + + +def get_sqml_text(el: ET.Element) -> str: + if not el: + return "" + + lines = [] + for item in el.iterfind("./xsc:Item", namespaces=NS): + for child in item: + if child.tag.endswith("Sql"): + if inner_text := get_text(child): + # Пропуск комментариев + if not inner_text.startswith("--"): + lines.append(process_sqml_text(inner_text)) + else: + # Например: [('TableId', 'tbl42K4K2TTGLNRDHRZABQAQH3XQ4'), ('PropId', 'colXJ7GB2ILWZGAVHRCJAZCMCM6E4'), ('Owner', 'CHILD')] + if child.items(): + if owner_type := child.get("Owner"): + attr_str = ( + owner_type + + "." + + ".".join(v for k, v in child.items() if k != "Owner") + ) + else: + attr_str = ",".join(f"{k}={v}" for k, v in child.items()) + lines.append(f"[{attr_str}]") + + return "".join(lines) + + +def get_exists_condition_sqml_text(condition_el: ET.Element) -> list[str]: + items = [] + if not condition_el: + return items + + condition_where_el = condition_el.find("./ads:ConditionWhere", namespaces=NS) + if sqml_text := get_sqml_text(condition_where_el): + items.append(sqml_text) + + condition_from_el = condition_el.find("./ads:ConditionFrom", namespaces=NS) + if sqml_text := get_sqml_text(condition_from_el): + items.append(sqml_text) + + for tag_name in ["SpecificConditionWhere", "SpecificConditionFrom"]: + for database, sqml_text in get_databases_specific_sqml_expression( + condition_el, tag_name + ): + items.append(f"{database}:{sqml_text}") + + return items + + +def get_ads_filters(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + for filter_el in ads_el.findall( + "./ads:Presentations/ads:Filters/ads:Filter", namespaces=NS + ): + filter_title = get_xml_object_title(filter_el) + + condition_el = filter_el.find("./ads:Condition", namespaces=NS) + if sqml_text := get_sqml_text(condition_el): + obj_by_items[filter_title].append(sqml_text) + + condition_from_el = filter_el.find("./ads:ConditionFrom", namespaces=NS) + if sqml_text := get_sqml_text(condition_from_el): + obj_by_items[filter_title].append(sqml_text) + + hint_el = filter_el.find("./ads:Hint", namespaces=NS) + if sqml_text := get_sqml_text(hint_el): + obj_by_items[filter_title].append(sqml_text) + + for tag_name in ["SpecificCondition", "SpecificConditionFrom", "SpecificHint"]: + for database, sqml_text in get_databases_specific_sqml_expression( + filter_el, tag_name + ): + obj_by_items[filter_title].append(f"{database}:{sqml_text}") + + enabled_sorting_el = filter_el.find("./ads:EnabledSorting", namespaces=NS) + if enabled_sorting_el: + enabled_sorting_hint_el = enabled_sorting_el.find( + "./ads:Hint", namespaces=NS + ) + if sqml_text := get_sqml_text(enabled_sorting_hint_el): + obj_by_items[filter_title].append(sqml_text) + + for database, sqml_text in get_databases_specific_sqml_expression( + enabled_sorting_el, "SpecificHint" + ): + obj_by_items[filter_title].append(f"{database}:{sqml_text}") + + return obj_by_items + + +def get_ads_props(ads_el: ET.Element) -> dict[str, list[str]]: + prop_by_items = defaultdict(list) + + for prop_el in ads_el.findall("./ads:Properties/ads:Property", namespaces=NS): + prop_title = get_xml_object_title(ads_el) + "/" + get_xml_object_title(prop_el) + + sqml_expression_el = prop_el.find("./ads:SqmlExpression", namespaces=NS) + if sqml_text := get_sqml_text(sqml_expression_el): + prop_by_items[prop_title].append(sqml_text) + + for database, sqml_text in get_databases_specific_sqml_expression( + prop_el, "SpecificSqmlExpression" + ): + prop_by_items[prop_title].append(f"{database}:{sqml_text}") + + parent_select_condition_el = prop_el.find( + "./ads:Presentation/ads:ParentSelect/ads:ParentSelectCondition", + namespaces=NS, + ) + for sqml_text in get_exists_condition_sqml_text(parent_select_condition_el): + prop_by_items[prop_title].append(sqml_text) + + return prop_by_items + + +def get_ads_editor_presentations(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + for presentation_el in ads_el.findall( + "./ads:Presentations/ads:EditorPresentations/ads:EditorPresentation", + namespaces=NS, + ): + editor_title = get_xml_object_title(presentation_el) + + for child_item_ref_el in presentation_el.findall( + "./ads:ChildExplorerItems/ads:Item/ads:ChildRef", namespaces=NS + ): + condition_el = child_item_ref_el.find("./ads:Condition", namespaces=NS) + child_sqml_items = get_exists_condition_sqml_text(condition_el) + if child_sqml_items: + child_ref_title = get_xml_object_title(child_item_ref_el) + + obj_by_items[editor_title].append( + f'{editor_title}/{child_ref_title}: {", ".join(child_sqml_items)}' + ) + + for child_item_entity_el in presentation_el.findall( + "./ads:ChildExplorerItems/ads:Item/ads:Entity", namespaces=NS + ): + condition_el = child_item_entity_el.find("./ads:Condition", namespaces=NS) + child_sqml_items = get_exists_condition_sqml_text(condition_el) + if child_sqml_items: + child_entity_title = get_xml_object_title(child_item_entity_el) + obj_by_items[editor_title].append( + f'{editor_title}/{child_entity_title}: {", ".join(child_sqml_items)}' + ) + + return obj_by_items + + +def get_ads_selector_presentations(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + for presentation_el in ads_el.findall( + "./ads:Presentations/ads:SelectorPresentations/ads:SelectorPresentation", + namespaces=NS, + ): + selector_title = get_xml_object_title(presentation_el) + + condition_el = presentation_el.find("./ads:Condition", namespaces=NS) + for sqml_text in get_exists_condition_sqml_text(condition_el): + obj_by_items[selector_title].append(sqml_text) + + addons_el = presentation_el.find("./ads:Addons", namespaces=NS) + if addons_el: + default_hint_el = addons_el.find("./ads:DefaultHint", namespaces=NS) + if sqml_text := get_sqml_text(default_hint_el): + obj_by_items[selector_title].append(sqml_text) + + specific_hint_el = addons_el.find("./ads:SpecificHint", namespaces=NS) + for database, sqml_text in get_databases_specific_sqml_expression( + specific_hint_el, "SpecificDefaultHint" + ): + obj_by_items[selector_title].append(f"{database}:{sqml_text}") + + return obj_by_items + + +def get_ads_sortings(ads_el: ET.Element) -> dict[str, list[str]]: + obj_by_items = defaultdict(list) + + sorting_el = ads_el.find( + "./ads:Presentations/ads:Sortings/ads:Sorting", namespaces=NS + ) + if sorting_el: + sorting_title = get_xml_object_title(sorting_el) + + hint_el = sorting_el.find("./ads:Hint", namespaces=NS) + if sqml_text := get_sqml_text(hint_el): + obj_by_items[sorting_title].append(sqml_text) + + specific_hint_el = sorting_el.find("./ads:SpecificHint", namespaces=NS) + for database, sqml_text in get_databases_specific_sqml_expression( + specific_hint_el, "SpecificHint" + ): + obj_by_items[sorting_title].append(f"{database}:{sqml_text}") + + return obj_by_items + + +def get_ads(model_path: Path) -> ADS | None: + model = ET.fromstring(model_path.read_text(encoding="utf-8")) + ads_el = model.find("./ads:AdsClassDefinition", namespaces=NS) + if not ads_el: + return + + return ADS( + title=get_xml_object_title(ads_el), + props=get_ads_props(ads_el), + editors=get_ads_editor_presentations(ads_el), + selectors=get_ads_selector_presentations(ads_el), + filters=get_ads_filters(ads_el), + sortings=get_ads_sortings(ads_el), + ) + + +def get_ads_list(branch_dir: Path | str) -> dict[str, list[ADS]]: + if isinstance(branch_dir, str): + branch_dir = Path(branch_dir) + + if not branch_dir.exists(): + raise Exception(f"Not exists: {branch_dir}") + + layer_module_by_ads_list = defaultdict(list) + + for layer_dir in branch_dir.glob("*"): + if not layer_dir.is_dir(): + continue + + layer_ads_dir = layer_dir / "ads" + if layer_ads_dir.is_dir(): + layer = layer_dir.name + for module_path in layer_ads_dir.glob("*/src"): + module = module_path.parent.name + layer_module = f"{layer}/{module}" + + for class_path in module_path.glob("*.xml"): + if ads := get_ads(class_path): + layer_module_by_ads_list[layer_module].append(ads) + + return layer_module_by_ads_list + + +def process(path: str) -> None: + indent1 = " " + indent2 = indent1 * 2 + + sqml_text_by_ids: dict[str, list[str]] = defaultdict(list) + + for layer_module, ads_list in get_ads_list(path).items(): + for ads in ads_list: + prop_by_sqmls = ads.props + if prop_by_sqmls: + for prop, items in prop_by_sqmls.items(): + for sqml_text in items: + sqml_text_by_ids[sqml_text].append(prop) + + editor_by_sqmls = ads.editors + if editor_by_sqmls: + for editor, items in editor_by_sqmls.items(): + for sqml in items: + id_str, sqml_text = sqml.split(": ") + sqml_text_by_ids[sqml_text].append(id_str) + + selector_by_sqmls = ads.selectors + if selector_by_sqmls: + for selector, items in selector_by_sqmls.items(): + for sqml_text in items: + sqml_text_by_ids[sqml_text].append(selector) + + filter_by_sqmls = ads.filters + if filter_by_sqmls: + for filter_name, items in filter_by_sqmls.items(): + for sqml_text in items: + sqml_text_by_ids[sqml_text].append(filter_name) + + sorting_by_sqmls = ads.sortings + if sorting_by_sqmls: + for sorting, items in sorting_by_sqmls.items(): + for sqml_text in items: + sqml_text_by_ids[sqml_text].append(sorting) + + for sqml_text, ids in sorted( + sqml_text_by_ids.items(), key=lambda x: len(x[1]), reverse=True + ): + # Если меньше 5, то пропускаем + if len(ids) < 5: + continue + + print(f"Sql: {sqml_text}") + print(f"{indent1}{len(ids)}:") + for id_str in ids: + print(f"{indent2}{id_str}") + print() + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Shows ADS entities with duplicates SQL definitions" + ) + parser.add_argument( + "path_trunk", + metavar="/PATH/TO/TRUNK", + help="Path to TX source tree (the directory containing branch.xml)", + ) + args = parser.parse_args() + + process(args.path_trunk) diff --git a/job_compassplus/tx_parse_xml/show_duplicates_of_branch_xml.py b/job_compassplus/tx_parse_xml/show_duplicates_of_branch_xml.py new file mode 100644 index 000000000..8599ced08 --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_duplicates_of_branch_xml.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import Counter +from pathlib import Path + +from bs4 import BeautifulSoup + + +def get_duplicates(project_dir: str | Path) -> dict[str, int]: + if isinstance(project_dir, str): + project_dir = Path(project_dir) + + file_name_branch = project_dir / "branch.xml" + + with open(file_name_branch, encoding="utf-8") as f: + root = BeautifulSoup(f, "html.parser") + + items = [] + for module in root.select("moduleinfo"): + layer_url = module.layerurl.text + module_id = module.moduleid.text + name = f"{layer_url}-{module_id}" + + definition_path = None + if module.definition: + definition_path = module.definition.get("path") + + if definition_path and definition_path != module_id: + name += f': {module.definition["path"]}' + + items.append(name) + + return {name: number for name, number in Counter(items).items() if number > 1} + + +if __name__ == "__main__": + project_dir = "C:/DEV__TX/trunk_tx" + duplicates = get_duplicates(project_dir) + + for name, number in sorted(duplicates.items(), reverse=True): + print(f"{name} = {number}") diff --git a/job_compassplus/tx_parse_xml/show_func_tags_service_bus.py b/job_compassplus/tx_parse_xml/show_func_tags_service_bus.py new file mode 100644 index 000000000..53ee5c9bc --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_func_tags_service_bus.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import defaultdict +from bs4 import BeautifulSoup, Tag + + +file_name = r"C:\DEV__TX\trunk_tx\com.tranzaxis\uds\Interfacing.Examples\etc\DinersClub\DinersClub.xml" + + +root = BeautifulSoup(open(file_name), "xml") + + +def get_func_title(func_el: Tag) -> str: + description = func_el.Description.text if func_el.Description else "" + attrs_title = ", ".join(f"{k}={v}" for k, v in func_el.attrs.items()) + return f"{attrs_title}. Description: {description}" + + +def get_funcs_from_user_funcs(node_el: Tag) -> list[Tag]: + try: + return [ + func_el + for func_el in node_el.find("UserFuncs", recursive=False).find_all( + "Func", recursive=False + ) + ] + except: + return [] + + +def get_funcs_from_user_props(node_el: Tag) -> list[Tag]: + try: + return [ + func_el + for func_el in node_el.find("UserProps", recursive=False).find_all("Func") + ] + except: + return [] + + +node_by_funcs = defaultdict(list) + +nodes = [] +if pipeline_nodes := root.find("PipelineNodes"): + nodes += pipeline_nodes.find_all("Node") +if other_nodes := root.find("OtherNodes"): + nodes += other_nodes.find_all("Node") + +for node_el in nodes: + node_title = node_el.Title.text if node_el.Title else "" + node_class_id = node_el["ClassId"] + + parent_title = f"{node_el.name} (Title: {node_title}. ClassId: {node_el['ClassId']}, EntityPid: {node_el['EntityPid']})" + + # Старый стиль хранения в UserFuncs/Func + for func_el in get_funcs_from_user_funcs(node_el): + func_title = get_func_title(func_el) + node_by_funcs[parent_title].append(func_title) + + # Новый стиль хранения в UserProps/Props/Func + for func_el in get_funcs_from_user_props(node_el): + func_title = get_func_title(func_el) + node_by_funcs[parent_title].append(func_title) + + try: + for stage_el in node_el.TransformStages.find_all( + "TransformStage", recursive=False + ): + parent_title = f"{node_el.name}/Stage#{stage_el.Seq.text} (ClassId: {stage_el['ClassId']}, NodeTitle: {node_title}, NodeClassId: {node_el['ClassId']}, NodeEntityPid: {node_el['EntityPid']})" + + for func_el in get_funcs_from_user_funcs(stage_el): + func_title = get_func_title(func_el) + node_by_funcs[parent_title].append(func_title) + + for func_el in get_funcs_from_user_props(stage_el): + func_title = get_func_title(func_el) + node_by_funcs[parent_title].append(func_title) + + except: + pass + +i = 1 +for node_title, funcs in node_by_funcs.items(): + print(f"{node_title} ({len(funcs)}):") + for func_title in funcs: + print(f" {i}. {func_title}") + i += 1 + + print() + +""" +Node/Stage#1 (ClassId: aclFZFQDAPB2BDMBBDUE44VOUW66Q, NodeTitle: Stop List Request, NodeClassId: aclZUJW4J4EZ5GUBEPQEX7NUXKZSA, NodeEntityPid: 319) (2): + 1. ClassGUID=aclDNZFNZC3RBCBFLFRTXPHWDYLFU, ProfileVersion=1, OwnerPid=110, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pru5S5NKVTPLVCR5EC23E46WNXNP4, Profile=Tran::TranXsd:Request transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Xml xmlRq) throws PipelineException. Description: Request + 2. ClassGUID=aclULCQ4C47UVHSVKA7JO7V3O6VBU, ProfileVersion=0, OwnerPid=110, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pruXTAMEVDGJZBOHPLCWVX3MHDG4E, Profile=Xml transform(Radix::ServiceBus.Nodes::Transformer transformer, Radix::ServiceBus::SbXsd:PipelineMessageRs rs, Tran::TranXsd:Response tranRs, Xml inXmlRq, Tran::TranXsd:Request outTranRq) throws PipelineException. Description: Response + +Node (Title: IsStopList. ClassId: aclP3F7MNUSNVHJRAMR2454ZA4GCQ, EntityPid: 318) (1): + 3. ClassGUID=aclYRODMAM7AZB55JOWOVGSJ2TL4I, ProfileVersion=0, OwnerPid=318, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclP3F7MNUSNVHJRAMR2454ZA4GCQ, OwnerPropId=pru5WVR5OZMIRC4VC6QHUQZ66EBWM, Profile=boolean check(Radix::ServiceBus.Nodes::Router.Jml router, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Tran::TranXsd:Request tranRq) throws PipelineException. Description: Is stop list request + +Node/Stage#1 (ClassId: acl2QQ7GRLAFNE73NPWZJ4I4CLSMU, NodeTitle: System Event, NodeClassId: acl4TJBHQN7PJBZFLBHDI3WHSWDSY, NodeEntityPid: 329) (2): + 4. ClassGUID=acl2G2QDD5EXRBCROWISX5QR5CUCA, ProfileVersion=0, OwnerPid=113, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=acl2QQ7GRLAFNE73NPWZJ4I4CLSMU, OwnerPropId=pruPSTIRHU6HRH2TJTGTE6XD6NZTI, Profile=Xml transform(Transformer transformer, ServiceBus::SbXsd:PipelineMessageRq rq, Xml inRq) throws PipelineException. Description: Request + 5. ClassGUID=aclIB36HWWSIZFGHBNZC4SXAYP2J4, ProfileVersion=0, OwnerPid=113, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=acl2QQ7GRLAFNE73NPWZJ4I4CLSMU, OwnerPropId=pruVGX63H52ZRFMNAAH5JHB6U5LGM, Profile=Xml transform(Transformer transformer, ServiceBus::SbXsd:PipelineMessageRs rs, Xml inRs, Xml inRq, Xml outRq) throws PipelineException. Description: Response + +Node (Title: IsEcho. ClassId: aclXL4IP5OEDFAXRMDKNVVVBOXHWU, EntityPid: 331) (1): + 6. ClassGUID=aclTQ77FRM62VEKFP3EI3JEFTG2RY, ProfileVersion=0, OwnerPid=331, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclXL4IP5OEDFAXRMDKNVVVBOXHWU, OwnerPropId=pruGU2USFURD5ATXP63WVDT5SISS4, Profile=boolean check(Router.Jml router, ServiceBus::SbXsd:PipelineMessageRq rq) throws PipelineException. Description: Is echo request + +Node/Stage#1 (ClassId: aclFZFQDAPB2BDMBBDUE44VOUW66Q, NodeTitle: Reverse Request, NodeClassId: aclZUJW4J4EZ5GUBEPQEX7NUXKZSA, NodeEntityPid: 330) (2): + 7. ClassGUID=aclDNZFNZC3RBCBFLFRTXPHWDYLFU, ProfileVersion=1, OwnerPid=114, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pru5S5NKVTPLVCR5EC23E46WNXNP4, Profile=Tran::TranXsd:Request transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Xml xmlRq) throws PipelineException. Description: Request + 8. ClassGUID=aclULCQ4C47UVHSVKA7JO7V3O6VBU, ProfileVersion=0, OwnerPid=114, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pruXTAMEVDGJZBOHPLCWVX3MHDG4E, Profile=Xml transform(Radix::ServiceBus.Nodes::Transformer transformer, Radix::ServiceBus::SbXsd:PipelineMessageRs rs, Tran::TranXsd:Response tranRs, Xml inXmlRq, Tran::TranXsd:Request outTranRq) throws PipelineException. Description: Response + +Node (Title: IsInvalid. ClassId: aclXL4IP5OEDFAXRMDKNVVVBOXHWU, EntityPid: 324) (1): + 9. ClassGUID=aclTQ77FRM62VEKFP3EI3JEFTG2RY, ProfileVersion=0, OwnerPid=324, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclXL4IP5OEDFAXRMDKNVVVBOXHWU, OwnerPropId=pruGU2USFURD5ATXP63WVDT5SISS4, Profile=boolean check(Router.Jml router, ServiceBus::SbXsd:PipelineMessageRq rq) throws PipelineException. Description: Is invalid request + +Node/Stage#1 (ClassId: aclDOHZLYSOGNBYFINCWBZ55XXKZE, NodeTitle: Auth-Rev Request, NodeClassId: acl3ZJO3VQ23JFWXNIZOFG36TF7BI, NodeEntityPid: 322) (2): + 10. ClassGUID=aclOFZ6CPB4Z5GKTHWQOSEILOHLPI, ProfileVersion=1, OwnerPid=111, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclDOHZLYSOGNBYFINCWBZ55XXKZE, OwnerPropId=pru2ZBAAHQB5VE3NMNQNLDKE4L2LI, Profile=Tran::TranXsd:Response transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRs rs, Xml xmlRs, Tran::TranXsd:Request inTranRq, Xml outXmlRq) throws PipelineException. Description: Response + 11. ClassGUID=aclTA2OUEEOOBAVFF36IOH5FQIXYM, ProfileVersion=1, OwnerPid=111, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclDOHZLYSOGNBYFINCWBZ55XXKZE, OwnerPropId=pruA3BKZH2TBVGPXJ6BFCY377Y3XE, Profile=Xml transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Tran::TranXsd:Request tranRq) throws PipelineException. Description: Request + +Node/Stage#1 (ClassId: aclFZFQDAPB2BDMBBDUE44VOUW66Q, NodeTitle: Auth Request, NodeClassId: aclZUJW4J4EZ5GUBEPQEX7NUXKZSA, NodeEntityPid: 333) (2): + 12. ClassGUID=aclDNZFNZC3RBCBFLFRTXPHWDYLFU, ProfileVersion=1, OwnerPid=116, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pru5S5NKVTPLVCR5EC23E46WNXNP4, Profile=Tran::TranXsd:Request transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Xml xmlRq) throws PipelineException. Description: Request + 13. ClassGUID=aclULCQ4C47UVHSVKA7JO7V3O6VBU, ProfileVersion=0, OwnerPid=116, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pruXTAMEVDGJZBOHPLCWVX3MHDG4E, Profile=Xml transform(Radix::ServiceBus.Nodes::Transformer transformer, Radix::ServiceBus::SbXsd:PipelineMessageRs rs, Tran::TranXsd:Response tranRs, Xml inXmlRq, Tran::TranXsd:Request outTranRq) throws PipelineException. Description: Response + +Node (Title: Echo Request. ClassId: aclXM5EJPEZFFBGBGEXO4W3T7ZJFM, EntityPid: 321) (1): + 14. ClassGUID=aclSQEW5FAQJ5EBFPU57S47V5XJYU, ProfileVersion=0, OwnerPid=321, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclXM5EJPEZFFBGBGEXO4W3T7ZJFM, OwnerPropId=pruTDGMAUN2LNECHHZUYWBLOQ5EFM, Profile=ServiceBus::SbXsd:PipelineMessageRs process(Processor.Jml processor, ServiceBus::SbXsd:PipelineMessageRq rq) throws PipelineException. Description: Echo request + +Node/Stage#1 (ClassId: aclDOHZLYSOGNBYFINCWBZ55XXKZE, NodeTitle: Admin Request, NodeClassId: acl3ZJO3VQ23JFWXNIZOFG36TF7BI, NodeEntityPid: 332) (2): + 15. ClassGUID=aclOFZ6CPB4Z5GKTHWQOSEILOHLPI, ProfileVersion=1, OwnerPid=115, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclDOHZLYSOGNBYFINCWBZ55XXKZE, OwnerPropId=pru2ZBAAHQB5VE3NMNQNLDKE4L2LI, Profile=Tran::TranXsd:Response transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRs rs, Xml xmlRs, Tran::TranXsd:Request inTranRq, Xml outXmlRq) throws PipelineException. Description: Response + 16. ClassGUID=aclTA2OUEEOOBAVFF36IOH5FQIXYM, ProfileVersion=1, OwnerPid=115, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclDOHZLYSOGNBYFINCWBZ55XXKZE, OwnerPropId=pruA3BKZH2TBVGPXJ6BFCY377Y3XE, Profile=Xml transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Tran::TranXsd:Request tranRq) throws PipelineException. Description: Request + +Node (Title: isSystem. ClassId: aclXL4IP5OEDFAXRMDKNVVVBOXHWU, EntityPid: 317) (1): + 17. ClassGUID=aclTQ77FRM62VEKFP3EI3JEFTG2RY, ProfileVersion=0, OwnerPid=317, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclXL4IP5OEDFAXRMDKNVVVBOXHWU, OwnerPropId=pruGU2USFURD5ATXP63WVDT5SISS4, Profile=boolean check(Router.Jml router, ServiceBus::SbXsd:PipelineMessageRq rq) throws PipelineException. Description: Is system request + +Node (Title: TryCatch. ClassId: aclXM5EJPEZFFBGBGEXO4W3T7ZJFM, EntityPid: 328) (1): + 18. ClassGUID=aclSQEW5FAQJ5EBFPU57S47V5XJYU, ProfileVersion=0, OwnerPid=328, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclXM5EJPEZFFBGBGEXO4W3T7ZJFM, OwnerPropId=pruTDGMAUN2LNECHHZUYWBLOQ5EFM, Profile=ServiceBus::SbXsd:PipelineMessageRs process(Processor.Jml processor, ServiceBus::SbXsd:PipelineMessageRq rq) throws PipelineException. Description: Catch exception + +Node/Stage#1 (ClassId: aclDOHZLYSOGNBYFINCWBZ55XXKZE, NodeTitle: Stop List Request, NodeClassId: acl3ZJO3VQ23JFWXNIZOFG36TF7BI, NodeEntityPid: 316) (2): + 19. ClassGUID=aclOFZ6CPB4Z5GKTHWQOSEILOHLPI, ProfileVersion=1, OwnerPid=109, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclDOHZLYSOGNBYFINCWBZ55XXKZE, OwnerPropId=pru2ZBAAHQB5VE3NMNQNLDKE4L2LI, Profile=Tran::TranXsd:Response transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRs rs, Xml xmlRs, Tran::TranXsd:Request inTranRq, Xml outXmlRq) throws PipelineException. Description: Response + 20. ClassGUID=aclTA2OUEEOOBAVFF36IOH5FQIXYM, ProfileVersion=1, OwnerPid=109, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclDOHZLYSOGNBYFINCWBZ55XXKZE, OwnerPropId=pruA3BKZH2TBVGPXJ6BFCY377Y3XE, Profile=Xml transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Tran::TranXsd:Request tranRq) throws PipelineException. Description: Request + +Node (Title: Invalid Request. ClassId: aclXM5EJPEZFFBGBGEXO4W3T7ZJFM, EntityPid: 315) (1): + 21. ClassGUID=aclSQEW5FAQJ5EBFPU57S47V5XJYU, ProfileVersion=0, OwnerPid=315, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclXM5EJPEZFFBGBGEXO4W3T7ZJFM, OwnerPropId=pruTDGMAUN2LNECHHZUYWBLOQ5EFM, Profile=ServiceBus::SbXsd:PipelineMessageRs process(Processor.Jml processor, ServiceBus::SbXsd:PipelineMessageRq rq) throws PipelineException. Description: Invalid request + +Node (Title: IsAdmin. ClassId: aclP3F7MNUSNVHJRAMR2454ZA4GCQ, EntityPid: 326) (1): + 22. ClassGUID=aclYRODMAM7AZB55JOWOVGSJ2TL4I, ProfileVersion=0, OwnerPid=326, OwnerEntityId=tblPNJV5QTXIBGJPBHK64RO3LH73A, OwnerClassId=aclP3F7MNUSNVHJRAMR2454ZA4GCQ, OwnerPropId=pru5WVR5OZMIRC4VC6QHUQZ66EBWM, Profile=boolean check(Radix::ServiceBus.Nodes::Router.Jml router, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Tran::TranXsd:Request tranRq) throws PipelineException. Description: Is admin request + +Node/Stage#1 (ClassId: aclFZFQDAPB2BDMBBDUE44VOUW66Q, NodeTitle: Admin Request, NodeClassId: aclZUJW4J4EZ5GUBEPQEX7NUXKZSA, NodeEntityPid: 325) (2): + 23. ClassGUID=aclDNZFNZC3RBCBFLFRTXPHWDYLFU, ProfileVersion=1, OwnerPid=112, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pru5S5NKVTPLVCR5EC23E46WNXNP4, Profile=Tran::TranXsd:Request transform(Transformer.Base transformer, Radix::ServiceBus::SbXsd:PipelineMessageRq rq, Xml xmlRq) throws PipelineException. Description: Request + 24. ClassGUID=aclULCQ4C47UVHSVKA7JO7V3O6VBU, ProfileVersion=0, OwnerPid=112, OwnerEntityId=tblTDP5BP5ZXBEOTJX74NH43PEPJA, OwnerClassId=aclFZFQDAPB2BDMBBDUE44VOUW66Q, OwnerPropId=pruXTAMEVDGJZBOHPLCWVX3MHDG4E, Profile=Xml transform(Radix::ServiceBus.Nodes::Transformer transformer, Radix::ServiceBus::SbXsd:PipelineMessageRs rs, Tran::TranXsd:Response tranRs, Xml inXmlRq, Tran::TranXsd:Request outTranRq) throws PipelineException. Description: Response + +Node (Title: Endpoint.NetHub. ClassId: aclQCQNNJMW4FCNZCRN2D5GNWKOKA, EntityPid: 6341) (5): + 25. ClassGUID=aclDUAVIDYVPZD4ZDUJBHXWJV7VTE, ProfileVersion=0, OwnerPid=6341, OwnerEntityId=tbl5HP4XTP3EGWDBRCRAAIT4AGD7E, OwnerClassId=aclQCQNNJMW4FCNZCRN2D5GNWKOKA, OwnerPropId=pru6HZ4WT3ULJBYTCTHWO5NFYXI5U, Profile=Str extractUniqueKey(byte[] data) throws PipelineException. Description: + 26. ClassGUID=aclQPC7P4UWZ5BQ5OIJAMWF3SY5X4, ProfileVersion=0, OwnerPid=6341, OwnerEntityId=tbl5HP4XTP3EGWDBRCRAAIT4AGD7E, OwnerClassId=aclQCQNNJMW4FCNZCRN2D5GNWKOKA, OwnerPropId=pruK7UNBG2GCZDFVAEGA3NFR2U72Q, Profile=int insertStan(byte[] data, int prevStan) throws PipelineException. Description: + 27. ClassGUID=aclKZFUEXWYNJCFPPKI6T2B5ERAZU, ProfileVersion=0, OwnerPid=6341, OwnerEntityId=tbl5HP4XTP3EGWDBRCRAAIT4AGD7E, OwnerClassId=aclQCQNNJMW4FCNZCRN2D5GNWKOKA, OwnerPropId=pruLXJ7LZAFXRD5ZDHXXBU24324SE, Profile=byte[] prepareEchoTestRq() throws PipelineException. Description: + 28. ClassGUID=aclBKU3COGRUVGLZABNOJSKU5PUM4, ProfileVersion=0, OwnerPid=6341, OwnerEntityId=tbl5HP4XTP3EGWDBRCRAAIT4AGD7E, OwnerClassId=aclQCQNNJMW4FCNZCRN2D5GNWKOKA, OwnerPropId=pruNHYZI7WV4ZGDPDCXONZQWDWP2Y, Profile=boolean isRequest(byte[] data, Common::Map parsingVars) throws NetHubFormatError, PipelineException. Description: + 29. ClassGUID=aclNJQPXOGPZVAJPCCRI26IFRTZHY, ProfileVersion=0, OwnerPid=6341, OwnerEntityId=tbl5HP4XTP3EGWDBRCRAAIT4AGD7E, OwnerClassId=aclQCQNNJMW4FCNZCRN2D5GNWKOKA, OwnerPropId=pruQ4URE5BVGVAB7HIHPXE2JN5LBY, Profile=boolean isCorrelated(byte[] rq, Common::Map rqParsingVars, byte[] rs, Common::Map rsParsingVars) throws NetHubFormatError, PipelineException. Description: + +Node (Title: Incoming.Rtp. ClassId: aclSJWDDYSTAJDNFGAKPB5ENWPCVQ, EntityPid: 16983) (1): + 30. ClassGUID=aclJUSHZQABQZFDVC3BHF2R4SDRNY, ProfileVersion=0, OwnerPid=16983, OwnerEntityId=tblKG37PKEBQPNRDB46AALOMT5GDM, OwnerClassId=aclSJWDDYSTAJDNFGAKPB5ENWPCVQ, OwnerPropId=pruUHNT6SGVYFGQNLYXBL6PRVG4RQ, OwnerExtGuid=7B74FE964CFA4446A91C6857A5C71145, Profile=Xml mask(Xml mess) throws PipelineException. Description: + +Node (Title: Outgoing.Rtp. ClassId: aclDL54XGQDBZCETH2Y35V52MIKXA, EntityPid: 16984) (1): + 31. ClassGUID=aclJUSHZQABQZFDVC3BHF2R4SDRNY, ProfileVersion=0, OwnerPid=16984, OwnerEntityId=tblKG37PKEBQPNRDB46AALOMT5GDM, OwnerClassId=aclDL54XGQDBZCETH2Y35V52MIKXA, OwnerPropId=pru4437EDZTLFCQROXATBM4NHG3WQ, OwnerExtGuid=8F4A6616021B43D49CD7714A39A26961, Profile=Xml mask(Xml mess) throws PipelineException. Description: +""" diff --git a/job_compassplus/tx_parse_xml/show_maintainers.py b/job_compassplus/tx_parse_xml/show_maintainers.py new file mode 100644 index 000000000..e45d69121 --- /dev/null +++ b/job_compassplus/tx_parse_xml/show_maintainers.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import defaultdict +from pathlib import Path + +from bs4 import BeautifulSoup + + +def parse_xml_partially(path: str | Path, line_contains: list) -> BeautifulSoup: + # Считаем только ту часть XML, где имеются необходимые атрибуты + xml_part = [] + for line in open(path, encoding="utf-8"): + xml_part.append(line) + if all(x in line for x in line_contains): + break + + return BeautifulSoup("\n".join(xml_part), "html.parser") + + +def get_owner_by_modules(project_dir: str | Path) -> dict[str, list[str]]: + if isinstance(project_dir, str): + project_dir = Path(project_dir) + + layer_url_by_name: dict[str, str] = dict() + layer_module_id_by_name: dict[tuple[str, str], str] = dict() + + for layer_path in project_dir.glob("*/layer.xml"): + root_layer = parse_xml_partially( + layer_path, line_contains=["Layer ", " Uri", " Name"] + ) + layer_url = root_layer.layer["uri"] + name = root_layer.layer["name"] + layer_url_by_name[layer_url] = name + + for path in layer_path.parent.glob("ads/*/module.xml"): + root_module = parse_xml_partially( + path, line_contains=["Module ", " Id", " Name"] + ) + module_id = root_module.module["id"] + + layer_module_id_by_name[layer_url, module_id] = root_module.module["name"] + + file_name_branch = project_dir / "branch.xml" + + with open(file_name_branch, encoding="utf-8") as f: + root = BeautifulSoup(f, "html.parser") + + owner_by_modules = defaultdict(list) + + for module in root.select("moduleinfo"): + layer_url = module.layerurl.text + layer_name = layer_url_by_name[layer_url] + + module_id = module.moduleid.text + module_name = layer_module_id_by_name[layer_url, module_id] + + definition_path_id = None + if module.definition: + definition_path_id = module.definition.get("path") + + if definition_path_id and definition_path_id != module_id: + title = f"{layer_name}::{module_name}/{definition_path_id} ({module_id}/{definition_path_id})" + else: + title = f"{layer_name}::{module_name} ({module_id})" + + owner_emails = [el.text for el in module.select("owneremail")] + for email in owner_emails: + if title not in owner_by_modules[email]: + owner_by_modules[email].append(title) + + return owner_by_modules + + +if __name__ == "__main__": + project_dir = "C:/DEV__TX/trunk" + owner_by_modules = get_owner_by_modules(project_dir) + + for email in sorted(owner_by_modules): + modules = owner_by_modules[email] + modules.sort() + + print(f"{email} ({len(modules)}):") + for module in modules: + print(f" {module}") + print() diff --git a/job_compassplus/tx_remove_hs_err_pid_log/main.py b/job_compassplus/tx_remove_hs_err_pid_log/main.py new file mode 100644 index 000000000..93083d911 --- /dev/null +++ b/job_compassplus/tx_remove_hs_err_pid_log/main.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt +import logging +import time +import sys + +from logging.handlers import RotatingFileHandler +from pathlib import Path + + +def get_logger( + name=__file__, + file: str | Path = "log.txt", + formatter: str = "[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s", + encoding="utf-8", + log_stdout=True, + log_file=True, +): + log = logging.getLogger(name) + log.setLevel(logging.DEBUG) + + formatter = logging.Formatter(formatter) + + if log_file: + fh = RotatingFileHandler( + file, maxBytes=10_000_000, backupCount=5, encoding=encoding + ) + fh.setFormatter(formatter) + log.addHandler(fh) + + if log_stdout: + sh = logging.StreamHandler(stream=sys.stdout) + sh.setFormatter(formatter) + log.addHandler(sh) + + return log + + +DIR = Path(__file__).resolve().parent + +log = get_logger( + file=DIR / "deleted.txt", + formatter="[%(asctime)s] %(message)s", +) + + +DIRS = [r"C:\DEV__TX", r"C:\DEV__OPTT", r"C:\DEV__RADIX"] + + +def run(dirs: list[str | Path]) -> None: + print(f"\nStarted {dt.datetime.today()}") + + for dir_path in dirs: + print(dir_path) + for file_name in Path(dir_path).glob("*/hs_err_pid*.log"): + ctime_timestamp = file_name.stat().st_ctime + ctime = dt.datetime.fromtimestamp(ctime_timestamp) + ctime = ctime.replace(microsecond=0) + + text = f"{file_name} (date creation: {ctime})" + print(text) + + # Удаление, если с даты создания прошло больше 1 часа + if dt.datetime.today() > ctime + dt.timedelta(hours=1): + log.info(text) + file_name.unlink(missing_ok=True) + + print(f"\nFinished {dt.datetime.today()}") + + +if __name__ == "__main__": + if "--one" in sys.argv: + run(DIRS) + sys.exit() + + while True: + run(DIRS) + time.sleep(2 * 60 * 60) diff --git a/job_compassplus/tx_remove_java_pid_hprof/main.py b/job_compassplus/tx_remove_java_pid_hprof/main.py new file mode 100644 index 000000000..3d84d5697 --- /dev/null +++ b/job_compassplus/tx_remove_java_pid_hprof/main.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt +import logging +import time +import sys + +from logging.handlers import RotatingFileHandler +from pathlib import Path + +# pip install humanize +from humanize import naturalsize as sizeof_fmt + + +def get_logger( + name=__file__, + file: str | Path = "log.txt", + formatter: str = "[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s", + encoding="utf-8", + log_stdout=True, + log_file=True, +): + log = logging.getLogger(name) + log.setLevel(logging.DEBUG) + + formatter = logging.Formatter(formatter) + + if log_file: + fh = RotatingFileHandler( + file, maxBytes=10_000_000, backupCount=5, encoding=encoding + ) + fh.setFormatter(formatter) + log.addHandler(fh) + + if log_stdout: + sh = logging.StreamHandler(stream=sys.stdout) + sh.setFormatter(formatter) + log.addHandler(sh) + + return log + + +DIR = Path(__file__).resolve().parent +DIRS = [r"C:\DEV__TX", r"C:\DEV__OPTT", r"C:\DEV__RADIX"] + + +log = get_logger( + file=DIR / "deleted.txt", + formatter="[%(asctime)s] %(message)s", +) + + +def run(dirs: list[str | Path]) -> None: + print(f"\nStarted {dt.datetime.today()}") + + for dir_path in dirs: + print(dir_path) + path = Path(dir_path) + + files = list(path.glob("*/*.hprof")) + files += list(path.glob("*/.config/var/log/heapdump.hprof.old")) + for f in files: + ctime_timestamp = f.stat().st_ctime + ctime = dt.datetime.fromtimestamp(ctime_timestamp) + ctime = ctime.replace(microsecond=0) + + file_size = f.stat().st_size + + text = f"{f} (date creation: {ctime}, size: {sizeof_fmt(file_size)})" + print(text) + + # Удаление, если с даты создания прошло больше 1 часа + if dt.datetime.today() > ctime + dt.timedelta(hours=1): + log.info(text) + f.unlink(missing_ok=True) + + print(f"\nFinished {dt.datetime.today()}") + + +if __name__ == "__main__": + if "--one" in sys.argv: + run(DIRS) + + sys.exit() + + while True: + run(DIRS) + time.sleep(2 * 60 * 60) diff --git a/job_compassplus/visa_base2_tc_counter.py b/job_compassplus/visa_base2_tc_counter.py new file mode 100644 index 000000000..a995cc86d --- /dev/null +++ b/job_compassplus/visa_base2_tc_counter.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import Counter +from pathlib import Path + + +# TODO: argparser +DIR = Path(r"C:\Users\ipetrash\Desktop\Visa Base 2") + +for path in DIR.rglob("*"): + if not path.is_file() or "errors" in str(path): + continue + + print(path) + + tc_by_counter = Counter() + tc_tcr_by_counter = Counter() + + with open(path, "rb") as f: + for line in f: + is_ctf: bool = len(line.rstrip(b"\n\r")) == 168 + tc_raw: bytes = line[:2] + tcr_raw: bytes = line[3:4] if is_ctf else line[5:6] + # TODO: Support EBCDIC, example b'\xf9\xf0' -> 90 + try: + tc: str = tc_raw.decode("ascii") + except UnicodeDecodeError as e: + print(f" [#] Skip. Error on {tc_raw}: {e}") + break + + try: + tcr: str = tcr_raw.decode("ascii") + except UnicodeDecodeError as e: + print(f" [#] Skip. Error on {tcr_raw}: {e}") + break + + if not tc.strip() or tc in ["90", "91", "92", "00"]: + continue + + tc_by_counter.update([tc]) + tc_tcr_by_counter.update([f"{tc}-{tcr}"]) + + print(" TC: " + ", ".join(f"{k}: {v}" for k, v in tc_by_counter.items())) + print(" TC-TCR: " + ", ".join(f"{k}: {v}" for k, v in tc_tcr_by_counter.items())) diff --git a/job_compassplus/web__show_job_report/main.py b/job_compassplus/web__show_job_report/main.py index cceade056..c23ea825b 100644 --- a/job_compassplus/web__show_job_report/main.py +++ b/job_compassplus/web__show_job_report/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Вывод переработки текущего (или конкретного) пользователя""" @@ -13,36 +13,35 @@ from flask import Flask +DIR = Path(__file__).resolve().parent +sys.path.append(str(DIR.parent / "job_report")) +from utils import get_report_persons_info, get_person_info + app = Flask(__name__) logging.basicConfig(level=logging.DEBUG) -DIR = Path(__file__).resolve().parent -sys.path.append(str(DIR.parent / 'job_report')) -from utils import get_report_persons_info, get_person_info - - @app.route("/") -def index(): +def index() -> str: report_dict = get_report_persons_info() try: - person = report_dict['Текущий пользователь'][0] + person = report_dict["Текущий пользователь"][0] except: person = None if person is None: - person = get_person_info(second_name='Петраш', report_dict=report_dict) + person = get_person_info(second_name="Петраш", report_dict=report_dict) if person: - return '{} {}'.format(person.full_name, person.deviation_of_time) + return f"{person.full_name} {person.deviation_of_time}" - return 'Not found specific user' + return "Not found specific user" -if __name__ == '__main__': +if __name__ == "__main__": # Localhost app.run(port=5001) diff --git a/job_compassplus/web_counter/README.md b/job_compassplus/web_counter/README.md new file mode 100644 index 000000000..09a991ce9 --- /dev/null +++ b/job_compassplus/web_counter/README.md @@ -0,0 +1,8 @@ +### Links: + * http://127.0.0.1:7777/docs + * http://127.0.0.1:7777/redoc + +### Run: +``` +python.exe -m uvicorn main:app --reload --port=7777 +``` \ No newline at end of file diff --git a/job_compassplus/web_counter/config.py b/job_compassplus/web_counter/config.py new file mode 100644 index 000000000..2fcb616c2 --- /dev/null +++ b/job_compassplus/web_counter/config.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + + +DIR = Path(__file__).resolve().parent + +DB_DIR_NAME: Path = DIR / "database" +DB_DIR_NAME.mkdir(parents=True, exist_ok=True) + +DB_FILE_NAME: Path = DB_DIR_NAME / "db.sqlite" diff --git a/job_compassplus/web_counter/db.py b/job_compassplus/web_counter/db.py new file mode 100644 index 000000000..260e1d0af --- /dev/null +++ b/job_compassplus/web_counter/db.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum +import time +import sys + +from typing import Type, Iterable, Any + +# pip install peewee +from peewee import ( + Model, + TextField, + ForeignKeyField, + CharField, + BlobField, + IntegerField, +) +from playhouse.shortcuts import model_to_dict +from playhouse.sqliteq import SqliteQueueDatabase + +from config import DIR, DB_FILE_NAME + +sys.path.append(str(DIR.parent.parent)) +from shorten import shorten + + +# This working with multithreading +# SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqliteq +db = SqliteQueueDatabase( + DB_FILE_NAME, + pragmas={ + "foreign_keys": 1, + "journal_mode": "wal", # WAL-mode + "cache_size": -1024 * 64, # 64MB page-cache + }, + use_gevent=False, # Use the standard library "threading" module. + autostart=True, + queue_max_size=64, # Max. # of pending writes that can accumulate. + results_timeout=5.0, # Max. time to wait for query to be executed. +) + + +class BaseModel(Model): + class Meta: + database = db + + @classmethod + def get_inherited_models(cls) -> list[Type["BaseModel"]]: + return sorted(cls.__subclasses__(), key=lambda x: x.__name__) + + @classmethod + def print_count_of_tables(cls) -> None: + items = [] + for sub_cls in cls.get_inherited_models(): + name = sub_cls.__name__ + count = sub_cls.select().count() + items.append(f"{name}: {count}") + + print(", ".join(items)) + + @classmethod + def count(cls, filters: Iterable = None) -> int: + query = cls.select() + if filters: + query = query.filter(*filters) + return query.count() + + def to_dict(self) -> dict[str, Any]: + return model_to_dict(self, recurse=False) + + def __str__(self) -> str: + fields = [] + for k, field in self._meta.fields.items(): + v = getattr(self, k) + + if isinstance(field, (TextField, CharField)): + if isinstance(v, enum.Enum): + v = v.value + + if v: + v = repr(shorten(v, length=30)) + + if isinstance(field, BlobField) and v is not None: + v = f"<{len(v)} bytes>" + + elif isinstance(field, ForeignKeyField): + k = f"{k}_id" + if v: + v = v.id + + fields.append(f"{k}={v}") + + return self.__class__.__name__ + "(" + ", ".join(fields) + ")" + + +class Counter(BaseModel): + name = CharField(primary_key=True) + value = IntegerField(default=0) + + @classmethod + def get_or_create(cls, name: str) -> "Counter": + obj: Counter | None = cls.get_or_none(name=name) + if not obj: + obj = cls.create(name=name) + return obj + + @classmethod + def increment(cls, name: str) -> int: + # Create or ignore + cls.get_or_create(name) + + cls.update(value=cls.value + 1).where(cls.name == name).execute() + return cls.get_or_create(name).value + + +db.connect() +db.create_tables(BaseModel.get_inherited_models()) + + +# Задержка в 50мс, чтобы дать время на запуск SqliteQueueDatabase и создание таблиц +# Т.к. в SqliteQueueDatabase запросы на чтение выполняются сразу, а на запись попадают в очередь +time.sleep(0.050) + + +if __name__ == "__main__": + BaseModel.print_count_of_tables() diff --git a/job_compassplus/web_counter/main.py b/job_compassplus/web_counter/main.py new file mode 100644 index 000000000..d520728c4 --- /dev/null +++ b/job_compassplus/web_counter/main.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from fastapi import FastAPI +from db import Counter + + +app = FastAPI() + + +@app.get("/increment/{name}") +def increment(name: str) -> int: + return Counter.increment(name) diff --git a/job_compassplus/web_counter/run_python10.bat b/job_compassplus/web_counter/run_python10.bat new file mode 100644 index 000000000..789784ee1 --- /dev/null +++ b/job_compassplus/web_counter/run_python10.bat @@ -0,0 +1 @@ +C:\Users\ipetrash\AppData\Local\Programs\Python\Python310\python.exe -m uvicorn main:app --reload --port=7777 \ No newline at end of file diff --git a/join_dict_values_if_key_in_other_values.py b/join_dict_values_if_key_in_other_values.py index 751420d5a..157fab459 100644 --- a/join_dict_values_if_key_in_other_values.py +++ b/join_dict_values_if_key_in_other_values.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # https://ru.stackoverflow.com/questions/854015/ @@ -11,7 +11,7 @@ """ -total = {'a': ['b', 'd'], 'b': ['c', 'd'], 'c': ['d']} +total = {"a": ["b", "d"], "b": ["c", "d"], "c": ["d"]} for k in total: for k1, v1 in total.items(): diff --git a/join_words.py b/join_words.py new file mode 100644 index 000000000..41444b98c --- /dev/null +++ b/join_words.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +def join_words(word_1: str, word_2: str) -> str: + result: str = word_1 + word_2 + for i in range(min(len(word_1), len(word_2))): + if word_1[-i:].upper() == word_2[:i].upper(): + result = word_1.removesuffix(word_1[-i:]) + word_2 + + return result + + +if __name__ == "__main__": + result = join_words("Капитан", "АНИМЕ") + print(result) + assert result == "КапитАНИМЕ" diff --git a/json_examples/counter/main.py b/json_examples/counter/main.py index 6300229d9..16070beef 100644 --- a/json_examples/counter/main.py +++ b/json_examples/counter/main.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -file_name = 'counter.json' - import json -json_data = json.load(open(file_name, encoding='utf-8')) + + +file_name = "counter.json" + +json_data = json.load(open(file_name, encoding="utf-8")) print(json_data) # Изменение объекта -json_data['counter'] += 1 +json_data["counter"] += 1 # Сохранение -json.dump(json_data, open(file_name, mode='w', encoding='utf-8')) +json.dump(json_data, open(file_name, mode="w", encoding="utf-8")) diff --git a/json_examples/dump__serialization__custom_object__default.py b/json_examples/dump__serialization__custom_object__default.py index ff57de312..41e484b89 100644 --- a/json_examples/dump__serialization__custom_object__default.py +++ b/json_examples/dump__serialization__custom_object__default.py @@ -1,11 +1,57 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import json from decimal import Decimal -data = ((69695, 'CASTROL', '156f9d', 'Castrol EDGE Professional A5 5W30 (1л) Lаnd Rover', 'Castrol-156F9D-Castrol EDGE Professional A5 5W30 (1л) Lаnd Rover', Decimal('1.00'), '', Decimal('684.25'), 0, 13155264, '', Decimal('0.00'), Decimal('0.00'), Decimal('0.00'), Decimal('0.00'), Decimal('0.00'), '7', '', ''), (69695, 'CASTROL', '15667c', 'Castrol EDGE Titanium 5W30 LL 504.00/507.00 (1L).Масло моторное', 'Castrol-15667C-Castrol EDGE Titanium 5W30 LL 504.00/507.00 (1L).Масло моторное', Decimal('40.00'), '', Decimal('599.15'), 0, 13155265, '', Decimal('0.00'), Decimal('0.00'), Decimal('0.00'), Decimal('0.00'), Decimal('0.00'), '7', '', '')) + + +data = ( + ( + 69695, + "CASTROL", + "156f9d", + "Castrol EDGE Professional A5 5W30 (1л) Lаnd Rover", + "Castrol-156F9D-Castrol EDGE Professional A5 5W30 (1л) Lаnd Rover", + Decimal("1.00"), + "", + Decimal("684.25"), + 0, + 13155264, + "", + Decimal("0.00"), + Decimal("0.00"), + Decimal("0.00"), + Decimal("0.00"), + Decimal("0.00"), + "7", + "", + "", + ), + ( + 69695, + "CASTROL", + "15667c", + "Castrol EDGE Titanium 5W30 LL 504.00/507.00 (1L).Масло моторное", + "Castrol-15667C-Castrol EDGE Titanium 5W30 LL 504.00/507.00 (1L).Масло моторное", + Decimal("40.00"), + "", + Decimal("599.15"), + 0, + 13155265, + "", + Decimal("0.00"), + Decimal("0.00"), + Decimal("0.00"), + Decimal("0.00"), + Decimal("0.00"), + "7", + "", + "", + ), +) print(data) @@ -21,7 +67,6 @@ def my_default(obj): return str(obj) -import json json_data = json.dumps(data, ensure_ascii=False, default=str) print(json_data) diff --git a/json_examples/e.py b/json_examples/e.py index 36086d2ca..5397e5aa3 100644 --- a/json_examples/e.py +++ b/json_examples/e.py @@ -1,31 +1,35 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" import json -if __name__ == '__main__': +if __name__ == "__main__": # Compact encoding: - print(json.dumps([1, 2, 3, {'4': 5, '6': 7}])) + print(json.dumps([1, 2, 3, {"4": 5, "6": 7}])) # Custom separators: - print(json.dumps([1, 2, 3, {'4': 5, '6': 7}], separators=(',', ':'))) + print(json.dumps([1, 2, 3, {"4": 5, "6": 7}], separators=(",", ":"))) # Pretty print: - print(json.dumps([1, 2, 3, {'4': 5, '6': 7}], indent=4)) + print(json.dumps([1, 2, 3, {"4": 5, "6": 7}], indent=4)) # Sort key: - print(json.dumps(["z", "b", "d", {'b': 5, 'a': 7, 'c': 2}], sort_keys=True, indent=4)) + print( + json.dumps(["z", "b", "d", {"b": 5, "a": 7, "c": 2}], sort_keys=True, indent=4) + ) # Create object: obj = [ - 3, 2, 1, + 3, + 2, + 1, [2, 1, 1, 4], { - 'a': 1, - 'b': 2, - 'c': 3, - } + "a": 1, + "b": 2, + "c": 3, + }, ] print(obj) @@ -39,5 +43,5 @@ # Test: print(obj2[3][0]) - print(obj2[4]['c']) - print(obj2[4]['b']) + print(obj2[4]["c"]) + print(obj2[4]["b"]) diff --git a/json_examples/load_with_replacing_value.py b/json_examples/load_with_replacing_value.py index 10ce4e5f8..86edf39a0 100644 --- a/json_examples/load_with_replacing_value.py +++ b/json_examples/load_with_replacing_value.py @@ -1,17 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import json def dict_clean(items, default): - return { - k: default if v is None else v - for k, v in items - } + return {k: default if v is None else v for k, v in items} text = """ @@ -30,15 +27,17 @@ def dict_clean(items, default): genre_translate = json.loads( - text, encoding='utf-8', - object_pairs_hook=lambda items: dict_clean(items, default=[]) + text, + encoding="utf-8", + object_pairs_hook=lambda items: dict_clean(items, default=[]), ) print(genre_translate) # {'Action-bar': [], 'Action': 'Action', 'Children': [{'Action': []}, {'Action': True}, {'Action': 'false'}, {'Action': {'need': []}}], 'RGB-bar': []} genre_translate = json.loads( - text, encoding='utf-8', - object_pairs_hook=lambda items: dict_clean(items, default="") + text, + encoding="utf-8", + object_pairs_hook=lambda items: dict_clean(items, default=""), ) print(genre_translate) # {'Action-bar': '', 'Action': 'Action', 'Children': [{'Action': ''}, {'Action': True}, {'Action': 'false'}, {'Action': {'need': ''}}], 'RGB-bar': ''} diff --git a/jsonmerge.py b/jsonmerge.py index 3c30fc8e3..9abd250a6 100644 --- a/jsonmerge.py +++ b/jsonmerge.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import json @@ -40,8 +40,7 @@ # TODO: нужно тестить, возможно, алгоритм где-то дырявый - -def jsonmerge(j1, j2): +def jsonmerge(j1, j2) -> None: for k, v in j2.items(): if k in j1 and isinstance(j1[k], dict) and isinstance(v, dict): jsonmerge(j1[k], v) diff --git a/k_notation_to_number.py b/k_notation_to_number.py index 84985a5c0..1aedeed5e 100644 --- a/k_notation_to_number.py +++ b/k_notation_to_number.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def k_notation_to_number(num_str: str) -> int: - i = num_str.index('k') + i = num_str.index("k") num, size = num_str[:i], len(num_str[i:]) - return int(float(num) * (1000 ** size)) + return int(float(num) * (1000**size)) -if __name__ == '__main__': - print(k_notation_to_number("1k")) # 1000 +if __name__ == "__main__": + print(k_notation_to_number("1k")) # 1000 print(k_notation_to_number("1.5kk")) # 1500000 - print(k_notation_to_number("1kkk")) # 1000000000 + print(k_notation_to_number("1kkk")) # 1000000000 diff --git a/kivy_examples/generate_form_with_cycle.py b/kivy_examples/generate_form_with_cycle.py index e7b20072d..9cab7a17e 100644 --- a/kivy_examples/generate_form_with_cycle.py +++ b/kivy_examples/generate_form_with_cycle.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from kivy.app import App @@ -10,17 +10,17 @@ class MyWidget(GridLayout): - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self.cols = 1 self.spacing = 10 self.padding = 10 - label_list = ['Собака', 'Сосед', 'Кот', 'Биткоин'] + label_list = ["Собака", "Сосед", "Кот", "Биткоин"] for i, title in enumerate(label_list): label = Label(text=title) - label.id = 'id:' + str(i) + label.id = "id:" + str(i) self.add_widget(label) @@ -30,5 +30,5 @@ def build(self): return MyWidget() -if __name__ == '__main__': +if __name__ == "__main__": MyApp().run() diff --git a/kivy_examples/hello_world.py b/kivy_examples/hello_world.py index 375774a11..0b5d70f2f 100644 --- a/kivy_examples/hello_world.py +++ b/kivy_examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Install kivy: @@ -15,8 +15,8 @@ class MyApp(App): def build(self): - return Label(text='Hello world') + return Label(text="Hello world") -if __name__ == '__main__': +if __name__ == "__main__": MyApp().run() diff --git a/kivy_examples/kivymd__example.py b/kivy_examples/kivymd__example.py index 524c65ef8..d46d8b185 100644 --- a/kivy_examples/kivymd__example.py +++ b/kivy_examples/kivymd__example.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os @@ -11,23 +11,23 @@ from kivymd.uix.list import OneLineListItem -KV = ''' +KV = """ ScrollView: MDList: id: container -''' +""" class Test(MDApp): def build(self): return Builder.load_string(KV) - def on_start(self): - for i in os.listdir(path='.'): + def on_start(self) -> None: + for i in os.listdir(path="."): self.root.ids.container.add_widget( OneLineListItem(text=i, on_release=lambda item, i=i: print(i)) ) -if __name__ == '__main__': +if __name__ == "__main__": Test().run() diff --git a/kivy_examples/kv_file_switch_between_screens/main.py b/kivy_examples/kv_file_switch_between_screens/main.py new file mode 100644 index 000000000..495422a27 --- /dev/null +++ b/kivy_examples/kv_file_switch_between_screens/main.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from kivy.app import App +from kivy.uix.screenmanager import Screen + + +class EegScreen(Screen): + pass + + +class SettingsScreen(Screen): + pass + + +class ExchangeScreen(Screen): + pass + + +class ConnectionsScreen(Screen): + pass + + +class MyApp(App): + pass + + +if __name__ == "__main__": + MyApp().run() diff --git a/kivy_examples/kv_file_switch_between_screens/my.kv b/kivy_examples/kv_file_switch_between_screens/my.kv new file mode 100644 index 000000000..905ab225f --- /dev/null +++ b/kivy_examples/kv_file_switch_between_screens/my.kv @@ -0,0 +1,66 @@ +BoxLayout: + orientation: 'horizontal' + + BoxLayout: + orientation: 'vertical' + + Label: + text: 'PERMANENT' + + Button: + text: 'Connections' + font_size: 20 + on_release: screen_manager.current = 'ConnectionsScreen' + Button: + text: 'Exchange' + font_size: 20 + on_release: screen_manager.current = 'ExchangeScreen' + + Button: + text: 'EEG Signal' + font_size: 20 + on_release: screen_manager.current = 'EegScreen' + + Button: + text: 'Settings' + font_size: 20 + on_release: screen_manager.current = 'SettingsScreen' + + ScreenManager: + id: screen_manager + + EegScreen: + id: eeg_screen + name: 'EegScreen' + + SettingsScreen: + id: settings_screen + name: 'SettingsScreen' + + ExchangeScreen: + id: exchange_screen + name: 'ExchangeScreen' + + ConnectionsScreen: + id: connections_screen + name: 'ConnectionsScreen' + +: + FloatLayout: + Label: + text: 'EegScreen' + +: + FloatLayout: + Label: + text: 'SettingsScreen' + +: + FloatLayout: + Label: + text: 'ExchangeScreen' + +: + FloatLayout: + Label: + text: 'ConnectionsScreen' \ No newline at end of file diff --git a/kivy_examples/login_screen.py b/kivy_examples/login_screen.py index f13d0054c..f745d35ed 100644 --- a/kivy_examples/login_screen.py +++ b/kivy_examples/login_screen.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from kivy.app import App @@ -13,26 +13,26 @@ class LoginScreen(GridLayout): - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self.cols = 2 self.spacing = 10 self.padding = 10 - self.add_widget(Label(text='User Name')) + self.add_widget(Label(text="User Name")) self.username = TextInput(multiline=False) - self.username.text = 'admin' + self.username.text = "admin" self.add_widget(self.username) - self.add_widget(Label(text='password')) + self.add_widget(Label(text="password")) self.password = TextInput(password=True, multiline=False) - self.password.text = 'admin' + self.password.text = "admin" self.add_widget(self.password) self.add_widget(Button(text="Ok", on_press=self.check)) - def check(self, button): + def check(self, button) -> None: print("auth called") message = "Success!" if self.username.text == "admin" else "Need admin!" @@ -53,5 +53,5 @@ def build(self): return LoginScreen() -if __name__ == '__main__': +if __name__ == "__main__": MyLoginScreenApp().run() diff --git a/kivy_examples/switch_between_widgets.py b/kivy_examples/switch_between_widgets.py index 678292c07..2009ce5a2 100644 --- a/kivy_examples/switch_between_widgets.py +++ b/kivy_examples/switch_between_widgets.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from kivy.app import App @@ -11,7 +11,7 @@ class ScreenMain(Screen): - def __init__ (self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) box_layout = BoxLayout(orientation="vertical", spacing=5, padding=[10]) @@ -26,13 +26,13 @@ def __init__ (self, **kwargs): box_layout.add_widget(button_new_pasword) self.add_widget(box_layout) - def _on_press_button_new_pasword(self, *args): - self.manager.transition.direction = 'left' - self.manager.current = 'len_password' + def _on_press_button_new_pasword(self, *args) -> None: + self.manager.transition.direction = "left" + self.manager.current = "len_password" class ScreenLenPassword(Screen): - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) box_layout = BoxLayout(orientation="vertical", spacing=5, padding=[10]) @@ -47,16 +47,16 @@ def __init__(self, **kwargs): box_layout.add_widget(button_new_pasword) self.add_widget(box_layout) - def _on_press_button_new_pasword(self, *args): - self.manager.transition.direction = 'right' - self.manager.current = 'main_screen' + def _on_press_button_new_pasword(self, *args) -> None: + self.manager.transition.direction = "right" + self.manager.current = "main_screen" class MainApp(App): def build(self): sm = ScreenManager() - sm.add_widget(ScreenMain(name='main_screen')) - sm.add_widget(ScreenLenPassword(name='len_password')) + sm.add_widget(ScreenMain(name="main_screen")) + sm.add_widget(ScreenLenPassword(name="len_password")) return sm diff --git a/krakozyabry_tx/krakozyabry_tx.py b/krakozyabry_tx/krakozyabry_tx.py index d17fb9f99..0535c6803 100644 --- a/krakozyabry_tx/krakozyabry_tx.py +++ b/krakozyabry_tx/krakozyabry_tx.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Пример конвертирования кракозябры "РќРµ указана точка" в человеческий вид.""" @@ -11,6 +11,6 @@ from PySide.QtGui import QApplication, QMessageBox -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) - QMessageBox.information(None, None, open('t', encoding='utf-8').read()) + QMessageBox.information(None, None, open("t", encoding="utf-8").read()) diff --git a/languagetool/check.py b/languagetool/check.py index 09f440aba..53effbed8 100644 --- a/languagetool/check.py +++ b/languagetool/check.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -14,6 +14,9 @@ """ +import requests + + def check_ru(text: str) -> list(): """ Функция делает запрос на https://languagetool.org/ чтобы проверить на правильность указанный текст. @@ -22,20 +25,19 @@ def check_ru(text: str) -> list(): """ - url = 'https://languagetool.org/api/v2/check' + url = "https://languagetool.org/api/v2/check" post_data = { - 'disabledRules': 'WHITESPACE_RULE', - 'allowIncompleteResults': 'true', - 'text': text, - 'language': 'ru', + "disabledRules": "WHITESPACE_RULE", + "allowIncompleteResults": "true", + "text": text, + "language": "ru", } - import requests rs = requests.post(url, data=post_data) if not rs.ok: - raise Exception('Проблема с {}, status_code = {}'.format(url, rs.status_code)) + raise Exception(f"Проблема с {url}, status_code = {rs.status_code}") - return rs.json()['matches'] + return rs.json()["matches"] def is_ok_ru(text: str) -> bool: @@ -47,7 +49,7 @@ def is_ok_ru(text: str) -> bool: return not check_ru(text) -if __name__ == '__main__': +if __name__ == "__main__": text = """\ Интересует другое почему она возникла? Дубаль два, надеюсь все пройдет отлично. @@ -58,20 +60,22 @@ def is_ok_ru(text: str) -> bool: if not matches: print("Ошибок нет") else: - print('Найденные проблемы:') + print("Найденные проблемы:") for match in matches: - error = match['message'] - context = match['context'] - offset = context['offset'] - length = context['length'] + error = match["message"] + context = match["context"] + offset = context["offset"] + length = context["length"] - error_text = context['text'][offset: offset + length] - print('"{}" [{}:{}]: "{}" -> {}'.format(error_text, offset, length, error, - [i['value'] for i in match['replacements']])) + error_text = context["text"][offset : offset + length] + replacements = [i["value"] for i in match["replacements"]] + print( + f'"{error_text}" [{offset}:{length}]: "{error}" -> {replacements}' + ) print() - print('-' * 20) + print("-" * 20) print(is_ok_ru(text)) print(is_ok_ru("Все хорошо!")) diff --git a/leftpad.py b/leftpad.py index 3f15b5e23..d54f944c1 100644 --- a/leftpad.py +++ b/leftpad.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Для интереса написать аналог NPM leftpad. @@ -18,7 +18,7 @@ # // => "01" -def leftpad(text, size, ch=' '): +def leftpad(text: str | int, size: int, ch: str | int = " "): text = str(text) ch = str(ch) @@ -27,17 +27,17 @@ def leftpad(text, size, ch=' '): # Версия с использованием общего алгоритм -def leftpad2(text, size, ch=' '): +def leftpad2(text: str | int, size: int, ch: str | int = " "): text = str(text) - ch = str(ch) - text_size = len(text) - result = '' pad_len = size - text_size if pad_len <= 0: return text + ch = str(ch) + + result = "" for i in range(pad_len): result += ch result += text @@ -45,11 +45,11 @@ def leftpad2(text, size, ch=' '): return result -if __name__ == '__main__': - assert leftpad('foo', 5) == " foo" - assert leftpad('foobar', 6) == "foobar" +if __name__ == "__main__": + assert leftpad("foo", 5) == " foo" + assert leftpad("foobar", 6) == "foobar" assert leftpad(1, 2, 0) == "01" - assert leftpad('foo', 5) == leftpad2('foo', 5) - assert leftpad('foobar', 6) == leftpad2('foobar', 6) - assert leftpad(1, 2, 0) == leftpad(1, 2, 0) + assert leftpad("foo", 5) == leftpad2("foo", 5) + assert leftpad("foobar", 6) == leftpad2("foobar", 6) + assert leftpad(1, 2, 0) == leftpad2(1, 2, 0) diff --git a/lex_yacc__examples/sly__examples/added_string_and_print.py b/lex_yacc__examples/sly__examples/added_string_and_print.py index e4fbd0215..2b69916a5 100644 --- a/lex_yacc__examples/sly__examples/added_string_and_print.py +++ b/lex_yacc__examples/sly__examples/added_string_and_print.py @@ -1,49 +1,59 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/dabeaz/sly/ # SOURCE: https://sly.readthedocs.io/en/latest/sly.html -from typing import Union - # pip install sly from sly import Lexer, Parser class MyLexer(Lexer): - tokens = {NAME, TEXT, NUMBER, PRINT, PLUS, TIMES, MINUS, DIVIDE, ASSIGN, LPAREN, RPAREN} - ignore = ' \t' + tokens = { + NAME, + TEXT, + NUMBER, + PRINT, + PLUS, + TIMES, + MINUS, + DIVIDE, + ASSIGN, + LPAREN, + RPAREN, + } + ignore = " \t" # Tokens - NUMBER = r'\d+' - NAME = r'[a-zA-Z_][a-zA-Z0-9_]*' - NAME['print'] = PRINT + NUMBER = r"\d+" + NAME = r"[a-zA-Z_][a-zA-Z0-9_]*" + NAME["print"] = PRINT @_(r'".+?"', r"'.+?'") def TEXT(self, t): return t # Special symbols - PLUS = r'\+' - MINUS = r'-' - TIMES = r'\*' - DIVIDE = r'/' - ASSIGN = r'=' - LPAREN = r'\(' - RPAREN = r'\)' + PLUS = r"\+" + MINUS = r"-" + TIMES = r"\*" + DIVIDE = r"/" + ASSIGN = r"=" + LPAREN = r"\(" + RPAREN = r"\)" # Ignored pattern - ignore_newline = r'\n+' + ignore_newline = r"\n+" # Extra action for newlines - def ignore_newline(self, t): - self.lineno += t.value.count('\n') + def ignore_newline(self, t) -> None: + self.lineno += t.value.count("\n") - def error(self, t): + def error(self, t) -> None: print(f"Illegal character '{t.value[0]}'") self.index += 1 @@ -52,27 +62,27 @@ class MyParser(Parser): tokens = MyLexer.tokens precedence = ( - ('left', PLUS, MINUS), - ('left', TIMES, DIVIDE), - ('right', UMINUS), + ("left", PLUS, MINUS), + ("left", TIMES, DIVIDE), + ("right", UMINUS), ) - def __init__(self): + def __init__(self) -> None: self.names = dict() - @_('NAME ASSIGN expr') - def statement(self, p): + @_("NAME ASSIGN expr") + def statement(self, p) -> None: self.names[p.NAME] = p.expr - @_('expr') - def statement(self, p) -> Union[int, str]: + @_("expr") + def statement(self, p) -> int | str: return p.expr - @_('PRINT expr') - def statement(self, p): + @_("PRINT expr") + def statement(self, p) -> None: print(p.expr) - @_('expr PLUS expr') + @_("expr PLUS expr") def expr(self, p): # Javascript style :D if isinstance(p.expr0, str) or isinstance(p.expr1, str): @@ -80,44 +90,44 @@ def expr(self, p): return p.expr0 + p.expr1 - @_('expr MINUS expr') + @_("expr MINUS expr") def expr(self, p): return p.expr0 - p.expr1 - @_('expr TIMES expr') + @_("expr TIMES expr") def expr(self, p): return p.expr0 * p.expr1 - @_('expr DIVIDE expr') + @_("expr DIVIDE expr") def expr(self, p): return p.expr0 / p.expr1 - @_('MINUS expr %prec UMINUS') + @_("MINUS expr %prec UMINUS") def expr(self, p): return -p.expr - @_('LPAREN expr RPAREN') + @_("LPAREN expr RPAREN") def expr(self, p): return p.expr - @_('NUMBER') + @_("NUMBER") def expr(self, p) -> int: return int(p.NUMBER) - @_('TEXT') + @_("TEXT") def expr(self, p) -> str: return p.TEXT[1:-1] - @_('NAME') + @_("NAME") def expr(self, p): try: return self.names[p.NAME] except LookupError: - print(f'Undefined name {p.NAME!r}') + print(f"Undefined name {p.NAME!r}") return 0 -if __name__ == '__main__': +if __name__ == "__main__": lexer = MyLexer() parser = MyParser() @@ -125,7 +135,7 @@ def expr(self, p): "12" + '3' + 456 """.strip() value = parser.parse(lexer.tokenize(text)) - print(f'{text} = {value!r}') + print(f"{text} = {value!r}") # 12 + '3' + 456 = '123456' print() @@ -134,19 +144,19 @@ def expr(self, p): "12" + '3' """.strip() value = parser.parse(lexer.tokenize(text)) - print(f'{text} = {value!r}') + print(f"{text} = {value!r}") # "12" + '3' = '123' print() items = [ """text = "Hello " + 'World!'""", - 'text', + "text", ] for line in items: value = parser.parse(lexer.tokenize(line)) if value is not None: - print(f'{line} = {value!r}') + print(f"{line} = {value!r}") else: print(line) # text = "Hello " + 'World!' @@ -164,7 +174,7 @@ def expr(self, p): while True: try: - text = input('calc > ') + text = input("calc > ") except EOFError: break if text: diff --git a/lex_yacc__examples/sly__examples/added_ternary_operator.py b/lex_yacc__examples/sly__examples/added_ternary_operator.py index 1885cd90e..3ab9e16bc 100644 --- a/lex_yacc__examples/sly__examples/added_ternary_operator.py +++ b/lex_yacc__examples/sly__examples/added_ternary_operator.py @@ -1,73 +1,88 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/dabeaz/sly/ # SOURCE: https://sly.readthedocs.io/en/latest/sly.html -from typing import Union - # pip install sly from sly import Lexer, Parser class MyLexer(Lexer): tokens = { - NAME, NUMBER, IF, ELSE, TRUE, FALSE, - PLUS, TIMES, MINUS, DIVIDE, LPAREN, RPAREN, - ASSIGN, EQ, LT, LE, GT, GE, NE, - QUESTION, COLON, + NAME, + NUMBER, + IF, + ELSE, + TRUE, + FALSE, + PLUS, + TIMES, + MINUS, + DIVIDE, + LPAREN, + RPAREN, + ASSIGN, + EQ, + LT, + LE, + GT, + GE, + NE, + QUESTION, + COLON, } - ignore = ' \t' + ignore = " \t" # Tokens - NUMBER = r'\d+' - NAME = r'[a-zA-Z_][a-zA-Z0-9_]*' - NAME['if'] = IF - NAME['else'] = ELSE - NAME['true'] = TRUE - NAME['false'] = FALSE + NUMBER = r"\d+" + NAME = r"[a-zA-Z_][a-zA-Z0-9_]*" + NAME["if"] = IF + NAME["else"] = ELSE + NAME["true"] = TRUE + NAME["false"] = FALSE # Special symbols - PLUS = r'\+' - MINUS = r'-' - TIMES = r'\*' - DIVIDE = r'/' - LPAREN = r'\(' - RPAREN = r'\)' - - QUESTION = r'\?' - COLON = r'\:' - - EQ = r'==' - ASSIGN = r'=' - LE = r'<=' - LT = r'<' - GE = r'>=' - GT = r'>' - NE = r'!=' - - @_(r'true') + PLUS = r"\+" + MINUS = r"-" + TIMES = r"\*" + DIVIDE = r"/" + LPAREN = r"\(" + RPAREN = r"\)" + + QUESTION = r"\?" + COLON = r"\:" + + EQ = r"==" + ASSIGN = r"=" + LE = r"<=" + LT = r"<" + GE = r">=" + GT = r">" + NE = r"!=" + + @_(r"true") def TRUE(self, t): t.value = True return t - @_(r'false') + @_(r"false") def FALSE(self, t): t.value = False return t # Ignored pattern - ignore_newline = r'\n+' + ignore_newline = r"\n+" # Extra action for newlines - def ignore_newline(self, t): - self.lineno += t.value.count('\n') + def ignore_newline(self, t) -> None: + self.lineno += t.value.count("\n") - def error(self, t): + def error(self, t) -> None: print(f"Illegal character '{t.value[0]}'") self.index += 1 @@ -77,113 +92,113 @@ class MyParser(Parser): tokens = MyLexer.tokens precedence = ( - ('left', IF, ELSE), - ('left', EQ, NE, LT, LE, GT, GE), - ('left', PLUS, MINUS), - ('left', TIMES, DIVIDE), - ('right', UMINUS), + ("left", IF, ELSE), + ("left", EQ, NE, LT, LE, GT, GE), + ("left", PLUS, MINUS), + ("left", TIMES, DIVIDE), + ("right", UMINUS), ) - def __init__(self): + def __init__(self) -> None: self.names = dict() - @_('NAME ASSIGN expr') - def statement(self, p): + @_("NAME ASSIGN expr") + def statement(self, p) -> None: self.names[p.NAME] = p.expr - @_('expr') + @_("expr") def statement(self, p) -> int: return p.expr # Ternary operator python - @_('expr IF expr ELSE expr') + @_("expr IF expr ELSE expr") def expr(self, p): return p.expr0 if p.expr1 else p.expr2 # Ternary operator c - @_('expr QUESTION expr COLON expr') + @_("expr QUESTION expr COLON expr") def expr(self, p): return p.expr1 if p.expr0 else p.expr2 - @_('expr LT expr') + @_("expr LT expr") def expr(self, p) -> bool: return p.expr0 < p.expr1 - @_('expr LE expr') + @_("expr LE expr") def expr(self, p) -> bool: return p.expr0 <= p.expr1 - @_('expr GT expr') + @_("expr GT expr") def expr(self, p) -> bool: return p.expr0 > p.expr1 - @_('expr GE expr') + @_("expr GE expr") def expr(self, p) -> bool: return p.expr0 >= p.expr1 - @_('expr EQ expr') + @_("expr EQ expr") def expr(self, p) -> bool: return p.expr0 == p.expr1 - @_('expr NE expr') + @_("expr NE expr") def expr(self, p) -> bool: return p.expr0 != p.expr1 - @_('expr PLUS expr') + @_("expr PLUS expr") def expr(self, p): return p.expr0 + p.expr1 - @_('expr MINUS expr') + @_("expr MINUS expr") def expr(self, p): return p.expr0 - p.expr1 - @_('expr TIMES expr') + @_("expr TIMES expr") def expr(self, p): return p.expr0 * p.expr1 - @_('expr DIVIDE expr') + @_("expr DIVIDE expr") def expr(self, p): return p.expr0 / p.expr1 - @_('MINUS expr %prec UMINUS') + @_("MINUS expr %prec UMINUS") def expr(self, p): return -p.expr - @_('LPAREN expr RPAREN') + @_("LPAREN expr RPAREN") def expr(self, p): return p.expr - @_('NUMBER') + @_("NUMBER") def expr(self, p) -> int: return int(p.NUMBER) - @_('TRUE', 'FALSE') + @_("TRUE", "FALSE") def expr(self, p) -> bool: return p[0] - @_('NAME') + @_("NAME") def expr(self, p): try: return self.names[p.NAME] except LookupError: - print(f'Undefined name {p.NAME!r}') + print(f"Undefined name {p.NAME!r}") return 0 -if __name__ == '__main__': +if __name__ == "__main__": lexer = MyLexer() parser = MyParser() items = [ - 'value = 2 > 1', - 'value', - 'value = 2 + 1 == 6 / 2', - 'value', + "value = 2 > 1", + "value", + "value = 2 + 1 == 6 / 2", + "value", ] for line in items: value = parser.parse(lexer.tokenize(line)) if value is not None: - print(f'{line} = {value!r}') + print(f"{line} = {value!r}") else: print(line) # value = 2 > 1 @@ -195,22 +210,22 @@ def expr(self, p): items = [ # Ternary operator python - '123 if value else 456', - '1 if true else 0', - '1 if false else 0', - 'true if 2 + 2 == 4 else false', + "123 if value else 456", + "1 if true else 0", + "1 if false else 0", + "true if 2 + 2 == 4 else false", # Ternary operator c - '2 + 1 == 6 / 2 ? 123 : 456', - 'true ? 1 : 0', - 'false ? 1 : 0', - 'x = true ? 123 : 456', - 'x' + "2 + 1 == 6 / 2 ? 123 : 456", + "true ? 1 : 0", + "false ? 1 : 0", + "x = true ? 123 : 456", + "x", ] for line in items: value = parser.parse(lexer.tokenize(line)) if value is not None: - print(f'({line}) -> {value!r}') + print(f"({line}) -> {value!r}") else: print(line) # (123 if value else 456) -> 123 @@ -227,7 +242,7 @@ def expr(self, p): while True: try: - text = input('calc > ') + text = input("calc > ") except EOFError: break if text: diff --git a/lex_yacc__examples/sly__examples/calc.py b/lex_yacc__examples/sly__examples/calc.py index 6d4393011..9441833f6 100644 --- a/lex_yacc__examples/sly__examples/calc.py +++ b/lex_yacc__examples/sly__examples/calc.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/dabeaz/sly/ @@ -14,29 +14,29 @@ class CalcLexer(Lexer): tokens = {NAME, NUMBER, PLUS, TIMES, MINUS, DIVIDE, ASSIGN, LPAREN, RPAREN} - ignore = ' \t' + ignore = " \t" # Tokens - NAME = r'[a-zA-Z_][a-zA-Z0-9_]*' - NUMBER = r'\d+' + NAME = r"[a-zA-Z_][a-zA-Z0-9_]*" + NUMBER = r"\d+" # Special symbols - PLUS = r'\+' - MINUS = r'-' - TIMES = r'\*' - DIVIDE = r'/' - ASSIGN = r'=' - LPAREN = r'\(' - RPAREN = r'\)' + PLUS = r"\+" + MINUS = r"-" + TIMES = r"\*" + DIVIDE = r"/" + ASSIGN = r"=" + LPAREN = r"\(" + RPAREN = r"\)" # Ignored pattern - ignore_newline = r'\n+' + ignore_newline = r"\n+" # Extra action for newlines - def ignore_newline(self, t): - self.lineno += t.value.count('\n') + def ignore_newline(self, t) -> None: + self.lineno += t.value.count("\n") - def error(self, t): + def error(self, t) -> None: print(f"Illegal character '{t.value[0]}'") self.index += 1 @@ -45,92 +45,103 @@ class CalcParser(Parser): tokens = CalcLexer.tokens precedence = ( - ('left', PLUS, MINUS), - ('left', TIMES, DIVIDE), - ('right', UMINUS), + ("left", PLUS, MINUS), + ("left", TIMES, DIVIDE), + ("right", UMINUS), ) - def __init__(self): + def __init__(self) -> None: self.names = dict() - @_('NAME ASSIGN expr') - def statement(self, p): + @_("NAME ASSIGN expr") + def statement(self, p) -> None: self.names[p.NAME] = p.expr - @_('expr') + @_("expr") def statement(self, p) -> int: return p.expr - @_('expr PLUS expr') + @_("expr PLUS expr") def expr(self, p): return p.expr0 + p.expr1 - @_('expr MINUS expr') + @_("expr MINUS expr") def expr(self, p): return p.expr0 - p.expr1 - @_('expr TIMES expr') + @_("expr TIMES expr") def expr(self, p): return p.expr0 * p.expr1 - @_('expr DIVIDE expr') + @_("expr DIVIDE expr") def expr(self, p): return p.expr0 / p.expr1 - @_('MINUS expr %prec UMINUS') + @_("MINUS expr %prec UMINUS") def expr(self, p): return -p.expr - @_('LPAREN expr RPAREN') + @_("LPAREN expr RPAREN") def expr(self, p): return p.expr - @_('NUMBER') + @_("NUMBER") def expr(self, p) -> int: return int(p.NUMBER) - @_('NAME') + @_("NAME") def expr(self, p): try: return self.names[p.NAME] except LookupError: - print(f'Undefined name {p.NAME!r}') + print(f"Undefined name {p.NAME!r}") return 0 -if __name__ == '__main__': +if __name__ == "__main__": lexer = CalcLexer() parser = CalcParser() - text = '2 + 2 * 2' + text = "2 + 2 * 2" + print(list(lexer.tokenize(text))) + # [ + # Token(type='NUMBER', value='2', lineno=1, index=0, end=1), + # Token(type='PLUS', value='+', lineno=1, index=2, end=3), + # Token(type='NUMBER', value='2', lineno=1, index=4, end=5), + # Token(type='TIMES', value='*', lineno=1, index=6, end=7), + # Token(type='NUMBER', value='2', lineno=1, index=8, end=9) + # ] + value = parser.parse(lexer.tokenize(text)) - print(f'{text} = {value}') - # 2 + 2 * 2 = 6 + print(f"{text!r} = {value}") + # '2 + 2 * 2' = 6 print() items = [ - 'a = 2', - 'a = a * 2', - 'b = 2', - 'a + b + 1', + "a = 2", + "a = a * 2", + "b = 2", + "a + b + 1", ] for line in items: value = parser.parse(lexer.tokenize(line)) if value is not None: - print(f'{line} = {value}') + print(f"{line!r} = {value}") else: - print(line) - # a = 2 - # a = a * 2 - # b = 2 - # a + b + 1 = 7 + print(f"{line!r}") + """ + 'a = 2' + 'a = a * 2' + 'b = 2' + 'a + b + 1' = 7 + """ print() while True: try: - text = input('calc > ') + text = input("calc > ") except EOFError: break if text: diff --git a/lex_yacc__examples/sly__examples/json_by_goodmami.py b/lex_yacc__examples/sly__examples/json_by_goodmami.py index 1fbbc1cc5..157fd8f85 100644 --- a/lex_yacc__examples/sly__examples/json_by_goodmami.py +++ b/lex_yacc__examples/sly__examples/json_by_goodmami.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/goodmami/python-parsing-benchmarks/blob/611cfc20ca7b61f1f489b0e56f201f6888a5c67b/bench/helpers.py @@ -23,23 +23,23 @@ _json_unesc_re = re.compile(r'\\(["/\\bfnrt]|u[0-9A-Fa-f])') _json_unesc_map = { '"': '"', - '/': '/', - '\\': '\\', - 'b': '\b', - 'f': '\f', - 'n': '\n', - 'r': '\r', - 't': '\t', + "/": "/", + "\\": "\\", + "b": "\b", + "f": "\f", + "n": "\n", + "r": "\r", + "t": "\t", } def _json_unescape(m): c = m.group(1) - if c[0] == 'u': + if c[0] == "u": return chr(int(c[1:], 16)) c2 = _json_unesc_map.get(c) if not c2: - raise ValueError(f'invalid escape sequence: {m.group(0)}') + raise ValueError(f"invalid escape sequence: {m.group(0)}") return c2 @@ -49,11 +49,11 @@ def json_unescape(s): def compile(): class JsonLexer(Lexer): - tokens = { STRING, NUMBER, TRUE, FALSE, NULL } - ignore = ' \t\n\r' - literals = { '{', '}', '[', ']', ':', ',' } + tokens = {STRING, NUMBER, TRUE, FALSE, NULL} + ignore = " \t\n\r" + literals = {"{", "}", "[", "]", ":", ","} - @_(r'-?(0|[1-9][0-9]*)(\.[0-9]+)?([Ee][+-]?[0-9]+)?') + @_(r"-?(0|[1-9][0-9]*)(\.[0-9]+)?([Ee][+-]?[0-9]+)?") def NUMBER(self, t): try: t.value = int(t.value) @@ -66,25 +66,24 @@ def STRING(self, t): t.value = json_unescape(t.value) return t - @_(r'true') + @_(r"true") def TRUE(self, t): t.value = True return t - @_(r'false') + @_(r"false") def FALSE(self, t): t.value = False return t - @_(r'null') + @_(r"null") def NULL(self, t): t.value = None return t - class JsonParser(Parser): tokens = JsonLexer.tokens - start = 'value' + start = "value" @_(r'"{" [ pairs ] "}"') def value(self, p): @@ -112,11 +111,7 @@ def value(self, p): def items(self, p): return [p.value0] + p.value1 - @_('STRING', - 'NUMBER', - 'TRUE', - 'FALSE', - 'NULL') + @_("STRING", "NUMBER", "TRUE", "FALSE", "NULL") def value(self, p): return p[0] @@ -132,7 +127,7 @@ def error(self, p): parse = compile() -if __name__ == '__main__': +if __name__ == "__main__": text = '{"abc": [1, 2.5, 3.0, true, null], "value": "123"}' print(parse(text)) diff --git a/lex_yacc__examples/sly__examples/json_by_hadware.py b/lex_yacc__examples/sly__examples/json_by_hadware.py new file mode 100644 index 000000000..8694f3572 --- /dev/null +++ b/lex_yacc__examples/sly__examples/json_by_hadware.py @@ -0,0 +1,89 @@ +from sly import Lexer, Parser +import pprint + + +class JSONLexer(Lexer): + tokens = {"FLOAT", "INTEGER", "STRING"} + + literals = {"{", "}", "[", "]", ",", ":"} + ignore = " \t\n" + + @_(r"\".*?\"") + def STRING(self, t): + t.value = t.value.strip('"') + return t + + @_(r"\d+\.\d*") + def FLOAT(self, t): + t.value = float(t.value) + return t + + @_(r"\d+") + def INTEGER(self, t): + t.value = int(t.value) + return t + + +class JSONParser(Parser): + tokens = JSONLexer.tokens + start = "json" + + @_("object", "array") + def json(self, p): + return p[0] + + @_('"{" members "}"') + def object(self, p): + return {key: value for key, value in p.members} + + @_("pair") + def members(self, p): + return [p.pair] + + @_('pair "," members') + def members(self, p): + return [p.pair] + p.members + + @_('STRING ":" value') + def pair(self, p): + return p.STRING, p.value + + @_('"[" elements "]"') + def array(self, p): + return p.elements + + @_("value") + def elements(self, p): + return [p.value] + + @_('value "," elements') + def elements(self, p): + return [p.value] + p.elements + + @_("STRING", "INTEGER", "FLOAT", "object", "array") + def value(self, p): + return p[0] + + def error(self, p): + raise ValueError("Parsing error at token %s" % str(p)) + + +if __name__ == "__main__": + lexer = JSONLexer() + parser = JSONParser() + json_text = """ + {"menu": { + "id": "file", + "value": "File", + "popup": { + "menuitem": [ + {"value": "New", "onclick": "CreateNewDoc()"}, + {"value": "Open", "onclick": "OpenDoc()"}, + {"value": "Close", "onclick": "CloseDoc()"} + ], + "delay" : 1.5 + } + }} + """ + result = parser.parse(lexer.tokenize(json_text)) + pprint.pprint(result) diff --git a/lex_yacc__examples/sly__examples/key=value.py b/lex_yacc__examples/sly__examples/key=value.py new file mode 100644 index 000000000..5f240181b --- /dev/null +++ b/lex_yacc__examples/sly__examples/key=value.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install sly +from sly import Lexer, Parser + + +class MyLexer(Lexer): + tokens = {NAME, ASSIGN, VALUE} + ignore = " \t" + + # Tokens + NAME = r"[a-zA-Z_][a-zA-Z0-9_]*" + ASSIGN = "=" + VALUE = r".+" + + # Ignored pattern + ignore_newline = r"\n+" + + # Extra action for newlines + def ignore_newline(self, t) -> None: + self.lineno += t.value.count("\n") + + def error(self, t) -> None: + print(f"Illegal character {t.value[0]!r}") + self.index += 1 + + +class MyParser(Parser): + tokens = MyLexer.tokens + + def __init__(self) -> None: + self.names = dict() + + @_("NAME ASSIGN VALUE") + def statement(self, p) -> None: + self.names[p.NAME] = p.VALUE + + +if __name__ == "__main__": + lexer = MyLexer() + + text = "abc=123" + print(*list(lexer.tokenize(text)), sep="\n") + """ + Token(type='NAME', value='abc', lineno=1, index=0, end=3) + Token(type='ASSIGN', value='=', lineno=1, index=3, end=4) + Token(type='VALUE', value='123', lineno=1, index=4, end=7) + """ + print() + + parser = MyParser() + + lines = ["abc=123", "x = 999", "y = 111"] + for line in lines: + tokens = lexer.tokenize(line) + parser.parse(tokens) + + print(parser.names) + # {'abc': '123', 'x': '999', 'y': '111'} diff --git a/list_split_by_pairs.py b/list_split_by_pairs.py index 727b7112a..a7734a1ba 100644 --- a/list_split_by_pairs.py +++ b/list_split_by_pairs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" items = [5, 80, 3, 83, 1, 77, 1, 77, 2, 82, 1, 77, 5, 81, 2, 78, 1, 81, 5, 85, 5, 85, 4, 84, 2, 78, 1, 81, 3, 83] diff --git a/lived time.py b/lived time.py index a812fb90e..c54571569 100644 --- a/lived time.py +++ b/lived time.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import datetime + + my_bd = datetime.datetime(day=18, month=8, year=1992) my_life = datetime.datetime.today() - my_bd -print('lived time: days = {} <=> seconds = {}'.format(my_life.days, int(my_life.total_seconds()))) +print(f"lived time: days = {my_life.days} <=> seconds = {int(my_life.total_seconds())}") diff --git a/load_and_exec_py_from_url.py b/load_and_exec_py_from_url.py index 552977e1b..591734c0e 100644 --- a/load_and_exec_py_from_url.py +++ b/load_and_exec_py_from_url.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from urllib.request import urlopen -url = 'https://raw.githubusercontent.com/gil9red/SimplePyScripts/c4ef2f1636f7d75b87807e858ea1eea6116df773/print_triangle.py' + +url = "https://raw.githubusercontent.com/gil9red/SimplePyScripts/c4ef2f1636f7d75b87807e858ea1eea6116df773/print_triangle.py" with urlopen(url) as f: exec(f.read()) diff --git a/logged_human_time_to_seconds.py b/logged_human_time_to_seconds.py deleted file mode 100644 index 1289cbf02..000000000 --- a/logged_human_time_to_seconds.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def logged_human_time_to_seconds(human_time: str) -> int: - """Конвертирование человеко-читаемого времени в секунды. - - >>> logged_human_time_to_seconds("1 minute") - 60 - >>> logged_human_time_to_seconds("1 hour") - 3600 - >>> logged_human_time_to_seconds("6 hours, 30 minutes") - 23400 - >>> logged_human_time_to_seconds("1 day, 30 minutes") - 30600 - """ - - # Jira help: - # You can specify a time unit after a time value 'X', such as Xw, Xd, Xh or Xm, to represent weeks (w), - # days (d), hours (h) and minutes (m), respectively. - # If you do not specify a time unit, minute will be assumed. - # Your current conversion rates are 1w = 5d and 1d = 8h. - - total_seconds = 0 - - for part in human_time.split(', '): - value, metric = part.lower().split() - value = int(value) - - if 'minute' in metric: - total_seconds += value * 60 - - elif 'hour' in metric: - total_seconds += value * 60 * 60 - - elif 'day' in metric: - total_seconds += value * 8 * 60 * 60 - - elif 'week' in metric: - total_seconds += value * 5 * 8 * 60 * 60 - - return total_seconds - - -if __name__ == '__main__': - text = """\ -2 hours -1 hour, 30 minutes -4 hours -7 hours -1 day, 1 hour -6 hours, 30 minutes -30 minutes -1 day -1 hour -4 hours, 30 minutes -40 minutes -7 hours, 30 minutes -1 day, 30 minutes -5 hours -5 hours, 30 minutes -1 minute -1 hour, 20 minutes -6 hours -3 hours, 30 minutes -15 minutes -3 hours -""" - - for line in list(set(text.splitlines())): - print(line, logged_human_time_to_seconds(line)) diff --git a/logger_example.py b/logger_example.py deleted file mode 100644 index 3f34cec4b..000000000 --- a/logger_example.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def get_logger(name, file='log.txt', encoding='utf-8'): - import logging - log = logging.getLogger(name) - log.setLevel(logging.DEBUG) - - formatter = logging.Formatter('[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s') - - # Simple file handler - # fh = logging.FileHandler(file, encoding=encoding) - # or: - from logging.handlers import RotatingFileHandler - fh = RotatingFileHandler(file, maxBytes=10000000, backupCount=5, encoding=encoding) - fh.setFormatter(formatter) - log.addHandler(fh) - - import sys - sh = logging.StreamHandler(stream=sys.stdout) - sh.setFormatter(formatter) - log.addHandler(sh) - - return log - - -log = get_logger(__file__) - - -if __name__ == '__main__': - log.debug('foo') - log.debug('bar') - log.debug(__file__) diff --git a/logging__examples/console_and_rotating_file_handler.py b/logging__examples/console_and_rotating_file_handler.py new file mode 100644 index 000000000..d074e0874 --- /dev/null +++ b/logging__examples/console_and_rotating_file_handler.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging +import sys + +from logging.handlers import RotatingFileHandler + + +def get_logger(name, file="log.txt", encoding="utf-8"): + log = logging.getLogger(name) + log.setLevel(logging.DEBUG) + + formatter = logging.Formatter( + "[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-8s %(message)s" + ) + + # Simple file handler + # fh = logging.FileHandler(file, encoding=encoding) + # or: + fh = RotatingFileHandler( + file, maxBytes=10_000_000, backupCount=5, encoding=encoding + ) + fh.setFormatter(formatter) + log.addHandler(fh) + + sh = logging.StreamHandler(stream=sys.stdout) + sh.setFormatter(formatter) + log.addHandler(sh) + + return log + + +log = get_logger(__file__) + + +if __name__ == "__main__": + log.debug("foo") + log.debug("bar") + log.debug(__file__) diff --git a/logging__examples/log-config.yaml b/logging__examples/log-config.yaml new file mode 100644 index 000000000..b2e5fccfc --- /dev/null +++ b/logging__examples/log-config.yaml @@ -0,0 +1,33 @@ +version: 1 + +formatters: + default: + format: "[%(asctime)s] %(name)-10s %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s" + +handlers: + console: + class: "logging.StreamHandler" + formatter: "default" + stream: "ext://sys.stdout" + + main-file: &main-file + class: "logging.handlers.RotatingFileHandler" + formatter: "default" + filename: "main.log" + encoding: "utf-8" + backupCount: 5 + maxBytes: 10000000 + delay: true + + main2-file: + <<: *main-file + filename: "main2.log" + +loggers: + main: + handlers: ["console", "main-file"] + level: "DEBUG" + + main2: + handlers: ["console", "main2-file"] + level: "INFO" diff --git a/logging__examples/logger_from_dict.py b/logging__examples/logger_from_dict.py new file mode 100644 index 000000000..bf1b9c108 --- /dev/null +++ b/logging__examples/logger_from_dict.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging.config +from logging import getLogger +from pathlib import Path +from typing import Any + + +DIR_LOGS: Path = Path(__file__).resolve().parent / "logs" +DIR_LOGS.mkdir(parents=True, exist_ok=True) + + +LOGGING: dict[str, Any] = { + "version": 1, + "formatters": { + "default": { + "format": "[%(asctime)s] %(name)-10s %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s", + } + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + "formatter": "default", + }, + "main-file": { + "class": "logging.handlers.RotatingFileHandler", + "filename": "main.log", + "maxBytes": 10_000_000, + "backupCount": 5, + "encoding": "utf-8", + "formatter": "default", + }, + "main2-file": { + "class": "logging.handlers.RotatingFileHandler", + "filename": "main2.log", + "maxBytes": 10_000_000, + "backupCount": 5, + "encoding": "utf-8", + "formatter": "default", + }, + }, + "loggers": { + "main": { + "level": "DEBUG", + "handlers": [ + "console", + "main-file", + ] + }, + "main2": { + "level": "INFO", + "handlers": [ + "console", + "main2-file", + ] + }, + }, +} +for handler in LOGGING["handlers"].values(): + try: + handler["filename"] = DIR_LOGS / handler["filename"] + except KeyError: + pass + + +logging.config.dictConfig(LOGGING) + +log = getLogger("main") +log.debug("DEBUG") +log.info("INFO") + +print() + +log = getLogger("main2") +log.debug("DEBUG") +log.info("INFO") diff --git a/logging__examples/logger_from_yaml.py b/logging__examples/logger_from_yaml.py new file mode 100644 index 000000000..5e9d1b926 --- /dev/null +++ b/logging__examples/logger_from_yaml.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import logging.config +from logging import getLogger +from pathlib import Path +from typing import Any + +# pip install PyYAML +import yaml + + +DIR: Path = Path(__file__).resolve().parent + +DIR_LOGS: Path = DIR / "logs" +DIR_LOGS.mkdir(parents=True, exist_ok=True) + +CONFIG_LOG_FILE_NAME: Path = DIR / "log-config.yaml" + + +LOGGING: dict[str, Any] = yaml.safe_load( + CONFIG_LOG_FILE_NAME.read_text("utf-8") +) +for handler in LOGGING["handlers"].values(): + try: + handler["filename"] = DIR_LOGS / handler["filename"] + except KeyError: + pass + + +logging.config.dictConfig(LOGGING) + +log = getLogger("main") +log.debug("DEBUG") +log.info("INFO") + +print() + +log = getLogger("main2") +log.debug("DEBUG") +log.info("INFO") diff --git a/login_vk.com.py b/login_vk.com.py index f837fb718..cb667f76b 100644 --- a/login_vk.com.py +++ b/login_vk.com.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -18,37 +18,39 @@ QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, True) # Телефон или емейл -LOGIN = '' -PASSWORD = '' +LOGIN = "" +PASSWORD = "" -URL = 'https://vk.com' +URL = "https://vk.com" -if __name__ == '__main__': - app = QApplication(sys.argv) - view = QWebView() - view.show() +app = QApplication(sys.argv) - view.load(URL) +view = QWebView() +view.show() - loop = QEventLoop() - view.loadFinished.connect(loop.quit) - loop.exec_() +view.load(URL) - doc = view.page().mainFrame().documentElement() +loop = QEventLoop() +view.loadFinished.connect(loop.quit) +loop.exec_() - email = doc.findFirst('#quick_email') - password = doc.findFirst('#quick_pass') - login_button = doc.findFirst('#quick_login_button') +doc = view.page().mainFrame().documentElement() - if email.isNull() or password.isNull() or login_button.isNull(): - raise Exception('Ошибка при авторизации: не найдены поля емейла или пароля, или кнопка "Войти".') +email = doc.findFirst("#quick_email") +password = doc.findFirst("#quick_pass") +login_button = doc.findFirst("#quick_login_button") - # Заполняем поля емейла/телефона и пароля - email.setAttribute('value', LOGIN) - password.setAttribute('value', PASSWORD) +if email.isNull() or password.isNull() or login_button.isNull(): + raise Exception( + 'Ошибка при авторизации: не найдены поля емейла или пароля, или кнопка "Войти".' + ) - # Кликаем на кнопку "Войти" - login_button.evaluateJavaScript('this.click()') +# Заполняем поля емейла/телефона и пароля +email.setAttribute("value", LOGIN) +password.setAttribute("value", PASSWORD) - sys.exit(app.exec_()) +# Кликаем на кнопку "Войти" +login_button.evaluateJavaScript("this.click()") + +sys.exit(app.exec_()) diff --git a/magic_8_ball.py b/magic_8_ball.py deleted file mode 100644 index f3f91492d..000000000 --- a/magic_8_ball.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# SOURCE: https://stepik.org/lesson/349846/step/1?unit=333701 - - -import random - - -ANSWERS = [ - "Бесспорно", "Предрешено", "Никаких сомнений", "Определённо да", "Можешь быть уверен в этом", "Мне кажется - да", - "Вероятнее всего", "Хорошие перспективы", "Знаки говорят - да", "Да", "Пока неясно, попробуй снова", - "Спроси позже", "Лучше не рассказывать", "Сейчас нельзя предсказать", "Сконцентрируйся и спроси опять", - "Даже не думай", "Мой ответ - нет", "По моим данным - нет", "Перспективы не очень хорошие", "Весьма сомнительно" -] - - -print('Привет Мир, я магический шар, и я знаю ответ на любой твой вопрос.') - -name = input('Как тебя зовут?\n') -print(f'Привет, {name}!') - -while True: - input('Задай вопрос так, чтобы ответ был "да" или "нет":\n') - print(random.choice(ANSWERS)) - - result = input('Еще?\n') - if 'да' not in result.lower(): - break - -print('Возвращайся, если возникнут вопросы!') diff --git a/magtu_ru/mgtu_get_students.py b/magtu_ru/mgtu_get_students.py index b2cd556ae..428327d7a 100644 --- a/magtu_ru/mgtu_get_students.py +++ b/magtu_ru/mgtu_get_students.py @@ -1,27 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import requests -rs = requests.get('http://magtu.ru/modules/mod_reiting/mobile.php?action=get_all_department') + + +rs = requests.get( + "http://magtu.ru/modules/mod_reiting/mobile.php?action=get_all_department" +) for dep in rs.json(): id_dep = list(dep.keys())[0] name = dep[id_dep] print(id_dep, name) - rs = requests.get('http://magtu.ru/modules/mod_reiting/mobile.php?action=get_spec_by_depart&depart_kod=' + id_dep) + rs = requests.get( + f"http://magtu.ru/modules/mod_reiting/mobile.php?action=get_spec_by_depart&depart_kod={id_dep}" + ) for kaf in rs.json(): id_kaf = list(kaf.keys())[0] name = kaf[id_kaf] - print(' ' + id_kaf, name) + print(" " + id_kaf, name) - rs = requests.get('http://magtu.ru/modules/mod_reiting/mobile.php?action=get_reiting&spec_kod=' + id_kaf) + rs = requests.get( + f"http://magtu.ru/modules/mod_reiting/mobile.php?action=get_reiting&spec_kod={id_kaf}" + ) for i, stud in enumerate(rs.json(), 1): name = stud[0] - print(' {}. {}'.format(i, name)) + print(f" {i}. {name}") print() diff --git a/magtu_ru/mgtu_get_students_gui.py b/magtu_ru/mgtu_get_students_gui.py index 4c9383c69..ee290cb13 100644 --- a/magtu_ru/mgtu_get_students_gui.py +++ b/magtu_ru/mgtu_get_students_gui.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import requests - try: from PyQt5.QtGui import * from PyQt5.QtWidgets import * @@ -19,7 +18,7 @@ class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.lw_dep = QListWidget() @@ -37,9 +36,11 @@ def __init__(self): self.setLayout(layout) - def fill(self): + def fill(self) -> None: self.lw_dep.clear() - rs = requests.get('http://magtu.ru/modules/mod_reiting/mobile.php?action=get_all_department') + rs = requests.get( + "http://magtu.ru/modules/mod_reiting/mobile.php?action=get_all_department" + ) rs.raise_for_status() for dep in rs.json(): @@ -50,11 +51,13 @@ def fill(self): item.setData(Qt.UserRole, id_dep) self.lw_dep.addItem(item) - def fill_kaf(self, item_dep): + def fill_kaf(self, item_dep) -> None: self.lw_kaf.clear() id_dep = item_dep.data(Qt.UserRole) - rs = requests.get('http://magtu.ru/modules/mod_reiting/mobile.php?action=get_spec_by_depart&depart_kod=' + id_dep) + rs = requests.get( + f"http://magtu.ru/modules/mod_reiting/mobile.php?action=get_spec_by_depart&depart_kod={id_dep}" + ) rs.raise_for_status() for kaf in rs.json(): @@ -65,11 +68,13 @@ def fill_kaf(self, item_dep): item.setData(Qt.UserRole, id_kaf) self.lw_kaf.addItem(item) - def fill_stu(self, item_kaf): + def fill_stu(self, item_kaf) -> None: self.lw_stu.clear() id_kaf = item_kaf.data(Qt.UserRole) - rs = requests.get('http://magtu.ru/modules/mod_reiting/mobile.php?action=get_reiting&spec_kod=' + id_kaf) + rs = requests.get( + f"http://magtu.ru/modules/mod_reiting/mobile.php?action=get_reiting&spec_kod={id_kaf}" + ) rs.raise_for_status() for stud in rs.json(): diff --git a/magtu_ru/raspisanie-konsultatsij-prepodavatelej.py b/magtu_ru/raspisanie-konsultatsij-prepodavatelej.py index b104361d0..9440ad6e8 100644 --- a/magtu_ru/raspisanie-konsultatsij-prepodavatelej.py +++ b/magtu_ru/raspisanie-konsultatsij-prepodavatelej.py @@ -1,36 +1,36 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -url = 'http://www.magtu.ru/student/bakalavriat-spetsialitet-magistratura/raspisanie-konsultatsij-prepodavatelej.html' +from urllib.parse import urljoin import requests -rs = requests.get(url) - from bs4 import BeautifulSoup -root = BeautifulSoup(rs.content, 'html.parser') -from urllib.parse import urljoin -for tag in root.select('[itemprop=articleBody] > *'): +url = "http://www.magtu.ru/student/bakalavriat-spetsialitet-magistratura/raspisanie-konsultatsij-prepodavatelej.html" +rs = requests.get(url) + +root = BeautifulSoup(rs.content, "html.parser") +for tag in root.select("[itemprop=articleBody] > *"): name = tag.name - if name == 'h3': + if name == "h3": print(tag.text.upper()) continue - elif name == 'p': + elif name == "p": print(tag.text) continue - elif name == 'ul': - for li in tag.select('li'): + elif name == "ul": + for li in tag.select("li"): if li.a: - print(' "{}": {}'.format(li.a.text, urljoin(rs.url, li.a['href']))) + print(f' "{li.a.text}": {urljoin(rs.url, li.a["href"])}') else: - print(' "{}"'.format(li.text)) + print(f' "{li.text}"') print() continue diff --git a/magtu_ru/selection_of_specialty.py b/magtu_ru/selection_of_specialty.py index fdba66722..87273d5d1 100644 --- a/magtu_ru/selection_of_specialty.py +++ b/magtu_ru/selection_of_specialty.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт получает список специальностей в формате HTML, разбирает таблицу и оформляет ее в JSON""" @@ -15,30 +15,28 @@ def element_to_text_list(el) -> str: - return ', '.join([child.strip() for child in el.strings]) + return ", ".join([child.strip() for child in el.strings]) -url = 'http://magtu.ru/modules/mod_abiturient_helper/tmpl/get_spec.php' +url = "http://magtu.ru/modules/mod_abiturient_helper/tmpl/get_spec.php" headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:50.0) Gecko/20100101 Firefox/50.0', - 'X-Requested-With': 'XMLHttpRequest', + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:50.0) Gecko/20100101 Firefox/50.0", + "X-Requested-With": "XMLHttpRequest", } # Русский язык и Математика -post_data = { - 'data': '01,02' -} +post_data = {"data": "01,02"} rs = requests.post(url, headers=headers, data=post_data) if not rs.ok or not rs.text: - print('Post запрос не вернул данные таблицы. Возможно, не хватает каких-то данных.') + print("Post запрос не вернул данные таблицы. Возможно, не хватает каких-то данных.") sys.exit() -root = BeautifulSoup(rs.text, 'lxml') +root = BeautifulSoup(rs.text, "lxml") table_rows = [] -for tr in root.select('table tr')[1:]: - td_list = tr.select('td') +for tr in root.select("table tr")[1:]: + td_list = tr.select("td") row_data = { "number": element_to_text_list(td_list[0]), diff --git a/manual_auth_to_github.py b/manual_auth_to_github.py index a88180693..dae2744e0 100644 --- a/manual_auth_to_github.py +++ b/manual_auth_to_github.py @@ -1,42 +1,46 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Скрипт для авторизации на github вручную""" -LOGIN = '' -PASSWORD = '' +import requests +from lxml import etree + + +LOGIN = "" +PASSWORD = "" -import requests session = requests.Session() -session.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0' +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0" -rs = session.get('https://github.com') +rs = session.get("https://github.com") print(rs) -rs = session.get('https://github.com/login') +rs = session.get("https://github.com/login") print(rs) -from lxml import etree root = etree.HTML(rs.content) input_name_by_value = dict() -for input_tag in root.xpath('//input'): +for input_tag in root.xpath("//input"): try: - input_name_by_value[input_tag.attrib['name']] = input_tag.attrib['value'] + input_name_by_value[input_tag.attrib["name"]] = input_tag.attrib["value"] except KeyError: pass -input_name_by_value['login'] = LOGIN -input_name_by_value['password'] = PASSWORD +input_name_by_value["login"] = LOGIN +input_name_by_value["password"] = PASSWORD -rs = session.post('https://github.com/session', data=input_name_by_value) +rs = session.post("https://github.com/session", data=input_name_by_value) print(rs) -rs = session.get('https://github.com/gil9red/search_in_users_github_gists') +rs = session.get("https://github.com/gil9red/search_in_users_github_gists") print(rs) print(rs.text) diff --git a/markdown__examples/common.py b/markdown__examples/common.py new file mode 100644 index 000000000..ef293bab7 --- /dev/null +++ b/markdown__examples/common.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +EXAMPLE_MARKDOWN = """\ +https://ru.wikipedia.org/wiki/Markdown + +-------- + +*выделение* (например, курсив) + +**сильное выделение** (например, полужирное начертание) + +-------- + +Пример кода внутри строки (inline) `Hello world!` + + + + + + + + + + + +-------- + +необходимо сделать ~~одну~~ другую вещь + +-------- + +* элемент маркированного списка +- ещё один элемент ненумерованного списка ++ буллеты элементов могут быть разными + +1. Элемент нумерованного списка +2. Элемент №2 того же списка +9. Элемент №3 списка — элементы нумеруются по порядку, цифра в начале строки не имеет значения + +-------- + +# Заголовок первого уровня +... +### Заголовок третьего уровня +... +###### Заголовок шестого уровня +... + +-------- + +> Данный текст будет заключен в HTML-теги
    + +-------- + +[Текст ссылки](http://example.com/ "Необязательный заголовок ссылки") + +Где-то среди текста встречается [текст ссылки][example]. + +Также ссылка повторяется [пример адреса][example]. + +Ссылка на [второй][foo] также [Bar][] ресурсы. + +[example]: http://example.com/ "Необязательный заголовок ссылки" +[foo]: http://example.net/ 'Необязательный заголовок ссылки' +[bar]: http://example.edu/ (Необязательный заголовок ссылки) + +-------- + +![Alt-текст](http://example.com/ "Заголовок изображения") +""" diff --git a/markdown__examples/hello_world.py b/markdown__examples/hello_world.py new file mode 100644 index 000000000..42fe7a160 --- /dev/null +++ b/markdown__examples/hello_world.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install markdown +import markdown + + +text_markdown = """ +*Hello* **[World](https://ru.wikipedia.org/wiki/Hello,_world!)**! +""".strip() +text_html = markdown.markdown(text_markdown) +print(text_html) +#

    Hello World!

    diff --git a/markdown__examples/pyqt5_gui.py b/markdown__examples/pyqt5_gui.py new file mode 100644 index 000000000..237e163bc --- /dev/null +++ b/markdown__examples/pyqt5_gui.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +# pip install pyqt5 +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QTextEdit, + QLabel, + QSplitter, + QVBoxLayout, + QTabWidget, +) + +# pip install markdown +import markdown + +from common import EXAMPLE_MARKDOWN + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.setWindowTitle(Path(__file__).stem) + + self.edit_markdown = QTextEdit() + self.edit_markdown.textChanged.connect(self._on_input_text_markdown) + + self.edit_result_qt = QTextEdit() + self.edit_result_qt.setReadOnly(True) + + self.edit_result_markdown = QTextEdit() + self.edit_result_markdown.setReadOnly(True) + + left_side_layout = QVBoxLayout() + left_side_layout.addWidget(QLabel("Markdown:")) + left_side_layout.addWidget(self.edit_markdown) + + left_side = QWidget() + left_side.setLayout(left_side_layout) + + tab_result = QTabWidget() + tab_result.addTab(self.edit_result_qt, "Result (Qt)") + tab_result.addTab(self.edit_result_markdown, "Result (markdown)") + + splitter = QSplitter() + splitter.addWidget(left_side) + splitter.addWidget(tab_result) + + main_layout = QVBoxLayout() + main_layout.addWidget(splitter) + + self.setLayout(main_layout) + + def _on_input_text_markdown(self) -> None: + self.edit_result_qt.setMarkdown(self.edit_markdown.toPlainText()) + + html = markdown.markdown(self.edit_markdown.toPlainText()) + self.edit_result_markdown.setHtml(html) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + mw.edit_markdown.setPlainText(EXAMPLE_MARKDOWN) + + app.exec() diff --git a/markdown__examples/pyqt6_gui.py b/markdown__examples/pyqt6_gui.py new file mode 100644 index 000000000..cb029aef6 --- /dev/null +++ b/markdown__examples/pyqt6_gui.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +# pip install pyqt6 +from PyQt6.QtWidgets import ( + QApplication, + QWidget, + QTextEdit, + QLabel, + QSplitter, + QVBoxLayout, + QTabWidget, +) + +# pip install markdown +import markdown + +from common import EXAMPLE_MARKDOWN + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.setWindowTitle(Path(__file__).stem) + + self.edit_markdown = QTextEdit() + self.edit_markdown.textChanged.connect(self._on_input_text_markdown) + + self.edit_result_qt = QTextEdit() + self.edit_result_qt.setReadOnly(True) + + self.edit_result_markdown = QTextEdit() + self.edit_result_markdown.setReadOnly(True) + + left_side_layout = QVBoxLayout() + left_side_layout.addWidget(QLabel("Markdown:")) + left_side_layout.addWidget(self.edit_markdown) + + left_side = QWidget() + left_side.setLayout(left_side_layout) + + tab_result = QTabWidget() + tab_result.addTab(self.edit_result_qt, "Result (Qt)") + tab_result.addTab(self.edit_result_markdown, "Result (markdown)") + + splitter = QSplitter() + splitter.addWidget(left_side) + splitter.addWidget(tab_result) + + main_layout = QVBoxLayout() + main_layout.addWidget(splitter) + + self.setLayout(main_layout) + + def _on_input_text_markdown(self) -> None: + self.edit_result_qt.setMarkdown(self.edit_markdown.toPlainText()) + + html = markdown.markdown(self.edit_markdown.toPlainText()) + self.edit_result_markdown.setHtml(html) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow() + mw.show() + + mw.edit_markdown.setPlainText(EXAMPLE_MARKDOWN) + + app.exec() diff --git a/matplotlib__examples/animate.py b/matplotlib__examples/animate.py index 73430f1aa..6bfe34666 100644 --- a/matplotlib__examples/animate.py +++ b/matplotlib__examples/animate.py @@ -1,31 +1,34 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import psutil -from datetime import datetime as dt from collections import deque +from datetime import datetime as dt + +import matplotlib.animation as animation import matplotlib.pyplot as plt from matplotlib.dates import date2num -import matplotlib.animation as animation + +import psutil + N = 600 x = deque([date2num(dt.now())], maxlen=N) y = deque([0], maxlen=N) fig, ax = plt.subplots(figsize=(8, 3)) -line, = ax.plot_date(x, y, marker="") +(line,) = ax.plot_date(x, y, marker="") -ax.spines['left'].set_visible(False) -ax.spines['bottom'].set_visible(False) -ax.spines['top'].set_visible(False) -ax.spines['right'].set_visible(False) +ax.spines["left"].set_visible(False) +ax.spines["bottom"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.spines["right"].set_visible(False) def get_data(): - return psutil.cpu_percent(.15) + return psutil.cpu_percent(0.15) def animate(i): @@ -34,8 +37,8 @@ def animate(i): ax.relim(visible_only=True) ax.autoscale_view(True) line.set_data(x, y) - ax.fill_between(x, -0.5, y, color='lightgrey') - return line, + ax.fill_between(x, -0.5, y, color="lightgrey") + return (line,) ani = animation.FuncAnimation(fig, animate, interval=300) diff --git a/matplotlib__examples/examples/exm1.py b/matplotlib__examples/examples/exm1.py index 59655c5ea..fb02671fd 100644 --- a/matplotlib__examples/examples/exm1.py +++ b/matplotlib__examples/examples/exm1.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Следующий пример строит график функции f(x) = x / sin(x): @@ -11,7 +11,7 @@ # !!! Импортируем пакет со вспомогательными функциями from matplotlib__examples import mlab -if __name__ == '__main__': +if __name__ == "__main__": # Будем рисовать график этой функции def func(x): """ @@ -19,7 +19,7 @@ def func(x): """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x # Интервал изменения переменной по оси X xmin = -20.0 @@ -29,13 +29,13 @@ def func(x): dx = 0.01 # !!! Создадим список координат по оси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist = [func (x) for x in xlist] + ylist = [func(x) for x in xlist] # !!! Нарисуем одномерный график - pylab.plot (xlist, ylist) + pylab.plot(xlist, ylist) # !!! Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm10.py b/matplotlib__examples/examples/exm10.py index b3b3df635..c3650f05e 100644 --- a/matplotlib__examples/examples/exm10.py +++ b/matplotlib__examples/examples/exm10.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Все классы для работы с трехмерными графиками находятся в пакете mpl_toolkits.mplot3d, @@ -14,8 +14,8 @@ import pylab from mpl_toolkits.mplot3d import Axes3D -if __name__ == '__main__': +if __name__ == "__main__": fig = pylab.figure() Axes3D(fig) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm11.py b/matplotlib__examples/examples/exm11.py index 8ed3ec829..e57d4b340 100644 --- a/matplotlib__examples/examples/exm11.py +++ b/matplotlib__examples/examples/exm11.py @@ -1,23 +1,23 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" +import numpy import pylab from mpl_toolkits.mplot3d import Axes3D -import numpy # Эта функция возвращает три двумерные матрицы: x, y, z. # Координаты x и y лежат в интервале от -10 до 10 с шагом 0.1. -def makeData (): - x = numpy.arange (-10, 10, 0.1) - y = numpy.arange (-10, 10, 0.1) +def makeData(): + x = numpy.arange(-10, 10, 0.1) + y = numpy.arange(-10, 10, 0.1) xgrid, ygrid = numpy.meshgrid(x, y) - zgrid = numpy.sin (xgrid) * numpy.sin (ygrid) / (xgrid * ygrid) + zgrid = numpy.sin(xgrid) * numpy.sin(ygrid) / (xgrid * ygrid) return xgrid, ygrid, zgrid -if __name__ == '__main__': +if __name__ == "__main__": x, y, z = makeData() # Чтобы отобразить наши данные, достаточно вызвать метод plot_surface() @@ -31,4 +31,4 @@ def makeData (): # axes.plot_surface(x, y, z, rstride=5, cstride=5) # Установка шага сетки (при 5, очень мелкая сетка) # axes.plot_surface(x, y, z, rstride=20, cstride=20) # Установка шага сетки (при 20, крупная сетка) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm12.py b/matplotlib__examples/examples/exm12.py index c93d45a38..b16136963 100644 --- a/matplotlib__examples/examples/exm12.py +++ b/matplotlib__examples/examples/exm12.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Цветовые карты используются, если нужно указать в какие цвета должны окрашиваться @@ -11,23 +11,25 @@ # чтобы создать градиент перехода от синего цвета к красному через белый. +import numpy import pylab + from mpl_toolkits.mplot3d import Axes3D from matplotlib__examples.colors import LinearSegmentedColormap -import numpy + # Эта функция возвращает три двумерные матрицы: x, y, z. # Координаты x и y лежат в интервале от -10 до 10 с шагом 0.1. -def makeData (): - x = numpy.arange (-10, 10, 0.1) - y = numpy.arange (-10, 10, 0.1) +def makeData(): + x = numpy.arange(-10, 10, 0.1) + y = numpy.arange(-10, 10, 0.1) xgrid, ygrid = numpy.meshgrid(x, y) - zgrid = numpy.sin (xgrid) * numpy.sin (ygrid) / (xgrid * ygrid) + zgrid = numpy.sin(xgrid) * numpy.sin(ygrid) / (xgrid * ygrid) return xgrid, ygrid, zgrid -if __name__ == '__main__': +if __name__ == "__main__": x, y, z = makeData() # Чтобы отобразить наши данные, достаточно вызвать метод plot_surface() @@ -54,7 +56,7 @@ def makeData (): # color_map = cm.cmap_d["jet"] # axes.plot_surface(x, y, z, rstride=4, cstride=4, cmap=color_map) - color_map = LinearSegmentedColormap.from_list("red_blue", ['b', 'w', 'r'], 256) + color_map = LinearSegmentedColormap.from_list("red_blue", ["b", "w", "r"], 256) axes.plot_surface(x, y, z, rstride=3, cstride=3, cmap=color_map) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm13.py b/matplotlib__examples/examples/exm13.py index 2d410e818..50768bc89 100644 --- a/matplotlib__examples/examples/exm13.py +++ b/matplotlib__examples/examples/exm13.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" import math @@ -9,6 +9,7 @@ # !!! Импортируем пакет со вспомогательными функциями from matplotlib__examples import mlab + # Будем рисовать график этой функции def func(x): """ @@ -19,7 +20,7 @@ def func(x): return math.sin(x) / x -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -20.0 xmax = 20.0 @@ -28,10 +29,10 @@ def func(x): dx = 0.2 # !!! Создадим список координат по оси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist = [func (x) for x in xlist] + ylist = [func(x) for x in xlist] # !!! Нарисуем одномерный график с использованием стиля pylab.plot(xlist, ylist, "x") @@ -50,4 +51,4 @@ def func(x): # pylab.plot(xlist, ylist, "-*k") # !!! Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm14.py b/matplotlib__examples/examples/exm14.py index 25fe00d23..ad26cf7a4 100644 --- a/matplotlib__examples/examples/exm14.py +++ b/matplotlib__examples/examples/exm14.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" import math @@ -9,6 +9,7 @@ # !!! Импортируем пакет со вспомогательными функциями from matplotlib__examples import mlab + # Будем рисовать график этой функции def func(x): """ @@ -19,7 +20,7 @@ def func(x): return math.sin(x) / x -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -20.0 xmax = 20.0 @@ -28,10 +29,10 @@ def func(x): dx = 0.2 # !!! Создадим список координат по оси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist = [func (x) for x in xlist] + ylist = [func(x) for x in xlist] # # !!! Нарисуем одномерный график с использованием стиля # pylab.plot(xlist, ylist, "x") @@ -58,8 +59,9 @@ def func(x): # pylab.plot (xlist, ylist, linestyle = "-", marker = "*", color = "k") # Результат будет выглядеть точно также. А, например, следующий код - pylab.plot(xlist, ylist, linestyle="-", marker="o", color="k", - markerfacecolor="#ff22aa") + pylab.plot( + xlist, ylist, linestyle="-", marker="o", color="k", markerfacecolor="#ff22aa" + ) # !!! Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm15.py b/matplotlib__examples/examples/exm15.py index 3479aff2b..7f45e86fc 100644 --- a/matplotlib__examples/examples/exm15.py +++ b/matplotlib__examples/examples/exm15.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Использование библиотеки Matplotlib. Как отобразить легенду @@ -14,17 +14,18 @@ # Импортируем пакет со вспомогательными функциями from matplotlib__examples import mlab + # Будем рисовать график этой функции -def func (x): +def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -20.0 xmax = 20.0 @@ -33,12 +34,11 @@ def func (x): dx = 0.01 # Создадим список координат по оси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist1 = [func (x) for x in xlist] - ylist2 = [func (x * 0.2) for x in xlist] - + ylist1 = [func(x) for x in xlist] + ylist2 = [func(x * 0.2) for x in xlist] ## Первый способ # В качестве параметра функции legend() нужно передать список или кортеж, @@ -49,16 +49,15 @@ def func (x): # виде легенды. # Нарисуем два одномерных графика - pylab.plot (xlist, ylist1, "b-") - pylab.plot (xlist, ylist2, "g--") + pylab.plot(xlist, ylist1, "b-") + pylab.plot(xlist, ylist2, "g--") # !!! Добавим легенду. # !!! Первому графику будет соответствовать надпись "f(x)", # !!! А второму - "f(0.2 * x)" - pylab.legend ( ("f(x)", "f(0.2 * x)") ) + pylab.legend(("f(x)", "f(0.2 * x)")) ## Первый способ - ## Второй способ # Того же самого результата мы можем добиться, если при рисовании графиков # будем использовать дополнительный параметр label, а затем вызовем функцию @@ -73,7 +72,6 @@ def func (x): # метка. ## Второй способ - ## Заголовок легенды # В легенду можно добавить заголовок, для этого в функцию legend() надо # передать дополнительный строковый параметр title со строкой заголовка. @@ -81,6 +79,5 @@ def func (x): # pylab.legend (title = "Sinc") ## Заголовок легенды - # Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm16.py b/matplotlib__examples/examples/exm16.py index 884b653d6..896a804b7 100644 --- a/matplotlib__examples/examples/exm16.py +++ b/matplotlib__examples/examples/exm16.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Использование библиотеки Matplotlib. Как отображать формулы в нотации TeX @@ -20,17 +20,18 @@ # Импортируем пакет со вспомогательными функциями from matplotlib__examples import mlab + # Будем рисовать график этой функции -def func (x): +def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -20.0 xmax = 20.0 @@ -39,18 +40,18 @@ def func (x): dx = 0.01 # Создадим список координат по оиси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist1 = [func (x) for x in xlist] - ylist2 = [func (x * 0.2) for x in xlist] + ylist1 = [func(x) for x in xlist] + ylist2 = [func(x * 0.2) for x in xlist] # !!! Графики имеют метки с формулами в формате TeX - pylab.plot (xlist, ylist1, "b-", label = "$f(x)$") - pylab.plot (xlist, ylist2, "g--", label = "$f(x \cdot 0.2)$") + pylab.plot(xlist, ylist1, "b-", label="$f(x)$") + pylab.plot(xlist, ylist2, "g--", label="$f(x \cdot 0.2)$") # !!! Добавим легенду с заголовком в виде формулы - pylab.legend (title = r"$f(x) = \frac{sin(x)}{x}$") + pylab.legend(title=r"$f(x) = \frac{sin(x)}{x}$") # Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm17.py b/matplotlib__examples/examples/exm17.py index a31cf5c3e..9d943598b 100644 --- a/matplotlib__examples/examples/exm17.py +++ b/matplotlib__examples/examples/exm17.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## Использование библиотеки Matplotlib. Как нарисовать несколько графиков в одном окне @@ -26,16 +26,16 @@ # Будем рисовать график этой функции -def func (x): +def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -20.0 xmax = 20.0 @@ -44,40 +44,40 @@ def func (x): dx = 0.01 # Создадим список координат по оиси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist = [func (x) for x in xlist] + ylist = [func(x) for x in xlist] # !!! Две строки, три столбца. # !!! Текущая ячейка - 1 - pylab.subplot (2, 3, 1) - pylab.plot (xlist, ylist) - pylab.title ("1") + pylab.subplot(2, 3, 1) + pylab.plot(xlist, ylist) + pylab.title("1") # !!! Две строки, три столбца. # !!! Текущая ячейка - 2 - pylab.subplot (2, 3, 2) - pylab.plot (xlist, ylist) - pylab.title ("2") + pylab.subplot(2, 3, 2) + pylab.plot(xlist, ylist) + pylab.title("2") # !!! Две строки, три столбца. # !!! Текущая ячейка - 4 - pylab.subplot (2, 3, 4) - pylab.plot (xlist, ylist) - pylab.title ("4") + pylab.subplot(2, 3, 4) + pylab.plot(xlist, ylist) + pylab.title("4") # !!! Две строки, три столбца. # !!! Текущая ячейка - 5 - pylab.subplot (2, 3, 5) - pylab.plot (xlist, ylist) - pylab.title ("5") + pylab.subplot(2, 3, 5) + pylab.plot(xlist, ylist) + pylab.title("5") # !!! Одна строка, три столбца. # !!! Текущая ячейка - 3 - pylab.subplot (1, 3, 3) - pylab.plot (xlist, ylist) - pylab.title ("3") + pylab.subplot(1, 3, 3) + pylab.plot(xlist, ylist) + pylab.title("3") # Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm18.py b/matplotlib__examples/examples/exm18.py index a98cd407e..7951b3c31 100644 --- a/matplotlib__examples/examples/exm18.py +++ b/matplotlib__examples/examples/exm18.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## Использование библиотеки Matplotlib. Более гибкий способ расположения графиков с @@ -22,33 +22,34 @@ # Импортируем один из пакетов Matplotlib import pylab -if __name__ == '__main__': + +if __name__ == "__main__": # Таблица графиков будет иметь три строки и три столбца (3,3) # Вывод будет осуществляться в ячейку с координатами (0, 0), # то есть 0-ая строка и 0-ой столбец # Оси для графика будут занимать две ячейки по горизонтали (colspan = 2) # и две ячейки по вертикали (rowspan = 2) - pylab.subplot2grid ((3,3), (0, 0), colspan = 2, rowspan = 2) - pylab.title ("Graph1") + pylab.subplot2grid((3, 3), (0, 0), colspan=2, rowspan=2) + pylab.title("Graph1") # Вывод будет осуществляться в ячейку с координатами (0, 2), # то есть 0-ая строка и 2-ой столбец (нумерация начинается с нуля) # Оси для графика будут занимать две ячейки по вертикали (rowspan = 2) - pylab.subplot2grid ((3,3), (0, 2), rowspan = 2) - pylab.title ("Graph2") + pylab.subplot2grid((3, 3), (0, 2), rowspan=2) + pylab.title("Graph2") # Вывод будет осуществляться в ячейку с координатами (2, 0), # то есть 2-ая строка и 0-ой столбец # Оси для графика будут занимать одну ячейку по вертикали и горизонтали # Аналог этого вызова - pylab.subplot (3, 3, 7) - pylab.subplot2grid ((3,3), (2, 0)) - pylab.title ("Graph3") + pylab.subplot2grid((3, 3), (2, 0)) + pylab.title("Graph3") # Вывод будет осуществляться в ячейку с координатами (2, 1), # то есть 2-ая строка и 1-ый столбец # Оси для графика будут занимать две ячейки по горизонтали (colspan = 2) - pylab.subplot2grid ((3,3), (2, 1), colspan = 2) - pylab.title ("Graph4") + pylab.subplot2grid((3, 3), (2, 1), colspan=2) + pylab.title("Graph4") - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm19.py b/matplotlib__examples/examples/exm19.py index 934118994..a235e2ab9 100644 --- a/matplotlib__examples/examples/exm19.py +++ b/matplotlib__examples/examples/exm19.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## Использование библиотеки Matplotlib. Использование класса GridSpec для расположения @@ -20,18 +20,19 @@ import math -import pylab import matplotlib__examples +import pylab + -def plotGraph (): +def plotGraph() -> None: # Будем рисовать график этой функции - def func (x): + def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x # Интервал изменения переменной по оси X xmin = -20.0 @@ -41,76 +42,76 @@ def func (x): dx = 0.01 # Создадим список координат по оси X на отрезке [-xmin; xmax], включая концы - xlist = matplotlib__examples.mlab.frange (xmin, xmax, dx) + xlist = matplotlib__examples.mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist = [func (x) for x in xlist] + ylist = [func(x) for x in xlist] - pylab.plot (xlist, ylist) + pylab.plot(xlist, ylist) -if __name__ == '__main__': +if __name__ == "__main__": # создаем таблицу (сетку) размером 4 x 4 ячеек grid = matplotlib__examples.gridspec.GridSpec(4, 4) # Одномерное индексирование. # 0-ая ячейка, начиная с левого верхнего угла # График занимает одну ячейку - pylab.subplot (grid[0]) - pylab.title ("Graph 1") + pylab.subplot(grid[0]) + pylab.title("Graph 1") plotGraph() # Одномерное индексирование. # 1-ая ячейка, начиная с левого верхнего угла (0-ая строка, 1-ый столбец) # График занимает одну ячейку - pylab.subplot (grid[1]) - pylab.title ("Graph 2") + pylab.subplot(grid[1]) + pylab.title("Graph 2") plotGraph() # Двумерное индексирование. # 1-ая строка, 0-ой столбец # График занимает одну ячейку - pylab.subplot (grid [1, 0]) - pylab.title ("Graph 3") + pylab.subplot(grid[1, 0]) + pylab.title("Graph 3") plotGraph() # Двумерное индексирование. # 0-ая строка. График занимает столбцы от 2 и до конца строки (2:) # График занимает одну строку и два столбца (2-й и 3-й) - pylab.subplot (grid [0, 2:]) - pylab.title ("Graph 4") + pylab.subplot(grid[0, 2:]) + pylab.title("Graph 4") plotGraph() # Двумерное индексирование. # График занимает строки, начиная с 1 и до 2 включительно (1: 3) # График занимает столбцы от 1 и до предпоследней ячейки включительно (1: -1) # График занимает две строки и два столбца - pylab.subplot (grid [1: 3, 1: -1]) - pylab.title ("Graph 5") + pylab.subplot(grid[1:3, 1:-1]) + pylab.title("Graph 5") plotGraph() # Двумерное индексирование. # График занимает строки, начиная со 2 и до конца всех строк (столбца) (2: ) # График занимает один столбец по горизонтали # График занимает две строки и один столбец - pylab.subplot (grid [2:, 0]) - pylab.title ("Graph 6") + pylab.subplot(grid[2:, 0]) + pylab.title("Graph 6") plotGraph() # Двумерное индексирование. # График занимает строки, начиная с 1 и до последней строки включительно (1:) # График занимает один последний столбец (-1) # График занимает две строки и один столбец - pylab.subplot (grid [1:, -1]) - pylab.title ("Graph 7") + pylab.subplot(grid[1:, -1]) + pylab.title("Graph 7") plotGraph() # Двумерное индексирование. # График занимает одну последнюю строку (-1) # График занимает два столбца: 1 и 2 (1: 3) # График занимает одну строку и два столбца - pylab.subplot (grid [-1, 1: 3]) - pylab.title ("Graph 8") + pylab.subplot(grid[-1, 1:3]) + pylab.title("Graph 8") plotGraph() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm2.py b/matplotlib__examples/examples/exm2.py index d390fdd40..dd41235f7 100644 --- a/matplotlib__examples/examples/exm2.py +++ b/matplotlib__examples/examples/exm2.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Если вызывать функцию plot() несколько раз подряд, то на график @@ -12,15 +12,15 @@ # !!! Импортируем пакет со вспомогательными функциями from matplotlib__examples import mlab -if __name__ == '__main__': +if __name__ == "__main__": # Будем рисовать график этой функции - def func (x): + def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x # Интервал изменения переменной по оси X xmin = -20.0 @@ -30,15 +30,15 @@ def func (x): dx = 0.01 # !!! Создадим список координат по оиси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist1 = [func (x) for x in xlist] - ylist2 = [func (x * 0.2) for x in xlist] + ylist1 = [func(x) for x in xlist] + ylist2 = [func(x * 0.2) for x in xlist] # !!! Нарисуем одномерные графики - pylab.plot (xlist, ylist1) - pylab.plot (xlist, ylist2) + pylab.plot(xlist, ylist1) + pylab.plot(xlist, ylist2) # !!! Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm20.py b/matplotlib__examples/examples/exm20.py index 789d67f36..3905c58e2 100644 --- a/matplotlib__examples/examples/exm20.py +++ b/matplotlib__examples/examples/exm20.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Использование библиотеки Matplotlib. Как изменять размеры ячеек таблицы при @@ -14,18 +14,19 @@ import math import pylab + import matplotlib__examples -def plotGraph (): +def plotGraph() -> None: # Будем рисовать график этой функции - def func (x): + def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x # Интервал изменения переменной по оси X xmin = -20.0 @@ -35,15 +36,15 @@ def func (x): dx = 0.01 # Создадим список координат по оси X на отрезке [-xmin; xmax], включая концы - xlist = matplotlib__examples.mlab.frange (xmin, xmax, dx) + xlist = matplotlib__examples.mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist = [func (x) for x in xlist] + ylist = [func(x) for x in xlist] - pylab.plot (xlist, ylist) + pylab.plot(xlist, ylist) -if __name__ == '__main__': +if __name__ == "__main__": # Создаем таблицу (сетку) с тремя строками и двумя столбцами rows = 3 cols = 2 @@ -54,13 +55,12 @@ def func (x): # height_ratios - список соотношений высот ячеек # В данном случае высота второй строки будет в 2 раза больше первого, # а высота третьей строки будет в 3 раза больше первой (в 1.5 раза больше второй) - grid = matplotlib__examples.gridspec.GridSpec(rows, - cols, - width_ratios=[1, 1.5], - height_ratios=[1, 2, 3]) + grid = matplotlib__examples.gridspec.GridSpec( + rows, cols, width_ratios=[1, 1.5], height_ratios=[1, 2, 3] + ) - for n in range (rows * cols): - pylab.subplot (grid[n]) + for n in range(rows * cols): + pylab.subplot(grid[n]) plotGraph() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm21.py b/matplotlib__examples/examples/exm21.py index 864596c1f..a8cef33cc 100644 --- a/matplotlib__examples/examples/exm21.py +++ b/matplotlib__examples/examples/exm21.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## Использование библиотеки Matplotlib. Как рисовать графики в разных окнах @@ -25,16 +25,16 @@ # Будем рисовать график этой функции -def func (x): +def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -20.0 xmax = 20.0 @@ -43,26 +43,26 @@ def func (x): dx = 0.01 # Создадим список координат по оиси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist1 = [func (x) for x in xlist] - ylist2 = [func (x * 0.2) for x in xlist] - ylist3 = [func (x * 2) for x in xlist] + ylist1 = [func(x) for x in xlist] + ylist2 = [func(x * 0.2) for x in xlist] + ylist3 = [func(x * 2) for x in xlist] # !!! Нарисуем график в первом окне - pylab.figure (1) - pylab.plot (xlist, ylist1, label = "f(x)") + pylab.figure(1) + pylab.plot(xlist, ylist1, label="f(x)") # !!! Нарисуем график во втором окне pylab.figure(2) - pylab.plot (xlist, ylist2, label = "f(x * 0.2)") + pylab.plot(xlist, ylist2, label="f(x * 0.2)") pylab.legend() # !!! Нарисуем еще один график в первом окне - pylab.figure (1) - pylab.plot (xlist, ylist3, label = "f(x * 2)") + pylab.figure(1) + pylab.plot(xlist, ylist3, label="f(x * 2)") pylab.legend() # Покажем окна с нарисованными графиками - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm22.py b/matplotlib__examples/examples/exm22.py index 3963a27aa..6c2af83ff 100644 --- a/matplotlib__examples/examples/exm22.py +++ b/matplotlib__examples/examples/exm22.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## Использование библиотеки Matplotlib. Как делать анимированные графики @@ -24,7 +24,7 @@ from matplotlib__examples import mlab -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -20.0 xmax = 20.0 @@ -33,25 +33,25 @@ dx = 0.01 # Создадим список координат по оиси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # !!! Включаем интерактивный режим pylab.ion() # У нас будет 50 кадров - for n in range (50): + for n in range(50): # Данные для очередного кадра - ylist = [math.sin (x + n / 2.0) for x in xlist] + ylist = [math.sin(x + n / 2.0) for x in xlist] # !!! Очистим график pylab.clf() # Выведем новые данные - pylab.plot (xlist, ylist) + pylab.plot(xlist, ylist) # !!! Нарисуем их # !!! Обратите внимание, что здесь используется функция draw(), а не show() pylab.draw() # !!! Закроем окно, иначе при завершении программы получим ошибку - pylab.close() \ No newline at end of file + pylab.close() diff --git a/matplotlib__examples/examples/exm23_1.py b/matplotlib__examples/examples/exm23_1.py index 4f4844aaf..9f452103f 100644 --- a/matplotlib__examples/examples/exm23_1.py +++ b/matplotlib__examples/examples/exm23_1.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## Использование библиотеки Matplotlib. Как изменять формат меток на осях @@ -19,13 +19,13 @@ if __name__ == "__main__": - xvals = numpy.arange (-10.0, 10.1, 0.1) - yvals = numpy.sinc (xvals) + xvals = numpy.arange(-10.0, 10.1, 0.1) + yvals = numpy.sinc(xvals) figure = pylab.figure() - axes = figure.add_subplot (1, 1, 1) + axes = figure.add_subplot(1, 1, 1) - pylab.plot (xvals, yvals) + pylab.plot(xvals, yvals) axes.grid() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm23_2.py b/matplotlib__examples/examples/exm23_2.py index f44344052..c4118d280 100644 --- a/matplotlib__examples/examples/exm23_2.py +++ b/matplotlib__examples/examples/exm23_2.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Теперь рассмотрим, как можно менять форматирование с помощью метода @@ -21,19 +21,19 @@ if __name__ == "__main__": - xvals = numpy.arange (-10.0, 10.1, 0.1) - yvals = numpy.sinc (xvals) + xvals = numpy.arange(-10.0, 10.1, 0.1) + yvals = numpy.sinc(xvals) figure = pylab.figure() - axes = figure.add_subplot (1, 1, 1) + axes = figure.add_subplot(1, 1, 1) # Создаем форматер formatter = matplotlib__examples.ticker.NullFormatter() # Установка форматера для оси X - axes.xaxis.set_major_formatter (formatter) + axes.xaxis.set_major_formatter(formatter) - pylab.plot (xvals, yvals) + pylab.plot(xvals, yvals) axes.grid() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm23_3.py b/matplotlib__examples/examples/exm23_3.py index d4f21431c..4b6da443b 100644 --- a/matplotlib__examples/examples/exm23_3.py +++ b/matplotlib__examples/examples/exm23_3.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## FormatStrFormatter @@ -27,20 +27,20 @@ if __name__ == "__main__": - xvals = numpy.arange (-10.0, 10.1, 0.1) - yvals = numpy.sinc (xvals) + xvals = numpy.arange(-10.0, 10.1, 0.1) + yvals = numpy.sinc(xvals) - pylab.rc('font',**{'family':'verdana'}) + pylab.rc("font", **{"family": "verdana"}) figure = pylab.figure() - axes = figure.add_subplot (1, 1, 1) + axes = figure.add_subplot(1, 1, 1) # Создаем форматер - formatter = matplotlib__examples.ticker.FormatStrFormatter ("%.3f") + formatter = matplotlib__examples.ticker.FormatStrFormatter("%.3f") # Установка форматера для оси X - axes.xaxis.set_major_formatter (formatter) + axes.xaxis.set_major_formatter(formatter) - pylab.plot (xvals, yvals) + pylab.plot(xvals, yvals) axes.grid() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm23_4.py b/matplotlib__examples/examples/exm23_4.py index a0f200db6..034afcaa3 100644 --- a/matplotlib__examples/examples/exm23_4.py +++ b/matplotlib__examples/examples/exm23_4.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## FuncFormatter @@ -28,31 +28,31 @@ import matplotlib__examples.ticker -def funcForFormatter (x, pos): +def funcForFormatter(x, pos) -> str: if x < 0: - return "минус {x}".format (x=abs(x)) + return f"минус {abs(x)}" if x > 0: - return "плюс {x}".format (x=x) + return f"плюс {x}" return "0" if __name__ == "__main__": - xvals = numpy.arange (-10.0, 10.1, 0.1) - yvals = numpy.sinc (xvals) + xvals = numpy.arange(-10.0, 10.1, 0.1) + yvals = numpy.sinc(xvals) - pylab.rc('font',**{'family':'verdana'}) + pylab.rc("font", **{"family": "verdana"}) figure = pylab.figure() - axes = figure.add_subplot (1, 1, 1) + axes = figure.add_subplot(1, 1, 1) # Создаем форматер formatter = matplotlib__examples.ticker.FuncFormatter(funcForFormatter) # Установка форматера для оси X - axes.xaxis.set_major_formatter (formatter) + axes.xaxis.set_major_formatter(formatter) - pylab.plot (xvals, yvals) + pylab.plot(xvals, yvals) axes.grid() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm23_5.py b/matplotlib__examples/examples/exm23_5.py index ec5e58528..b056caea7 100644 --- a/matplotlib__examples/examples/exm23_5.py +++ b/matplotlib__examples/examples/exm23_5.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## ScalarFormatter @@ -14,17 +14,16 @@ import numpy import pylab -import matplotlib__examples.ticker if __name__ == "__main__": - xvals = numpy.arange (-10.0, 10.1, 0.1) - yvals = numpy.sinc (xvals) * 1e5 + xvals = numpy.arange(-10.0, 10.1, 0.1) + yvals = numpy.sinc(xvals) * 1e5 figure = pylab.figure() - axes = figure.add_subplot (1, 1, 1) + axes = figure.add_subplot(1, 1, 1) - pylab.plot (xvals, yvals) + pylab.plot(xvals, yvals) axes.grid() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm23_6.py b/matplotlib__examples/examples/exm23_6.py index 6e70a96c3..82a30583e 100644 --- a/matplotlib__examples/examples/exm23_6.py +++ b/matplotlib__examples/examples/exm23_6.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## FixedFormatter @@ -22,20 +22,22 @@ if __name__ == "__main__": - xvals = numpy.arange (-10.0, 10.1, 0.1) - yvals = numpy.sinc (xvals) + xvals = numpy.arange(-10.0, 10.1, 0.1) + yvals = numpy.sinc(xvals) - pylab.rc('font',**{'family':'verdana'}) + pylab.rc("font", **{"family": "verdana"}) figure = pylab.figure() - axes = figure.add_subplot (1, 1, 1) + axes = figure.add_subplot(1, 1, 1) # Создаем форматер - formatter = matplotlib__examples.ticker.FixedFormatter([u"Раз", u"Два", u"Три", u"Четыре", u"Пять"]) + formatter = matplotlib__examples.ticker.FixedFormatter( + ["Раз", "Два", "Три", "Четыре", "Пять"] + ) # Установка форматера для оси X - axes.xaxis.set_major_formatter (formatter) + axes.xaxis.set_major_formatter(formatter) - pylab.plot (xvals, yvals) + pylab.plot(xvals, yvals) axes.grid() - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm24.py b/matplotlib__examples/examples/exm24.py index af033b352..91b453301 100644 --- a/matplotlib__examples/examples/exm24.py +++ b/matplotlib__examples/examples/exm24.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" ## Использование библиотеки Matplotlib. Как изменять интервал осей @@ -20,16 +20,16 @@ # Будем рисовать график этой функции -def func (x): +def func(x): """ sinc (x) """ if x == 0: return 1.0 - return math.sin (x) / x + return math.sin(x) / x -if __name__ == '__main__': +if __name__ == "__main__": # Интервал изменения переменной по оси X xmin = -50.0 xmax = 50.0 @@ -38,16 +38,16 @@ def func (x): dx = 0.2 # !!! Создадим список координат по оси X на отрезке [-xmin; xmax], включая концы - xlist = mlab.frange (xmin, xmax, dx) + xlist = mlab.frange(xmin, xmax, dx) # Вычислим значение функции в заданных точках - ylist = [func (x) for x in xlist] + ylist = [func(x) for x in xlist] # !!! Нарисуем одномерный график с использованием стиля - pylab.plot (xlist, ylist) + pylab.plot(xlist, ylist) # !!! На графике будет показан только участок от -10 до 30 по оси X - pylab.xlim (-10, 30) + pylab.xlim(-10, 30) # !!! Покажем окно с нарисованным графиком - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm3.py b/matplotlib__examples/examples/exm3.py index 02636ce3a..e0c25af85 100644 --- a/matplotlib__examples/examples/exm3.py +++ b/matplotlib__examples/examples/exm3.py @@ -1,11 +1,11 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Простой пример рисования графика import pylab -if __name__ == '__main__': - pylab.plot(range(1, 20), [i * i for i in range(1, 20)], 'ro') +if __name__ == "__main__": + pylab.plot(range(1, 20), [i * i for i in range(1, 20)], "ro") # pylab.savefig('example.png') - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm4.py b/matplotlib__examples/examples/exm4.py index ee5e37a51..54ecf0a23 100644 --- a/matplotlib__examples/examples/exm4.py +++ b/matplotlib__examples/examples/exm4.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Для рисования кругового графика используется функция pie() из модуля pylab. @@ -8,11 +8,11 @@ import pylab -if __name__ == '__main__': +if __name__ == "__main__": # Данные для построения графика data = [20.0, 10.0, 5.0, 1.0, 0.5] # Нарисовать круговой график - pylab.pie (data) + pylab.pie(data) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm5.py b/matplotlib__examples/examples/exm5.py index fbff62293..401d4771d 100644 --- a/matplotlib__examples/examples/exm5.py +++ b/matplotlib__examples/examples/exm5.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # По умолчанию создается окно, у которого ширина и высота различны, из-за чего "пирог" @@ -11,17 +11,17 @@ import pylab -if __name__ == '__main__': +if __name__ == "__main__": # Данные для построения графика data = [20.0, 10.0, 5.0, 1.0, 0.5] # Создать новое окно (фигуру) с одинаковыми размерами сторон (6 x 6 дюйма) - pylab.figure(figsize=(6, 6) ) + pylab.figure(figsize=(6, 6)) # Установим размеры осей по горизонтали и вертикали тоже одинаковыми pylab.axes([0.0, 0.0, 1.0, 1.0]) # И снова нарисуем график - pylab.pie (data) + pylab.pie(data) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm6.py b/matplotlib__examples/examples/exm6.py index 5eadccc24..1ba8f4ca6 100644 --- a/matplotlib__examples/examples/exm6.py +++ b/matplotlib__examples/examples/exm6.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Для того, чтобы добавить текстовые метки к секторам, достаточно передать их список @@ -6,7 +6,7 @@ import pylab -if __name__ == '__main__': +if __name__ == "__main__": # Данные для построения графика data = [20.0, 10.0, 5.0, 1.0, 0.5] @@ -14,12 +14,12 @@ labels = ["data1", "data2", "data3", "data4", "data5"] # Создать новое окно (фигуру) с одинаковыми размерами сторон (6 x 6 дюйма) - pylab.figure(figsize=(6, 6) ) + pylab.figure(figsize=(6, 6)) # Установим размеры осей по горизонтали и вертикали тоже одинаковыми pylab.axes([0.05, 0.05, 0.9, 0.9]) # И снова нарисуем график - pylab.pie (data, labels=labels) + pylab.pie(data, labels=labels) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm7.py b/matplotlib__examples/examples/exm7.py index cc4aeb8b9..fe12001ad 100644 --- a/matplotlib__examples/examples/exm7.py +++ b/matplotlib__examples/examples/exm7.py @@ -1,11 +1,11 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Цвета секторов задаются с помощью параметра colors функции pie(). import pylab -if __name__ == '__main__': +if __name__ == "__main__": # Данные для построения графика data = [20.0, 10.0, 5.0, 1.0, 0.5] @@ -16,12 +16,12 @@ colors = ["g", "r", "#FF00BB", "0.5", "y"] # Создать новое окно (фигуру) с одинаковыми размерами сторон (6 x 6 дюйма) - pylab.figure(figsize=(6, 6) ) + pylab.figure(figsize=(6, 6)) # Установим размеры осей по горизонтали и вертикали тоже одинаковыми pylab.axes([0.05, 0.05, 0.9, 0.9]) # И снова нарисуем график - pylab.pie (data, colors=colors, labels=labels) + pylab.pie(data, colors=colors, labels=labels) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm8.py b/matplotlib__examples/examples/exm8.py index 28672dd31..0ca842f93 100644 --- a/matplotlib__examples/examples/exm8.py +++ b/matplotlib__examples/examples/exm8.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Часто при отображении круговых графиков сектора немного "выдвигают" из центра. @@ -7,7 +7,7 @@ import pylab -if __name__ == '__main__': +if __name__ == "__main__": # Данные для построения графика data = [20.0, 10.0, 5.0, 1.0, 0.5] @@ -21,12 +21,12 @@ explode = [0.02, 0.05, 0.08, 0.08, 0.05] # Создать новое окно (фигуру) с одинаковыми размерами сторон (6 x 6 дюйма) - pylab.figure(figsize=(6, 6) ) + pylab.figure(figsize=(6, 6)) # Установим размеры осей по горизонтали и вертикали тоже одинаковыми pylab.axes([0.1, 0.1, 0.8, 0.8]) # И снова нарисуем график - pylab.pie (data, explode=explode, colors=colors, labels=labels) + pylab.pie(data, explode=explode, colors=colors, labels=labels) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/examples/exm9.py b/matplotlib__examples/examples/exm9.py index 10f0e373c..b682dfa88 100644 --- a/matplotlib__examples/examples/exm9.py +++ b/matplotlib__examples/examples/exm9.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Из эстетических соображений к графику можно добавить тень. Для этого в функцию pie() @@ -7,7 +7,7 @@ import pylab -if __name__ == '__main__': +if __name__ == "__main__": # Данные для построения графика data = [20.0, 10.0, 5.0, 1.0, 0.5] @@ -20,12 +20,12 @@ explode = [0.02, 0.05, 0.08, 0.08, 0.05] # Создать новое окно (фигуру) с одинаковыми размерами сторон (6 x 6 дюйма) - pylab.figure(figsize=(6, 6) ) + pylab.figure(figsize=(6, 6)) # Установим размеры осей по горизонтали и вертикали тоже одинаковыми pylab.axes([0.1, 0.1, 0.8, 0.8]) # И снова нарисуем график - pylab.pie (data, explode=explode, colors=colors, labels=labels, shadow=True) + pylab.pie(data, explode=explode, colors=colors, labels=labels, shadow=True) - pylab.show() \ No newline at end of file + pylab.show() diff --git a/matplotlib__examples/price_by_date__plot.py b/matplotlib__examples/price_by_date__plot.py index b5fb68c51..7bd7ad0ab 100644 --- a/matplotlib__examples/price_by_date__plot.py +++ b/matplotlib__examples/price_by_date__plot.py @@ -1,34 +1,70 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' - - -if __name__ == '__main__': - price_by_date = {156802: '2017-04-30', 147719: '2017-04-02', 120172: '2017-01-27', 123787: '2017-02-01', - 127890: '2017-02-11', 131699: '2017-02-16', 146452: '2017-03-27', 148507: '2017-04-09', - 144858: '2017-03-18', 144209: '2017-03-16', 126114: '2017-02-06', 102947: '2017-01-16', - 154404: '2017-04-22', 141733: '2017-03-11', 123249: '2017-01-29', 148008: '2017-04-07', - 112297: '2017-01-19', 120371: '2017-01-28', 119923: '2017-01-25', 127541: '2017-02-10', - 127163: '2017-02-07', 142911: '2017-03-14', 151106: '2017-04-10', 147020: '2017-03-29', - 123598: '2017-01-30', 136784: '2017-03-05', 134737: '2017-02-21', 135507: '2017-02-19', - 135636: '2017-02-20', 136535: '2017-02-27', 123866: '2017-02-04', 143710: '2017-03-15', - 142562: '2017-03-13', 138833: '2017-03-10', 153705: '2017-04-20', 137834: '2017-03-06', - 116076: '2017-01-22', 90386: '2017-01-15', 128239: '2017-02-14', 145136: '2017-03-20', - 146033: '2017-03-22', 124115: '2017-02-05', 135158: '2017-02-17', 145784: '2017-03-21', - 145535: '2017-03-19'} - - # Данные для построения графика - x = list(sorted(price_by_date.keys())) - - from datetime import datetime - get_date = lambda date_str: datetime.strptime(date_str, '%Y-%m-%d') - y = list(get_date(price_by_date[key]) for key in sorted(price_by_date.keys())) - - import pylab - pylab.plot(x, y) - - pylab.xlabel('Price') - pylab.ylabel('Date') - pylab.grid() - pylab.show() +__author__ = "ipetrash" + + +from datetime import datetime +import pylab + + +price_by_date = { + 156802: "2017-04-30", + 147719: "2017-04-02", + 120172: "2017-01-27", + 123787: "2017-02-01", + 127890: "2017-02-11", + 131699: "2017-02-16", + 146452: "2017-03-27", + 148507: "2017-04-09", + 144858: "2017-03-18", + 144209: "2017-03-16", + 126114: "2017-02-06", + 102947: "2017-01-16", + 154404: "2017-04-22", + 141733: "2017-03-11", + 123249: "2017-01-29", + 148008: "2017-04-07", + 112297: "2017-01-19", + 120371: "2017-01-28", + 119923: "2017-01-25", + 127541: "2017-02-10", + 127163: "2017-02-07", + 142911: "2017-03-14", + 151106: "2017-04-10", + 147020: "2017-03-29", + 123598: "2017-01-30", + 136784: "2017-03-05", + 134737: "2017-02-21", + 135507: "2017-02-19", + 135636: "2017-02-20", + 136535: "2017-02-27", + 123866: "2017-02-04", + 143710: "2017-03-15", + 142562: "2017-03-13", + 138833: "2017-03-10", + 153705: "2017-04-20", + 137834: "2017-03-06", + 116076: "2017-01-22", + 90386: "2017-01-15", + 128239: "2017-02-14", + 145136: "2017-03-20", + 146033: "2017-03-22", + 124115: "2017-02-05", + 135158: "2017-02-17", + 145784: "2017-03-21", + 145535: "2017-03-19", +} + +# Данные для построения графика +x = list(sorted(price_by_date.keys())) + +get_date = lambda date_str: datetime.strptime(date_str, "%Y-%m-%d") +y = list(get_date(price_by_date[key]) for key in sorted(price_by_date.keys())) + +pylab.plot(x, y) + +pylab.xlabel("Price") +pylab.ylabel("Date") +pylab.grid() +pylab.show() diff --git a/max_seqs_zeros__itertools.groupby.py b/max_seqs_zeros__itertools.groupby.py index 25022be7f..9ffd7dcf7 100644 --- a/max_seqs_zeros__itertools.groupby.py +++ b/max_seqs_zeros__itertools.groupby.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import itertools @@ -11,13 +11,13 @@ def end_zeros(num: int) -> int: size = 0 for x, items in itertools.groupby(str(num)): - if x == '0': + if x == "0": size = max(size, len(list(items))) return size -if __name__ == '__main__': +if __name__ == "__main__": assert end_zeros(10979000000) == 6 assert end_zeros(1000000) == 6 assert end_zeros(10009) == 3 diff --git a/mechanize__examples/follow_link.py b/mechanize__examples/follow_link.py new file mode 100644 index 000000000..0b9c12732 --- /dev/null +++ b/mechanize__examples/follow_link.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install mechanize +import mechanize + + +br = mechanize.Browser() +br.open("https://docs.python.org/3/") + +print(f"Title: {br.title()}") +print(f"URL: {br.geturl()}") +""" +Title: 3.11.1 Documentation +URL: https://docs.python.org/3/ +""" + +print() + +br.follow_link(text_regex=r"Tutorial") + +print(f"Title: {br.title()}") +print(f"URL: {br.geturl()}") +""" +Title: The Python Tutorial — Python 3.11.1 documentation +URL: https://docs.python.org/3/tutorial/index.html +""" diff --git a/mechanize__examples/submit_form.py b/mechanize__examples/submit_form.py new file mode 100644 index 000000000..32156515f --- /dev/null +++ b/mechanize__examples/submit_form.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import json + +# pip install mechanize +import mechanize + + +br = mechanize.Browser() +br.open("https://httpbin.org/forms/post") + +print(f"Title: {br.title()}") +print(f"URL: {br.geturl()}") + +rs = br.response() + +print("Headers:") +print(rs.info()) +print() + +print("Response body:") +print(rs.read()) + +print("\n" + "-" * 100 + "\n") + +br.select_form(nr=0) # First form +br["custname"] = "Customer" +br["custtel"] = "+79990001122" +br["custemail"] = "my@example.com" +br["size"] = ["medium"] # radio +br["topping"] = ["bacon", "cheese"] # checkbox +br["delivery"] = "11:15" +br["comments"] = "My comment!" + +rs2 = br.submit() + +print("Headers:") +print(rs2.info()) +print() + +rs2_body = rs2.read() +print("Response body:") +print(rs2_body) + +rs2_json = json.loads(rs2_body) +print(rs2_json["form"]) +# {'comments': 'My comment!', 'custemail': 'my@example.com', 'custname': 'Customer', 'custtel': '+79990001122', +# 'delivery': '11:15', 'size': 'medium', 'topping': ['bacon', 'cheese']} diff --git a/mediawiki__wikipedia.py b/mediawiki__wikipedia.py index 7cef42c82..2f3d25329 100644 --- a/mediawiki__wikipedia.py +++ b/mediawiki__wikipedia.py @@ -1,38 +1,39 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +from urllib.parse import unquote + # pip install pymediawiki # https://github.com/barrust/mediawiki # http://pymediawiki.readthedocs.io/en/latest/index.html +from mediawiki import MediaWiki -def wiki_search(query: str, lang='ru', unquote_percent_encoded=False) -> str: +def wiki_search(query: str, lang="ru", unquote_percent_encoded=False) -> str: # Default using wikipedia - from mediawiki import MediaWiki wikipedia = MediaWiki(lang=lang) result = wikipedia.opensearch(query, results=1) if not result: - return '' + return "" _, text, url = result[0] if unquote_percent_encoded: - from urllib.parse import unquote url = unquote(url) - return '{} ({})'.format(text, url) + return f"{text} ({url})" -if __name__ == '__main__': - query = 'GitHub' +if __name__ == "__main__": + query = "GitHub" print(wiki_search(query)) - query = 'python' + query = "python" print(wiki_search(query)) - query = 'магнитогорск' + query = "магнитогорск" print(wiki_search(query)) print(wiki_search(query, unquote_percent_encoded=True)) diff --git a/merge_url_params.py b/merge_url_params.py new file mode 100644 index 000000000..d3cac3876 --- /dev/null +++ b/merge_url_params.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from urllib.parse import ParseResult, urlparse, urlencode, parse_qsl + + +def merge_url_params(url: str, params: dict) -> str: + result: ParseResult = urlparse(url) + + current_params = dict(parse_qsl(result.query)) + merged_params = {**current_params, **params} + + new_query = urlencode(merged_params, doseq=True) + return result._replace(query=new_query).geturl() + + +if __name__ == "__main__": + url = "https://examples.com/jobs/search?keywords=engineer" + new_params = {"location": "United States", "keywords": True, "items": [1, 2, 3]} + + assert ( + merge_url_params(url, new_params) + == "https://examples.com/jobs/search?keywords=True&location=United+States&items=1&items=2&items=3" + ) + + url = "https://examples.com/jobs/search" + assert ( + merge_url_params(url, new_params) + == "https://examples.com/jobs/search?location=United+States&keywords=True&items=1&items=2&items=3" + ) diff --git a/metabolism.py b/metabolism.py new file mode 100644 index 000000000..2dc8969e5 --- /dev/null +++ b/metabolism.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +""" +Формулы для вычисления обмена веществ +""" + + +import enum + + +@enum.unique +class FactorEnum(enum.Enum): + # Сидячий образ жизни. Минимум нагрузки + V_12 = 1.2 + + # Небольшая физическая нагрузка/занятия спортом 1-3 в неделю + V_1375 = 1.375 + + # Достаточно большая физическая нагрузка/занятия спортом 3-5 в неделю + V_155 = 1.55 + + # Большая физическая нагрузка/занятия спортом 6-7 раз в неделю + V_1725 = 1.725 + + # Очень большая ежедневная физическая нагрузка/занятия спортом 2 раза в день + V_19 = 1.9 + + +def get_1918_for_male( + weight_kg: int, height_cm, age: int, factor: FactorEnum = FactorEnum.V_12 +) -> int: + """ + Формула-Уравнение Харриса-Бенедикта для мужчин. + """ + value = (13.7416 * weight_kg) + (5.0033 * height_cm) - (6.7500 * age) + 66.4730 + return int(value * factor.value) + + +def get_1918_for_female( + weight_kg: int, height_cm, age: int, factor: FactorEnum = FactorEnum.V_12 +) -> int: + """ + Формула-Уравнение Харриса-Бенедикта для женщин. + """ + value = (9.5634 * weight_kg) + (1.8496 * height_cm) - (4.6756 * age) + 665.0955 + return int(value * factor.value) + + +def get_1984_for_male( + weight_kg: int, height_cm, age: int, factor: FactorEnum = FactorEnum.V_12 +) -> int: + """ + Пересмотренная Формула-Уравнение Харриса-Бенедикта для мужчин. + """ + value = (13.397 * weight_kg) + (4.799 * height_cm) - (5.677 * age) + 88.362 + return int(value * factor.value) + + +def get_1984_for_female( + weight_kg: int, height_cm, age: int, factor: FactorEnum = FactorEnum.V_12 +) -> int: + """ + Пересмотренная Формула-Уравнение Харриса-Бенедикта для женщин. + """ + value = (9.247 * weight_kg) + (3.098 * height_cm) - (4.330 * age) + 447.593 + return int(value * factor.value) + + +def get_2005_for_male( + weight_kg: int, height_cm, age: int, factor: FactorEnum = FactorEnum.V_12 +) -> int: + """ + Формула-Уравнение Миффлина-Санкт-Джеора для мужчин. + """ + value = (10 * weight_kg) + (6.25 * height_cm) - (5 * age) + 5 + return int(value * factor.value) + + +def get_2005_for_female( + weight_kg: int, height_cm, age: int, factor: FactorEnum = FactorEnum.V_12 +) -> int: + """ + Формула-Уравнение Миффлина-Санкт-Джеора для женщин. + """ + value = (10 * weight_kg) + (6.25 * height_cm) - (5 * age) - 161 + return int(value * factor.value) + + +if __name__ == "__main__": + weight_kg = 100 + height_cm = 190 + age = 30 + + print("male (1918):", get_1918_for_male(weight_kg, height_cm, age)) + print("female (1918):", get_1918_for_female(weight_kg, height_cm, age)) + # male (1918): 2626 + # female (1918): 2199 + + print() + + print("male 1984:", get_1984_for_male(weight_kg, height_cm, age)) + print("female 1984:", get_1984_for_female(weight_kg, height_cm, age)) + # male 1984: 2603 + # female 1984: 2197 + + print() + + print("male 2005:", get_2005_for_male(weight_kg, height_cm, age)) + print("female 2005:", get_2005_for_female(weight_kg, height_cm, age)) + # male 2005: 2451 + # female 2005: 2251 diff --git a/minecraft__seed.py b/minecraft__seed.py deleted file mode 100644 index c37fe962e..000000000 --- a/minecraft__seed.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# SOURCE: https://minecraft-ru.gamepedia.com/Зерно -# Последовательность в поле ввода преобразуется с помощью Java-функции String.hashCode(). -# Например, строка «abc» конвертируется в числовое значение 97×31² + 98×31 + 99 = 96354. - -def get_value_seed_v1(seed: str) -> int: - return sum(map(lambda x: x[1] * (31 ** x[0]), enumerate(map(ord, reversed(seed))))) - - -def get_value_seed_v2(seed: str) -> int: - value = 0 - - for i in range(len(seed)): - j = (len(seed) - 1) - i - - value += ord(seed[i]) * (31 ** j) - - return value - - -def get_value_seed_v3(seed: str) -> int: - return sum(ord(seed[i]) * (31 ** ((len(seed) - 1) - i)) for i in range(len(seed))) - - -if __name__ == '__main__': - # print(97 * 31 ** 2 + 98 * 31 + 99) - - print(get_value_seed_v1('abc')) - print(get_value_seed_v2('abc')) - print(get_value_seed_v3('abc')) - - assert get_value_seed_v1('abc') == 96354 - assert get_value_seed_v2('abc') == 96354 - assert get_value_seed_v3('abc') == 96354 diff --git a/mock__examples/_test_datetime.py b/mock__examples/_test_datetime.py new file mode 100644 index 000000000..7d7a765ad --- /dev/null +++ b/mock__examples/_test_datetime.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime + + +def get() -> tuple[datetime, datetime]: + return datetime.now(), datetime.utcnow() diff --git a/mock__examples/datetime_now_utcnow.py b/mock__examples/datetime_now_utcnow.py new file mode 100644 index 000000000..416091557 --- /dev/null +++ b/mock__examples/datetime_now_utcnow.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from unittest.mock import patch, Mock +import datetime + +import _test_datetime + + +def test() -> None: + print("local:", datetime.datetime.now(), datetime.datetime.utcnow()) + print("import:", *_test_datetime.get()) + print() + + +datetime_mock = Mock(wraps=datetime.datetime) +datetime_mock.now.return_value = datetime.datetime(1999, 1, 1) +datetime_mock.utcnow.return_value = datetime.datetime(2000, 1, 1) + +test() + +with ( + patch("datetime.datetime", new=datetime_mock), + patch("_test_datetime.datetime", new=datetime_mock) +): + test() + # 1999-01-01 00:00:00 2000-01-01 00:00:00 + + assert datetime.datetime.now() == datetime.datetime(1999, 1, 1) + assert datetime.datetime.utcnow() == datetime.datetime(2000, 1, 1) + + now_2, utcnow_2 = _test_datetime.get() + assert datetime.datetime.now() == now_2 + assert datetime.datetime.utcnow() == utcnow_2 + +test() diff --git a/module click.py b/module click.py deleted file mode 100644 index 2c5ab50e1..000000000 --- a/module click.py +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import click - -# @click.group() -# def greet (): pass -# -# @greet.command () -# def hello ( ** kwargs ): pass -# -# @greet.command () -# def goodbye ( ** kwargs ): pass -# -# if __name__ == '__main__': -# greet () - -# @click.command() -# @click.argument('filename', required=False) -# def touch(filename): -# click.echo('filename=' + str(filename)) -# touch() - -# @click.command() -# @click.argument('src', nargs=-1) -# @click.argument('dst', nargs=1) -# def copy(src, dst): -# for fn in src: -# click.echo('move %s to folder %s' % (fn, dst)) -# -# if __name__ == '__main__': -# copy() - - -# @click.command() -# @click.argument('input', type=click.File('rb')) -# @click.argument('output', type=click.File('wb')) -# def inout(input, output): -# while True: -# chunk = input.read(1024) -# if not chunk: -# break -# output.write(chunk) -# -# inout() - -# -# @click.command() -# @click.argument('f', type=click.Path(exists=True)) -# def touch(f): -# click.echo(click.format_filename(f)) -# -# touch() - -# import os -# for k, v in os.environ.items(): -# print(k + ': ') -# for i, val in enumerate(v.split(';'), 1): -# print(' {}. {}'.format(i, val)) -# quit() - -# @click.command() -# @click.argument('src', envvar='SRC', type=click.File('r')) -# def echo(src): -# click.echo(src.read()) - -# @click.command() -# @click.argument('files', nargs=1, type=int) -# def touch(files): -# print(type(files)) -# click.echo(files) - - -# @click.command() -# @click.option('--value', type=[str, int]) -# def touch(value): -# print(value) -# click.echo('%s=%s' % value) - - -# @click.command() -# @click.option('--message', '-m', multiple=True) -# def touch(message): -# print(type(message), message) -# click.echo('\n'.join(message)) - - -# @click.command() -# @click.option('-m', '--message') -# def touch(message): -# print(type(message), message) -# click.echo('\n'.join(message)) - -# @click.command() -# @click.option('-v', '--verbose', count=True) -# def touch(verbose): -# click.echo('Verbosity: %s' % verbose) - -# @click.command() -# @click.option('-shoot/-no-shoot') -# @click.option('-flag', is_flag=True) -# @click.option('/debug;/no-debug') -# def touch(shoot, flag, debug): -# click.echo('Verbosity: %s %s %s' % (shoot, flag, debug)) - - -# import sys -# -# @click.command() -# @click.option('--upper', 'case', flag_value='upper', default=True) -# @click.option('--lower', 'case', flag_value='lower') -# def touch(case): -# print(case) -# click.echo(getattr(sys.platform, case)()) -# -# touch() - - -# @click.command() -# @click.option('--hash-type', '-hash', type=click.Choice(['md5', 'sha1'])) -# def digest(hash_type): -# click.echo(hash_type) -# -# digest() - -# @click.command() -# @click.option('--name', prompt=True) -# @click.option('--name2', prompt='Your name please') -# def hello(name, name2): -# click.echo('Hello %s-%s!' % (name, name2)) -# -# hello() - - -# @click.command() -# @click.option('--login', prompt='Your login please') -# @click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True) -# # @click.password_option() -# def encrypt(password, login): -# import codecs -# click.echo('Encrypting password to %s %s' % (codecs.encode(password, 'ROT13'), login)) -# -# encrypt() - - -# import os -# -# -# @click.command() -# # @click.option('--username', prompt=True, default=lambda: os.environ.get('JAVA_HOME', '')) -# @click.option('--username', prompt=True, default=lambda: 1) -# @click.option('--username', prompt=True, default=lambda: [1, 2, 3]) -# def hello(username): -# print("Hello,", username) -# -# hello() - - -# def print_version(ctx, param, value): -# if not value or ctx.resilient_parsing: -# return -# click.echo('Version 1.0') -# ctx.exit() -# -# @click.command() -# @click.option('--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True) -# def hello(): -# click.echo('Hello World!') -# -# hello() - - -# def abort_if_false(ctx, param, value): -# if not value: -# ctx.abort() -# -# @click.command() -# @click.option('--yes', is_flag=True, callback=abort_if_false, -# expose_value=False, -# prompt='Are you sure you want to drop the db?') -# def dropdb(): -# click.echo('Dropped all tables!') - - -# @click.command() -# @click.confirmation_option(help='Are you sure you want to drop the db?') -# def dropdb(): -# click.echo('Dropped all tables!') -# -# dropdb() - diff --git a/moving_LNKs_from_non_existent_files.py b/moving_LNKs_from_non_existent_files.py index f255e0ac8..4876bcee8 100644 --- a/moving_LNKs_from_non_existent_files.py +++ b/moving_LNKs_from_non_existent_files.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import shutil @@ -11,23 +11,23 @@ import winshell -DIR = Path(r'~\Desktop\Пройти').expanduser() -DIR_NOT_EXISTENT = DIR / 'Несуществуют' +DIR = Path(r"~\Desktop\Пройти").expanduser() +DIR_NOT_EXISTENT = DIR / "Несуществуют" files = [] -for file_name in DIR.glob('*.lnk'): +for file_name in DIR.glob("*.lnk"): shortcut = winshell.shortcut(str(file_name)) path = Path(shortcut.path) if not path.exists(): files.append(file_name) -print(f'Найдено {len(files)}') +print(f"Найдено {len(files)}") if files: DIR_NOT_EXISTENT.mkdir(parents=True, exist_ok=True) for f in files: - print(f'Выполнено перемещение {f.name} в папку {DIR_NOT_EXISTENT}') + print(f"Выполнено перемещение {f.name} в папку {DIR_NOT_EXISTENT}") new_file = DIR_NOT_EXISTENT / f.name shutil.move(f, new_file) diff --git a/my_torrent_list_from_iknowwhatyoudownload.py b/my_torrent_list_from_iknowwhatyoudownload.py index 96e7a7ede..88caad2f4 100644 --- a/my_torrent_list_from_iknowwhatyoudownload.py +++ b/my_torrent_list_from_iknowwhatyoudownload.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -11,31 +11,34 @@ """ +import time + import requests from bs4 import BeautifulSoup -import time def get_my_torrents(append_torrent_size=False): - rs = requests.get('http://iknowwhatyoudownload.com/ru/peer/', headers={'User-Agent': '-'}) - root = BeautifulSoup(rs.content, 'lxml') + rs = requests.get( + "http://iknowwhatyoudownload.com/ru/peer/", headers={"User-Agent": "-"} + ) + root = BeautifulSoup(rs.content, "lxml") # Если нужно вместе с названием передавать и размер торрента if not append_torrent_size: - return [item.text.strip() for item in root.select('.torrent_files > a')] + return [item.text.strip() for item in root.select(".torrent_files > a")] items = list() - for row in root.select('table > tbody > tr'): - name = row.select_one('.name-column').text.strip() - size = row.select_one('.size-column').text.strip() + for row in root.select("table > tbody > tr"): + name = row.select_one(".name-column").text.strip() + size = row.select_one(".size-column").text.strip() items.append((name, size)) return items -if __name__ == '__main__': +if __name__ == "__main__": while True: try: items = get_my_torrents() @@ -45,4 +48,4 @@ def get_my_torrents(append_torrent_size=False): time.sleep(60 * 60 * 12) except Exception as e: - print('Error:', e) + print("Error:", e) diff --git a/namedtuple__typing__example/main.py b/namedtuple__typing__example/main.py index 1d4965687..daec6fe50 100644 --- a/namedtuple__typing__example/main.py +++ b/namedtuple__typing__example/main.py @@ -1,28 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from collections import namedtuple +from typing import NamedTuple -Point = namedtuple('Point', ['x', 'y']) +Point = namedtuple("Point", ["x", "y"]) # # TypeError: __new__() missing 2 required positional arguments: 'x' and 'y' # p = Point() # print(p) p = Point(1, 2) -print(p) # Point(x=1, y=2) +print(p) # Point(x=1, y=2) print(p._asdict()) # OrderedDict([('x', 1), ('y', 2)]) print() # SOURCE: https://docs.python.org/3/library/typing.html#typing.NamedTuple -from typing import NamedTuple - - class Point(NamedTuple): x: int = 0 y: int = 0 @@ -32,5 +30,5 @@ class Point(NamedTuple): print(p) # Point(x=0, y=0) p = Point(1, 2) -print(p) # Point(x=1, y=2) +print(p) # Point(x=1, y=2) print(p._asdict()) # OrderedDict([('x', 1), ('y', 2)]) diff --git a/namedtuple_example/namedtuple.py b/namedtuple_example/namedtuple.py index 72ab00839..e41df4050 100644 --- a/namedtuple_example/namedtuple.py +++ b/namedtuple_example/namedtuple.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# TODO: https://docs.python.org/3/library/collections.html#collections.namedtuple_example -# Basic example from collections import namedtuple -Point = namedtuple('Point', ['x', 'y']) + + +Point = namedtuple("Point", ["x", "y"]) p = Point(11, y=22) # instantiate with positional or keyword arguments -print(p[0] + p[1]) # indexable like the plain tuple (11, 22) -x, y = p # unpack like a regular tuple +print(p[0] + p[1]) # indexable like the plain tuple (11, 22) +x, y = p # unpack like a regular tuple print(x, y) -print(p.x + p.y) # fields also accessible by name -print(p) # readable __repr__ with a name=value style +print(p.x + p.y) # fields also accessible by name +print(p) # readable __repr__ with a name=value style print() -XYZPoint = namedtuple('XYZPoint', ['x', 'y', 'z']) +XYZPoint = namedtuple("XYZPoint", ["x", "y", "z"]) xyz = XYZPoint(2, 2, z=5) print(xyz) print("z:", xyz.z) diff --git a/natasha__examples/hello_world.py b/natasha__examples/hello_world.py index 47534b7cc..337be9ca2 100644 --- a/natasha__examples/hello_world.py +++ b/natasha__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/natasha/natasha @@ -17,14 +17,14 @@ morph_tagger = NewsMorphTagger(emb) -text = 'Появление ООН было обусловлено целым рядом объективных факторов' +text = "Появление ООН было обусловлено целым рядом объективных факторов" doc = Doc(text) doc.segment(segmenter) doc.tag_morph(morph_tagger) for token in doc.tokens: - print(f'[{token.start}:{token.stop}] {token.pos} {token.text!r}\n{token}\n') + print(f"[{token.start}:{token.stop}] {token.pos} {token.text!r}\n{token}\n") """ [0:9] NOUN 'Появление' @@ -52,16 +52,16 @@ DocToken(start=55, stop=63, text='факторов', pos='NOUN', feats=) """ -print('\n' + '-' * 10 + '\n') +print("\n" + "-" * 10 + "\n") -text = 'Hello World!' +text = "Hello World!" doc = Doc(text) doc.segment(segmenter) doc.tag_morph(morph_tagger) for token in doc.tokens: - print(f'[{token.start}:{token.stop}] {token.pos} {token.text!r}\n{token}\n') + print(f"[{token.start}:{token.stop}] {token.pos} {token.text!r}\n{token}\n") """ [0:5] X 'Hello' diff --git a/neutralize_emoji.py b/neutralize_emoji.py index bade2dbb5..e77a533b1 100644 --- a/neutralize_emoji.py +++ b/neutralize_emoji.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/crinny/gatee/blob/11f78228fbb42dc4e06d180d90974849d4d4e45f/bot/utils/emoji.py#L7 @@ -12,8 +12,7 @@ def neutralize_emoji(character: str) -> str: Remove skin tone and gender modifiers from the emoji. """ return ( - character - .replace("🏻", "") + character.replace("🏻", "") .replace("🏼", "") .replace("🏽", "") .replace("🏾", "") @@ -23,11 +22,11 @@ def neutralize_emoji(character: str) -> str: ) -if __name__ == '__main__': - text = ', '.join(['👨', '👨🏻', '👨🏼', '👨🏽', '👨🏾', '👨🏿']) +if __name__ == "__main__": + text = ", ".join(["👨", "👨🏻", "👨🏼", "👨🏽", "👨🏾", "👨🏿"]) print(neutralize_emoji(text)) # 👨, 👨, 👨, 👨, 👨, 👨 - text = ', '.join(['👩', '👩🏻', '👩🏼', '👩🏽', '👩🏾', '👩🏿']) + text = ", ".join(["👩", "👩🏻", "👩🏼", "👩🏽", "👩🏾", "👩🏿"]) print(neutralize_emoji(text)) # 👩, 👩, 👩, 👩, 👩, 👩 diff --git a/nltk__examples/_add_support_russian.py b/nltk__examples/_add_support_russian.py index 448e1cb06..cd36ad0ad 100644 --- a/nltk__examples/_add_support_russian.py +++ b/nltk__examples/_add_support_russian.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install nltk import nltk -nltk.download('punkt') +nltk.download("punkt") diff --git a/nltk__examples/_add_support_russian_stopwords.py b/nltk__examples/_add_support_russian_stopwords.py index 9ac18f5b0..319b3192b 100644 --- a/nltk__examples/_add_support_russian_stopwords.py +++ b/nltk__examples/_add_support_russian_stopwords.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install nltk import nltk -nltk.download('stopwords') +nltk.download("stopwords") diff --git a/nltk__examples/nltk_sent_tokenize__with_new_lines.py b/nltk__examples/nltk_sent_tokenize__with_new_lines.py index c06d8b7fc..c28747e24 100644 --- a/nltk__examples/nltk_sent_tokenize__with_new_lines.py +++ b/nltk__examples/nltk_sent_tokenize__with_new_lines.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re @@ -19,16 +19,16 @@ sentences = [] for line in text.splitlines(): - sentences += nltk.sent_tokenize(line, language='russian') + sentences += nltk.sent_tokenize(line, language="russian") print(*map(repr, sentences)) # 'Согласен.' 'В первое ПОЕ' 'Да вся вторая часть' 'А читеры — подлецы.' -print('-' * 100) +print("-" * 100) -text = text.replace('\n', '. ') -text = re.sub('[.]{2,}', '.', text) +text = text.replace("\n", ". ") +text = re.sub("[.]{2,}", ".", text) -sentences = nltk.sent_tokenize(text, language='russian') +sentences = nltk.sent_tokenize(text, language="russian") print(*map(repr, sentences)) # 'Согласен.' 'В первое ПОЕ.' 'Да вся вторая часть.' 'А читеры — подлецы.' diff --git a/nltk__examples/split_by_sentences.py b/nltk__examples/split_by_sentences.py index 8a5bd40b7..361b5bae7 100644 --- a/nltk__examples/split_by_sentences.py +++ b/nltk__examples/split_by_sentences.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install nltk diff --git a/nltk__examples/split_by_sentences_and_words.py b/nltk__examples/split_by_sentences_and_words.py index 32895af0a..cb7fe956f 100644 --- a/nltk__examples/split_by_sentences_and_words.py +++ b/nltk__examples/split_by_sentences_and_words.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install nltk @@ -20,17 +20,17 @@ А читеры — подлецы. """ -sentences = nltk.sent_tokenize(text, language='russian') +sentences = nltk.sent_tokenize(text, language="russian") for sent in sentences: print(repr(sent)) words = nltk.word_tokenize(sent) - print(f'({len(words)}): {words}') + print(f"({len(words)}): {words}") words_no_punct = [word for word in words if not is_punctuation(word)] - print(f'({len(words_no_punct)}): {words_no_punct}') + print(f"({len(words_no_punct)}): {words_no_punct}") - words_no_punct = [word for word in words if 'PNCT' not in morph.parse(word)[0].tag] - print(f'({len(words_no_punct)}): {words_no_punct}') + words_no_punct = [word for word in words if "PNCT" not in morph.parse(word)[0].tag] + print(f"({len(words_no_punct)}): {words_no_punct}") print() diff --git a/nltk__examples/stopwords_russian.py b/nltk__examples/stopwords_russian.py index e8cedaf97..47513bbb8 100644 --- a/nltk__examples/stopwords_russian.py +++ b/nltk__examples/stopwords_russian.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install nltk from nltk.corpus import stopwords -print(stopwords.words('russian')) +print(stopwords.words("russian")) # ['и', 'в', 'во', 'не', 'что', 'он', 'на', 'я', 'с', 'со', 'как', 'а', 'то', 'все', 'она', # 'так', 'его', 'но', 'да', 'ты', 'к', 'у', 'же', 'вы', 'за', 'бы', 'по', 'только', 'ее', # 'мне', 'было', 'вот', 'от', 'меня', 'еще', 'нет', 'о', 'из', 'ему', 'теперь', 'когда', diff --git a/now_UTC_datetime.py b/now_UTC_datetime.py index eaf3fc64c..7017a5840 100644 --- a/now_UTC_datetime.py +++ b/now_UTC_datetime.py @@ -1,9 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT -utc_datetime = DT.datetime.utcnow() -print(utc_datetime.strftime("%d/%m/%Y %H:%M:%S")) +from datetime import datetime, timezone + +utc_datetime_old = datetime.utcnow().replace(microsecond=0) +print(utc_datetime_old) + +utc_datetime = datetime.now(timezone.utc).replace(microsecond=0) +print(utc_datetime) + +assert utc_datetime_old != utc_datetime +assert utc_datetime_old == utc_datetime.replace(tzinfo=None) diff --git a/now_UTC_datetime__previous_365_days.py b/now_UTC_datetime__previous_365_days.py index a4fa9840c..f08b36aff 100644 --- a/now_UTC_datetime__previous_365_days.py +++ b/now_UTC_datetime__previous_365_days.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT -utc_datetime = DT.datetime.utcnow() +from datetime import datetime, timedelta, timezone + + +utc_datetime = datetime.now(timezone.utc) for i in range(365 + 1): - date = utc_datetime - DT.timedelta(days=i) + date = utc_datetime - timedelta(days=i) print(date.strftime("%d/%m/%Y %H:%M:%S")) diff --git a/office__excel__openpyxl__xlwt/draw_image_in_sheet/__init__.py b/office__excel__openpyxl__xlwt/draw_image_in_sheet/__init__.py index 06334aa61..f25732790 100644 --- a/office__excel__openpyxl__xlwt/draw_image_in_sheet/__init__.py +++ b/office__excel__openpyxl__xlwt/draw_image_in_sheet/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' - +__author__ = "ipetrash" diff --git a/office__excel__openpyxl__xlwt/draw_image_in_sheet/main.py b/office__excel__openpyxl__xlwt/draw_image_in_sheet/main.py index a51fede76..8a34f582c 100644 --- a/office__excel__openpyxl__xlwt/draw_image_in_sheet/main.py +++ b/office__excel__openpyxl__xlwt/draw_image_in_sheet/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl @@ -12,7 +12,7 @@ # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/8b69f96d0bd87c10f3d0779ff46b02558c4cca84/excel__openpyxl__xlwt/xlsx__set_row_column_size.py -def set_row_column_size(ws): +def set_row_column_size(ws) -> None: # SOURCE: excel__openpyxl__xlwt\excel\xl\worksheets\sheet1.xml # ws.sheet_format.defaultColWidth = 1.77734375 @@ -34,7 +34,7 @@ def get_pixel_array(img, rgb_hex=False): r, g, b = img.getpixel((x, y)) if rgb_hex: - value = '{:02X}{:02X}{:02X}'.format(r, g, b) + value = f"{r:02X}{g:02X}{b:02X}" row.append(value) else: row.append((r, g, b)) @@ -42,8 +42,8 @@ def get_pixel_array(img, rgb_hex=False): return pixels -def draw_image(ws, img): - img = img.convert('RGB') +def draw_image(ws, img) -> None: + img = img.convert("RGB") # Resize img.thumbnail((250, 250)) @@ -56,9 +56,9 @@ def draw_image(ws, img): cell.fill = PatternFill(fgColor=pixels[i][j], fill_type="solid") -if __name__ == '__main__': +if __name__ == "__main__": wb = openpyxl.Workbook() - ws = wb.get_active_sheet() + ws = wb.active # Масштаб 10% ws.sheet_view.zoomScale = 10 @@ -70,4 +70,4 @@ def draw_image(ws, img): ws.title = file_name draw_image(ws, img) - wb.save('excel.xlsx') + wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/draw_image_list_in_sheets/main.py b/office__excel__openpyxl__xlwt/draw_image_list_in_sheets/main.py index 06afd6776..51c990878 100644 --- a/office__excel__openpyxl__xlwt/draw_image_list_in_sheets/main.py +++ b/office__excel__openpyxl__xlwt/draw_image_list_in_sheets/main.py @@ -1,34 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import glob import os +import sys import openpyxl # pip install Pillow from PIL import Image -import sys -sys.path.append('..') - # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/2c4391214de936260926a47440f2bad2d6fb90da/excel__openpyxl__xlwt/draw_image_in_sheet +sys.path.append("..") from draw_image_in_sheet.main import set_row_column_size, get_pixel_array, draw_image -if __name__ == '__main__': +if __name__ == "__main__": wb = openpyxl.Workbook() # Remove default sheet for sheet_name in wb.sheetnames: - sheet = wb.get_sheet_by_name(sheet_name) - wb.remove_sheet(sheet) + sheet = wb[sheet_name] + wb.remove(sheet) # Append images - for file_name in glob.glob('images/*.jpg') + glob.glob('images/*.png'): + for file_name in glob.glob("images/*.jpg") + glob.glob("images/*.png"): title = os.path.basename(file_name) ws = wb.create_sheet(title) @@ -40,4 +39,4 @@ img = Image.open(file_name) draw_image(ws, img) - wb.save('excel.xlsx') \ No newline at end of file + wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/find_cells_by_color/main.py b/office__excel__openpyxl__xlwt/find_cells_by_color/main.py index 2eb75e28e..93789d6c5 100644 --- a/office__excel__openpyxl__xlwt/find_cells_by_color/main.py +++ b/office__excel__openpyxl__xlwt/find_cells_by_color/main.py @@ -1,14 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/a/845335/201445 -def find_cells_by_color(ws, color='00000000'): - ret = {} +from openpyxl.worksheet.worksheet import Worksheet + + +def find_cells_by_color(ws: Worksheet, color: str = "00000000") -> dict: + ret = dict() for row in ws.iter_rows(): for cell in row: if cell.fill.fgColor.value == color: @@ -17,19 +20,20 @@ def find_cells_by_color(ws, color='00000000'): return ret -if __name__ == '__main__': +if __name__ == "__main__": from openpyxl import load_workbook - wb = load_workbook('excel.xlsx') - ws = wb.get_active_sheet() - print('background colors for ALL cells:\n') + wb = load_workbook("excel.xlsx") + ws = wb.active + + print("background colors for ALL cells:\n") for row in ws.iter_rows(): for cell in row: - print(f'[{cell.coordinate}]: {cell.fill.fgColor.value}', end=' ') + print(f"[{cell.coordinate}]: {cell.fill.fgColor.value}", end=" ") print() print() - cells = find_cells_by_color(ws, color='FFFFFF00') - print(f'given color has been found in the following cells: {cells}') + cells = find_cells_by_color(ws, color="FFFFFF00") + print(f"given color has been found in the following cells: {cells}") diff --git a/office__excel__openpyxl__xlwt/get_sheets/main.py b/office__excel__openpyxl__xlwt/get_sheets/main.py index 50a1f737b..a863e8001 100644 --- a/office__excel__openpyxl__xlwt/get_sheets/main.py +++ b/office__excel__openpyxl__xlwt/get_sheets/main.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl -wb = openpyxl.load_workbook('excel.xlsx') -print(wb.get_sheet_names()) # ['Sheet', 'auto', 'Students', 'Students1'] +wb = openpyxl.load_workbook("excel.xlsx") +print(wb.sheetnames) # ['Sheet', 'auto', 'Students', 'Students1'] -for name in wb.get_sheet_names(): - ws = wb.get_sheet_by_name(name) +for name in wb.sheetnames: + ws = wb[name] print(name, ws) diff --git a/office__excel__openpyxl__xlwt/hidden_columns.py b/office__excel__openpyxl__xlwt/hidden_columns.py index d9fb0b38d..43312110b 100644 --- a/office__excel__openpyxl__xlwt/hidden_columns.py +++ b/office__excel__openpyxl__xlwt/hidden_columns.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://openpyxl.readthedocs.io/en/stable/usage.html#fold-columns-outline @@ -12,7 +12,7 @@ wb = openpyxl.Workbook() -ws = wb.get_active_sheet() -ws.column_dimensions.group('B', 'D', hidden=True) +ws = wb.active +ws.column_dimensions.group("B", "D", hidden=True) -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/insert_image/main.py b/office__excel__openpyxl__xlwt/insert_image/main.py index 0dd37fe83..bb47534f4 100644 --- a/office__excel__openpyxl__xlwt/insert_image/main.py +++ b/office__excel__openpyxl__xlwt/insert_image/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://openpyxl.readthedocs.io/en/stable/usage.html#inserting-an-image @@ -12,14 +12,14 @@ wb = openpyxl.Workbook() -ws = wb.get_active_sheet() +ws = wb.active -ws['A1'] = 'You should see three logos below' +ws["A1"] = "You should see three logos below" # Create an image -img = Image('logo.png') +img = Image("logo.png") # Add to worksheet and anchor next to cells -ws.add_image(img, 'A2') +ws.add_image(img, "A2") -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/load__or_create__append_new_sheets__xlsx.py b/office__excel__openpyxl__xlwt/load__or_create__append_new_sheets__xlsx.py index fb98bc61d..bd8639d84 100644 --- a/office__excel__openpyxl__xlwt/load__or_create__append_new_sheets__xlsx.py +++ b/office__excel__openpyxl__xlwt/load__or_create__append_new_sheets__xlsx.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl -columns = ['Name', 'Age', 'Course'] +columns = ["Name", "Age", "Course"] rows = [ - ['Vasya', '16', 1], - ['Anya', '17', 2], - ['Inna', '16', 1], + ["Vasya", "16", 1], + ["Anya", "17", 2], + ["Inna", "16", 1], ] -FILE_NAME = 'excel.xlsx' +FILE_NAME = "excel.xlsx" try: wb = openpyxl.load_workbook(FILE_NAME) @@ -24,11 +24,11 @@ # Удаление листа, создаваемого по умолчанию, при создании документа for sheet_name in wb.sheetnames: - sheet = wb.get_sheet_by_name(sheet_name) - wb.remove_sheet(sheet) + sheet = wb[sheet_name] + wb.remove(sheet) # Создание нового листа, названия новых листов будут инкрементироваться: Students, Students1, Students2, и т.п. -ws = wb.create_sheet('Students') +ws = wb.create_sheet("Students") for i, value in enumerate(columns, 1): ws.cell(row=1, column=i).value = value @@ -37,4 +37,4 @@ for j, value in enumerate(row, 1): ws.cell(row=i, column=j).value = value -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/print_cells/main.py b/office__excel__openpyxl__xlwt/print_cells/main.py new file mode 100644 index 000000000..09b1e7a67 --- /dev/null +++ b/office__excel__openpyxl__xlwt/print_cells/main.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import openpyxl + + +path = "../get_sheets/excel.xlsx" + +wb = openpyxl.load_workbook(path) +sheet = wb.active + +for row in range(1, sheet.max_row + 1): + for col in range(1, sheet.max_column + 1): + cell = sheet.cell(row=row, column=col) + print(cell.value, end=" | ") + print() +""" +Language | Text | +python | ---------- | +java | jjjjjjjjjj | +c# | ***** | +c++ | 00000000000000000000000000000000000000000000000000 | +""" diff --git a/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/output.xlsx b/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/output.xlsx new file mode 100644 index 000000000..36fb9146d Binary files /dev/null and b/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/output.xlsx differ diff --git a/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/read.py b/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/read.py new file mode 100644 index 000000000..3222048c8 --- /dev/null +++ b/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/read.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import json +from pathlib import Path + +# pip install pyexcel-xlsx +from pyexcel_xlsx import read_data + + +DIR = Path(__file__).resolve().parent +file_name = str(DIR / "output.xlsx") + + +data = read_data(file_name) +print(json.dumps(data, ensure_ascii=False, indent=4)) +""" +{ + "Sheet 1": [ + [ + "Col1", + "Col2", + "Col3" + ], + [ + 1, + 2, + 3 + ], + [ + 4, + 5, + 6 + ] + ], + "Sheet 2": [ + [ + "row 1", + "row 2", + "row 3" + ] + ], + "Страница 3": [ + [ + "Поле:", + "Привет" + ], + [ + "Поле:", + "Мир!" + ] + ] +} +""" diff --git a/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/write.py b/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/write.py new file mode 100644 index 000000000..ea4e7b1b0 --- /dev/null +++ b/office__excel__openpyxl__xlwt/pyexcel_xlsx__examples/write.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +# pip install pyexcel-xlsx +from pyexcel_xlsx import save_data + + +DIR = Path(__file__).resolve().parent +file_name = str(DIR / "output.xlsx") + + +data = { + "Sheet 1": [ + ["Col1", "Col2", "Col3"], + [1, 2, 3], + [4, 5, 6] + ], + "Sheet 2": [["row 1"]], +} +data.update({ + "Sheet 2": [ + ["row 1", "row 2", "row 3"] + ] +}) +data.update({ + "Страница 3": [ + ["Поле:", "Привет"], + ["Поле:", "Мир!"] + ] +}) + +save_data(file_name, data) diff --git a/office__excel__openpyxl__xlwt/random_fill_color.py b/office__excel__openpyxl__xlwt/random_fill_color.py index ee7dba5f8..892a93926 100644 --- a/office__excel__openpyxl__xlwt/random_fill_color.py +++ b/office__excel__openpyxl__xlwt/random_fill_color.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import random import openpyxl from openpyxl.styles import PatternFill +from openpyxl.worksheet.worksheet import Worksheet # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/8b69f96d0bd87c10f3d0779ff46b02558c4cca84/excel__openpyxl__xlwt/xlsx__set_row_column_size.py -def set_row_column_size(ws): +def set_row_column_size(ws: Worksheet) -> None: # SOURCE: excel__openpyxl__xlwt\excel\xl\worksheets\sheet1.xml # ws.sheet_format.defaultColWidth = 1.77734375 @@ -20,11 +21,11 @@ def set_row_column_size(ws): # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/1c2274804f52004b07601ece535ec74b3f82aa8d/get_random_hex_color.py -def get_random_hex_color(): - return ''.join(random.choices('0123456789ABCDEF', k=6)) +def get_random_hex_color() -> str: + return "".join(random.choices("0123456789ABCDEF", k=6)) -def set_fill_color(ws): +def set_fill_color(ws: Worksheet) -> None: size = 250 for i in range(1, size + 1): @@ -34,7 +35,7 @@ def set_fill_color(ws): wb = openpyxl.Workbook() -ws = wb.get_active_sheet() +ws = wb.active # Масштаб 10% ws.sheet_view.zoomScale = 10 @@ -42,4 +43,4 @@ def set_fill_color(ws): set_row_column_size(ws) set_fill_color(ws) -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/set_cell_color.py b/office__excel__openpyxl__xlwt/set_cell_color.py index cbdb67450..4dd7a2080 100644 --- a/office__excel__openpyxl__xlwt/set_cell_color.py +++ b/office__excel__openpyxl__xlwt/set_cell_color.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl @@ -9,10 +9,10 @@ wb = openpyxl.Workbook() -ws = wb.get_active_sheet() +ws = wb.active -ws.cell(row=1, column=1).fill = PatternFill(fgColor='FF0000', fill_type="solid") -ws.cell(row=2, column=1).fill = PatternFill(fgColor='00FF00', fill_type="solid") -ws['A3'].fill = PatternFill(fgColor='0000FF', fill_type="solid") +ws.cell(row=1, column=1).fill = PatternFill(fgColor="FF0000", fill_type="solid") +ws.cell(row=2, column=1).fill = PatternFill(fgColor="00FF00", fill_type="solid") +ws["A3"].fill = PatternFill(fgColor="0000FF", fill_type="solid") -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xls__xlwt__hello_world.py b/office__excel__openpyxl__xlwt/xls__xlwt__hello_world.py index 26a66be22..360aed216 100644 --- a/office__excel__openpyxl__xlwt/xls__xlwt__hello_world.py +++ b/office__excel__openpyxl__xlwt/xls__xlwt__hello_world.py @@ -1,23 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install xlwt import xlwt -columns = ['Name', 'Age', 'Course'] +columns = ["Name", "Age", "Course"] rows = [ - ['Vasya', '16', 1], - ['Anya', '17', 2], - ['Inna', '16', 1], + ["Vasya", "16", 1], + ["Anya", "17", 2], + ["Inna", "16", 1], ] wb = xlwt.Workbook() -ws = wb.add_sheet('Students') +ws = wb.add_sheet("Students") for i, column in enumerate(columns): ws.write(0, i, column) @@ -26,4 +26,4 @@ for j, data in enumerate(row): ws.write(i, j, data) -wb.save('excel.xls') +wb.save("excel.xls") diff --git a/office__excel__openpyxl__xlwt/xlsx__auto_size__columns.py b/office__excel__openpyxl__xlwt/xlsx__auto_size__columns.py index c1e4c5f9e..651585216 100644 --- a/office__excel__openpyxl__xlwt/xlsx__auto_size__columns.py +++ b/office__excel__openpyxl__xlwt/xlsx__auto_size__columns.py @@ -1,33 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl +from openpyxl.worksheet.worksheet import Worksheet -def set_column_size(worksheet): - dims = dict() +def set_column_size(ws: Worksheet) -> None: + dims: dict[str, int] = dict() - for row in worksheet.rows: + for row in ws.rows: for cell in row: if not cell.value: continue - dims[cell.column] = max(dims.get(cell.column, 0), len(str(cell.value))) + column = cell.column_letter + dims[column] = max(dims.get(column, 0), len(str(cell.value))) for col, value in dims.items(): - worksheet.column_dimensions[col].width = value * 1.15 # Append 15% + ws.column_dimensions[col].width = value * 1.15 # Append 15% -def fill_sheet(ws): - columns = ['Language', 'Text'] +def fill_sheet(ws: Worksheet) -> None: + columns = ["Language", "Text"] rows = [ - ['python', '-' * 10], - ['java', 'j' * 10], - ['c#', '*' * 5], - ['c++', '0' * 50], + ["python", "-" * 10], + ["java", "j" * 10], + ["c#", "*" * 5], + ["c++", "0" * 50], ] for i, value in enumerate(columns, 1): @@ -40,14 +42,14 @@ def fill_sheet(ws): wb = openpyxl.Workbook() -ws = wb.get_active_sheet() +ws = wb.active # Sheet 1 fill_sheet(ws) # Sheet 2 -ws = wb.create_sheet('auto') +ws = wb.create_sheet("set_column_size") fill_sheet(ws) set_column_size(ws) -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xlsx__formula_cell__SUM_function.py b/office__excel__openpyxl__xlwt/xlsx__formula_cell__SUM_function.py index 9c0dfa4c3..980b13b9c 100644 --- a/office__excel__openpyxl__xlwt/xlsx__formula_cell__SUM_function.py +++ b/office__excel__openpyxl__xlwt/xlsx__formula_cell__SUM_function.py @@ -1,22 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl -columns = ['Language', 'Text'] -rows = [ - ['python', 1], - ['java', 2], - ['c#', 3], - ['Total:', 0] -] +columns = ["Language", "Text"] +rows = [["python", 1], ["java", 2], ["c#", 3], ["Total:", 0]] wb = openpyxl.Workbook() -ws = wb.get_active_sheet() +ws = wb.active for i, value in enumerate(columns, 1): ws.cell(row=1, column=i).value = value @@ -27,6 +22,6 @@ cell.value = value # Total: -ws.cell(row=5, column=2).value = '=SUM(B2:B4)' +ws.cell(row=5, column=2).value = "=SUM(B2:B4)" -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell.py b/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell.py index 3d8039aa1..8efdf89a2 100644 --- a/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell.py +++ b/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from urllib.parse import quote import openpyxl -columns = ['Language', 'Text', 'Hyperlink'] +columns = ["Language", "Text", "Hyperlink"] rows = [ - ['python', 'excel'], - ['java', 'excel'], - ['c#', 'excel'], + ["python", "excel"], + ["java", "excel"], + ["c#", "excel"], ] wb = openpyxl.Workbook() -ws = wb.get_active_sheet() +ws = wb.active for i, value in enumerate(columns, 1): ws.cell(row=1, column=i).value = value @@ -27,8 +27,9 @@ ws.cell(row=i, column=1).value = lang ws.cell(row=i, column=2).value = text - ws.cell(row=i, column=3).hyperlink = "https://stackoverflow.com/search?q=" + quote(lang + ' ' + text) + url = "https://stackoverflow.com/search?q=" + quote(lang + " " + text) + ws.cell(row=i, column=3).hyperlink = url ws.cell(row=i, column=3).value = "StackOverflow" -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell_to_sheet.py b/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell_to_sheet.py new file mode 100644 index 000000000..43d24fd1f --- /dev/null +++ b/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell_to_sheet.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import openpyxl +from openpyxl.worksheet.hyperlink import Hyperlink + + +wb = openpyxl.Workbook() + +sheet1 = wb.active + +sheet2 = wb.create_sheet(title="Лист 2") + +cell = sheet1.cell(row=1, column=1) +cell.hyperlink = f"#'{sheet2.title}'!K20" +cell.value = "Go" +cell.style = "Hyperlink" + +cell = sheet1.cell(row=2, column=1) +cell.hyperlink = Hyperlink( + location=f"'{sheet2.title}'!K20", + ref="", +) +cell.value = "Go 2" +cell.style = "Hyperlink" + +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell_to_sheet_for_exists.py b/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell_to_sheet_for_exists.py new file mode 100644 index 000000000..630b1a382 --- /dev/null +++ b/office__excel__openpyxl__xlwt/xlsx__hyperlink_cell_to_sheet_for_exists.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import openpyxl + + +# NOTE: from xlsx__hyperlink_cell_to_sheet.py +filename = "excel.xlsx" + +wb = openpyxl.load_workbook(filename=filename) + +sheet1 = wb["Sheet"] +sheet2 = wb["Лист 2"] + +cell = sheet1.cell(row=5, column=1) +cell.hyperlink = f"#'{sheet2.title}'!B20" +cell.value = "Go 3" +cell.style = "Hyperlink" + +wb.save(filename) diff --git a/office__excel__openpyxl__xlwt/xlsx__merge_cells__and__formula_cell__SUM_function.py b/office__excel__openpyxl__xlwt/xlsx__merge_cells__and__formula_cell__SUM_function.py index 3460d7117..e1b83ff07 100644 --- a/office__excel__openpyxl__xlwt/xlsx__merge_cells__and__formula_cell__SUM_function.py +++ b/office__excel__openpyxl__xlwt/xlsx__merge_cells__and__formula_cell__SUM_function.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl -columns = ['Language', 'Text', 'Total'] +columns = ["Language", "Text", "Total"] rows = [ - ['python', 1], - ['java', 2], - ['c#', 3], + ["python", 1], + ["java", 2], + ["c#", 3], ] wb = openpyxl.Workbook() -ws = wb.get_active_sheet() +ws = wb.active for i, value in enumerate(columns, 1): ws.cell(row=1, column=i).value = value @@ -25,9 +25,9 @@ cell = ws.cell(row=i, column=j) cell.value = value -ws.merge_cells('C2:C4') +ws.merge_cells("C2:C4") # Total: -ws['C2'].value = '=SUM(B2:B4)' +ws["C2"].value = "=SUM(B2:B4)" -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xlsx__openpyxl__hello_world.py b/office__excel__openpyxl__xlwt/xlsx__openpyxl__hello_world.py index 5d39b5031..afba7b9a9 100644 --- a/office__excel__openpyxl__xlwt/xlsx__openpyxl__hello_world.py +++ b/office__excel__openpyxl__xlwt/xlsx__openpyxl__hello_world.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install openpyxl import openpyxl -columns = ['Name', 'Age', 'Course'] +columns = ["Name", "Age", "Course"] rows = [ - ['Vasya', '16', 1], - ['Anya', '17', 2], - ['Inna', '16', 1], + ["Vasya", "16", 1], + ["Anya", "17", 2], + ["Inna", "16", 1], ] @@ -20,10 +20,10 @@ # Remove default sheet for sheet_name in wb.sheetnames: - sheet = wb.get_sheet_by_name(sheet_name) - wb.remove_sheet(sheet) + sheet = wb[sheet_name] + wb.remove(sheet) -ws = wb.create_sheet('Students') +ws = wb.create_sheet("Students") for i, value in enumerate(columns, 1): ws.cell(row=1, column=i).value = value @@ -32,4 +32,4 @@ for j, value in enumerate(row, 1): ws.cell(row=i, column=j).value = value -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xlsx__set_row_column_size.py b/office__excel__openpyxl__xlwt/xlsx__set_row_column_size.py index 90c0a8c98..7324565de 100644 --- a/office__excel__openpyxl__xlwt/xlsx__set_row_column_size.py +++ b/office__excel__openpyxl__xlwt/xlsx__set_row_column_size.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl +from openpyxl.worksheet.worksheet import Worksheet -def set_row_column_size(ws): +def set_row_column_size(ws: Worksheet) -> None: # SOURCE: excel__openpyxl__xlwt\excel\xl\worksheets\sheet1.xml # ws.sheet_format.defaultColWidth = 1.77734375 @@ -16,10 +17,9 @@ def set_row_column_size(ws): wb = openpyxl.Workbook() -ws = wb.get_active_sheet() # Sheet 2 ws = wb.create_sheet() set_row_column_size(ws) -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__excel__openpyxl__xlwt/xlsx__set_sheet_zoom_scale.py b/office__excel__openpyxl__xlwt/xlsx__set_sheet_zoom_scale.py index cbc34638a..2de7489aa 100644 --- a/office__excel__openpyxl__xlwt/xlsx__set_sheet_zoom_scale.py +++ b/office__excel__openpyxl__xlwt/xlsx__set_sheet_zoom_scale.py @@ -1,30 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import openpyxl + wb = openpyxl.Workbook() -ws = wb.get_active_sheet() -ws.title = '10' +ws = wb.active +ws.title = "10" ws.sheet_view.zoomScale = 10 -ws = wb.create_sheet('20') +ws = wb.create_sheet("20") ws.sheet_view.zoomScale = 20 -ws = wb.create_sheet('50') +ws = wb.create_sheet("50") ws.sheet_view.zoomScale = 50 -ws = wb.create_sheet('100') +ws = wb.create_sheet("100") # ws.sheet_view.zoomScale = 100 -ws = wb.create_sheet('200') +ws = wb.create_sheet("200") ws.sheet_view.zoomScale = 200 -ws = wb.create_sheet('500') +ws = wb.create_sheet("500") ws.sheet_view.zoomScale = 500 -wb.save('excel.xlsx') +wb.save("excel.xlsx") diff --git a/office__word__doc_docx/counter_of_font.py b/office__word__doc_docx/counter_of_font.py new file mode 100644 index 000000000..8d61973c3 --- /dev/null +++ b/office__word__doc_docx/counter_of_font.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from collections import Counter +from pathlib import Path + +# pip install python-docx==1.1.2 +import docx +from docx.text.run import Run +from docx.text.paragraph import Paragraph + + +def get_font_name_and_size( + doc: docx.Document, + p: Paragraph, + r: Run, +) -> tuple[str | None, int | None]: + def _get_normal_size(size: int | None) -> int | None: + return size // 12700 if size else None + + try: + font_name = r.font.name + except: + font_name = None + + if not font_name: + try: + font_name = r.style.font.name + except: + pass + + if not font_name: + try: + font_name = p.style.font.name + except: + pass + + if not font_name: + try: + font_name = doc.styles["Normal"].font.name + except: + pass + + try: + font_size = r.font.size + except: + font_size = None + + if not font_size: + try: + font_size = r.style.font.size + except: + pass + + if not font_size: + try: + font_size = p.style.font.size + except: + pass + + if not font_size: + try: + font_size = doc.styles["Normal"].font.size + except: + pass + + return font_name, _get_normal_size(font_size) + + +def process(path: Path) -> None: + for file_name in path.glob("*.docx"): + print(file_name) + + try: + doc_file = docx.Document(file_name) + + name_and_size = [] + names = [] + sizes = [] + + for p in doc_file.paragraphs: + for r in p.runs: + name, size = get_font_name_and_size(doc_file, p, r) + name_and_size.append((name, size)) + names.append(name) + sizes.append(size) + + for (name, size), number in Counter(name_and_size).most_common(): + print((name, size), number) + + except Exception as e: + print(e) + + print() + + +if __name__ == "__main__": + path = Path(__file__).parent / "fill_template" + process(path) diff --git a/office__word__doc_docx/create_table__function.py b/office__word__doc_docx/create_table__function.py index 02ec74971..7520e7e0d 100644 --- a/office__word__doc_docx/create_table__function.py +++ b/office__word__doc_docx/create_table__function.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from docx import Document -def create_table(document, headers, rows, style='Table Grid'): +def create_table(document, headers, rows, style="Table Grid"): cols_number = len(headers) table = document.add_table(rows=1, cols=cols_number) @@ -27,20 +27,18 @@ def create_table(document, headers, rows, style='Table Grid'): document = Document() -headers = ('№ п/п', 'Наименование параметра', 'Единицы измерения', 'Значение') +headers = ("№ п/п", "Наименование параметра", "Единицы измерения", "Значение") records_table1 = ( - (0, 'Nan', 'Nan', 0), - (1, 'Первая величина', '-/-', 0), - (2, 'Вторая величина', '-/-', 'Базальт'), - (3, 'Третья величина', 'м^2/ч', 0) + (0, "Nan", "Nan", 0), + (1, "Первая величина", "-/-", 0), + (2, "Вторая величина", "-/-", "Базальт"), + (3, "Третья величина", "м^2/ч", 0), ) table1 = create_table(document, headers, records_table1) document.add_paragraph() -rows = [ - [x, x, x * x] for x in range(1, 10) -] -table2 = create_table(document, ('x', 'y', 'x * y'), rows) +rows = [[x, x, x * x] for x in range(1, 10)] +table2 = create_table(document, ("x", "y", "x * y"), rows) document.save("testing.docx") diff --git a/office__word__doc_docx/fill_template/save_style.py b/office__word__doc_docx/fill_template/save_style.py index 50137dd64..d11db0b6d 100644 --- a/office__word__doc_docx/fill_template/save_style.py +++ b/office__word__doc_docx/fill_template/save_style.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt # pip install python-docx import docx # SOURCE: https://stackoverflow.com/a/55733040/5909792 -def docx_replace(doc, data): +def docx_replace(doc, data) -> None: paragraphs = list(doc.paragraphs) for t in doc.tables: for row in t.rows: @@ -20,7 +20,8 @@ def docx_replace(doc, data): paragraphs.append(paragraph) for p in paragraphs: for key, val in data.items(): - key_name = '${{{}}}'.format(key) # I'm using placeholders in the form ${PlaceholderName} + # I'm using placeholders in the form ${PlaceholderName} + key_name = "${{{}}}".format(key) if key_name in p.text: inline = p.runs # Replace strings and retain the same style. @@ -34,10 +35,11 @@ def docx_replace(doc, data): found_all = False replace_done = False for i in range(len(inline)): - # case 1: found in single run so short circuit the replace if key_name in inline[i].text and not started: - found_runs.append((i, inline[i].text.find(key_name), len(key_name))) + found_runs.append( + (i, inline[i].text.find(key_name), len(key_name)) + ) text = inline[i].text.replace(key_name, str(val)) inline[i].text = text replace_done = True @@ -49,7 +51,11 @@ def docx_replace(doc, data): continue # case 2: search for partial text, find first run - if key_name[key_index] in inline[i].text and inline[i].text[-1] in key_name and not started: + if ( + key_name[key_index] in inline[i].text + and inline[i].text[-1] in key_name + and not started + ): # check sequence start_index = inline[i].text.find(key_name[key_index]) check_length = len(inline[i].text) @@ -70,7 +76,11 @@ def docx_replace(doc, data): break # case 2: search for partial text, find subsequent run - if key_name[key_index] in inline[i].text and started and not found_all: + if ( + key_name[key_index] in inline[i].text + and started + and not found_all + ): # check sequence chars_found = 0 check_length = len(inline[i].text) @@ -90,21 +100,25 @@ def docx_replace(doc, data): for i, item in enumerate(found_runs): index, start, length = [t for t in item] if i == 0: - text = inline[index].text.replace(inline[index].text[start:start + length], str(val)) + text = inline[index].text.replace( + inline[index].text[start : start + length], str(val) + ) inline[index].text = text else: - text = inline[index].text.replace(inline[index].text[start:start + length], '') + text = inline[index].text.replace( + inline[index].text[start : start + length], "" + ) inline[index].text = text # print(p.text) -if __name__ == '__main__': - from_filename = 'template.docx' - to_filename = 'save_style.docx' +if __name__ == "__main__": + from_filename = "template.docx" + to_filename = "save_style.docx" REPLACING = { - 'title': 'My pretty title!', - 'date_time': DT.datetime.now().strftime('%Y/%m/%d %H:%M:%S'), + "title": "My pretty title!", + "date_time": dt.datetime.now().strftime("%Y/%m/%d %H:%M:%S"), } doc = docx.Document(from_filename) diff --git a/office__word__doc_docx/fill_template/simple.py b/office__word__doc_docx/fill_template/simple.py index f73227886..86fd52b63 100644 --- a/office__word__doc_docx/fill_template/simple.py +++ b/office__word__doc_docx/fill_template/simple.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt # pip install python-docx import docx -from_filename = 'template.docx' -to_filename = 'simple.docx' +from_filename = "template.docx" +to_filename = "simple.docx" REPLACING = { - '${title}': 'My pretty title!', - '${date_time}': DT.datetime.now().strftime('%Y/%m/%d %H:%M:%S'), + "${title}": "My pretty title!", + "${date_time}": dt.datetime.now().strftime("%Y/%m/%d %H:%M:%S"), } doc = docx.Document(from_filename) diff --git a/office__word__doc_docx/hello_world/main.py b/office__word__doc_docx/hello_world/main.py index e9b51843e..866e84d79 100644 --- a/office__word__doc_docx/hello_world/main.py +++ b/office__word__doc_docx/hello_world/main.py @@ -1,47 +1,84 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/python-openxml/python-docx # SOURCE: https://python-docx.readthedocs.io/en/latest/ +import os + # pip install python-docx import docx from docx.shared import Inches, Cm, Mm document = docx.Document() -document.add_heading('Document Title', level=0) +document.add_heading("Document Title", level=0) -p = document.add_paragraph('A plain paragraph having some ') -p.add_run('bold').bold = True -p.add_run(' and some ') -p.add_run('italic.').italic = True +p = document.add_paragraph("A plain paragraph having some ") +p.add_run("bold").bold = True +p.add_run(" and some ") +p.add_run("italic.").italic = True -document.add_heading('Heading, level 1', level=1) -document.add_paragraph('Intense quote', style='Intense Quote') -document.add_paragraph('first item in unordered list', style='List Bullet') -document.add_paragraph('first item in ordered list', style='List Number') +document.add_heading("Heading, level 1", level=1) +document.add_paragraph("Intense quote", style="Intense Quote") +document.add_paragraph("first item in unordered list", style="List Bullet") +document.add_paragraph("first item in ordered list", style="List Number") document.add_page_break() # Table -document.add_heading('Table', level=1) +document.add_heading("Table", level=1) -headers = ['id', 'url', 'name', 'short_name', 'birthday', 'job'] +headers = ["id", "url", "name", "short_name", "birthday", "job"] rows = [ - ['#1', 'http://amiller.example.com', 'Andrew Miller', 'amiller', '11 December', 'Testing Engineer'], - ['#2', 'http://ataylor.example.com', 'Anthony Taylor', 'ataylor', '17 July', 'Software Engineer'], - ['#3', 'http://dmoore.example.com', 'Daniel Moore', 'dmoore', '2 March', 'Testing Engineer'], - ['#4', 'http://dsmith.example.com', 'David Smith', 'dsmith', '5 January', 'Testing Engineer'], - ['#5', 'http://awilson.example.com', 'Alexander Wilson', 'awilson', '11 April', 'Software Engineer'] + [ + "#1", + "http://amiller.example.com", + "Andrew Miller", + "amiller", + "11 December", + "Testing Engineer", + ], + [ + "#2", + "http://ataylor.example.com", + "Anthony Taylor", + "ataylor", + "17 July", + "Software Engineer", + ], + [ + "#3", + "http://dmoore.example.com", + "Daniel Moore", + "dmoore", + "2 March", + "Testing Engineer", + ], + [ + "#4", + "http://dsmith.example.com", + "David Smith", + "dsmith", + "5 January", + "Testing Engineer", + ], + [ + "#5", + "http://awilson.example.com", + "Alexander Wilson", + "awilson", + "11 April", + "Software Engineer", + ], ] # Без style='Table Grid' у таблицы не будет выделена решетка -table = document.add_table(rows=1, cols=len(headers), style='Table Grid') +table = document.add_table(rows=1, cols=len(headers), style="Table Grid") heading_cells = table.rows[0].cells for i, value in enumerate(headers): @@ -59,28 +96,27 @@ document.add_page_break() # Image -file_name_image = 'image.jpg' +file_name_image = "image.jpg" -document.add_heading('Image', level=1) +document.add_heading("Image", level=1) -document.add_heading('image__original', level=2) +document.add_heading("image__original", level=2) document.add_picture(file_name_image) -document.add_heading('image__Inches_3', level=2) +document.add_heading("image__Inches_3", level=2) document.add_picture(file_name_image, width=Inches(3.0)) -document.add_heading('image__millimeters_30', level=2) +document.add_heading("image__millimeters_30", level=2) document.add_picture(file_name_image, width=Mm(30)) -document.add_heading('image__centimeters_5', level=2) +document.add_heading("image__centimeters_5", level=2) document.add_picture(file_name_image, width=Cm(5)) document.add_page_break() # Save -file_name_doc = 'word.docx' +file_name_doc = "word.docx" document.save(file_name_doc) # Open file -import os os.startfile(file_name_doc) diff --git a/office__word__doc_docx/hyperlink.py b/office__word__doc_docx/hyperlink.py index 663272f20..7fe12fe83 100644 --- a/office__word__doc_docx/hyperlink.py +++ b/office__word__doc_docx/hyperlink.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install python-docx @@ -9,7 +9,7 @@ # SOURCE: https://github.com/python-openxml/python-docx/issues/74#issuecomment-261169410 -def add_hyperlink(paragraph, url, text, color='1111FF', underline=True): +def add_hyperlink(paragraph, url, text, color="1111FF", underline=True): """ A function that places a hyperlink within a paragraph object. @@ -23,32 +23,37 @@ def add_hyperlink(paragraph, url, text, color='1111FF', underline=True): # This gets access to the document.xml.rels file and gets a new relation id value part = paragraph.part - r_id = part.relate_to(url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True) + r_id = part.relate_to( + url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True + ) # Create the w:hyperlink tag and add needed values - hyperlink = docx.oxml.shared.OxmlElement('w:hyperlink') - hyperlink.set(docx.oxml.shared.qn('r:id'), r_id, ) + hyperlink = docx.oxml.shared.OxmlElement("w:hyperlink") + hyperlink.set( + docx.oxml.shared.qn("r:id"), + r_id, + ) # Create a w:r element - new_run = docx.oxml.shared.OxmlElement('w:r') + new_run = docx.oxml.shared.OxmlElement("w:r") # Create a new w:rPr element - rPr = docx.oxml.shared.OxmlElement('w:rPr') + rPr = docx.oxml.shared.OxmlElement("w:rPr") # Add color if it is given if not color is None: - c = docx.oxml.shared.OxmlElement('w:color') - c.set(docx.oxml.shared.qn('w:val'), color) - rPr.append(c) + c = docx.oxml.shared.OxmlElement("w:color") + c.set(docx.oxml.shared.qn("w:val"), color) + rPr.append(c) # Remove underlining if it is requested if not underline: - u = docx.oxml.shared.OxmlElement('w:u') - u.set(docx.oxml.shared.qn('w:val'), 'none') - rPr.append(u) + u = docx.oxml.shared.OxmlElement("w:u") + u.set(docx.oxml.shared.qn("w:val"), "none") + rPr.append(u) - u = docx.oxml.shared.OxmlElement('w:u') - u.set(docx.oxml.shared.qn('w:val'), 'single') + u = docx.oxml.shared.OxmlElement("w:u") + u.set(docx.oxml.shared.qn("w:val"), "single") rPr.append(u) # Join all the xml elements together add add the required text to the w:r element @@ -61,36 +66,37 @@ def add_hyperlink(paragraph, url, text, color='1111FF', underline=True): return hyperlink -if __name__ == '__main__': +if __name__ == "__main__": document = docx.Document() - document.add_heading('Hyperlink', level=0) + document.add_heading("Hyperlink", level=0) - p = document.add_paragraph('hyperlink: ') - add_hyperlink(p, 'https://google.ru', 'google') + p = document.add_paragraph("hyperlink: ") + add_hyperlink(p, "https://google.ru", "google") - p = document.add_paragraph('hyperlink: ') - add_hyperlink(p, 'https://google.ru', 'google', underline=False) + p = document.add_paragraph("hyperlink: ") + add_hyperlink(p, "https://google.ru", "google", underline=False) - p = document.add_paragraph('hyperlink: ') - add_hyperlink(p, 'https://google.ru', 'google', color='00FF00') + p = document.add_paragraph("hyperlink: ") + add_hyperlink(p, "https://google.ru", "google", color="00FF00") - p = document.add_paragraph('hyperlink: ') - add_hyperlink(p, 'https://google.ru', 'google', color='FF8822') + p = document.add_paragraph("hyperlink: ") + add_hyperlink(p, "https://google.ru", "google", color="FF8822") # Empty line document.add_paragraph() - p = document.add_paragraph('Hello World! -> ') - add_hyperlink(p, 'https://google.ru/search?q=Hello', 'Hello') - p.add_run(' ') - add_hyperlink(p, 'https://google.ru/search?q=World', 'World') - p.add_run(' -> ') - add_hyperlink(p, 'https://google.ru/search?q=Hello World!', 'Hello World!') + p = document.add_paragraph("Hello World! -> ") + add_hyperlink(p, "https://google.ru/search?q=Hello", "Hello") + p.add_run(" ") + add_hyperlink(p, "https://google.ru/search?q=World", "World") + p.add_run(" -> ") + add_hyperlink(p, "https://google.ru/search?q=Hello World!", "Hello World!") # Save - file_name_doc = 'word.docx' + file_name_doc = "word.docx" document.save(file_name_doc) # Open file import os + os.startfile(file_name_doc) diff --git a/office__word__doc_docx/read_docx/read_docx.py b/office__word__doc_docx/read_docx/read_docx.py index 83e6bfa5b..f517e961d 100644 --- a/office__word__doc_docx/read_docx/read_docx.py +++ b/office__word__doc_docx/read_docx/read_docx.py @@ -1,20 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import re + # pip install python-docx from docx import Document + + document = Document("Обеденное меню 777.docx") # Регулярка для поиска последовательностей пробелов: от двух подряд и более -import re -multi_space_pattern = re.compile(r'[ ]{2,}') +multi_space_pattern = re.compile(r" {2,}") for table in document.tables: for row in table.rows: - name, weight, price = [multi_space_pattern.sub(' ', i.text.strip()) for i in row.cells] + name, weight, price = [ + multi_space_pattern.sub(" ", i.text.strip()) for i in row.cells + ] if name == weight == price or (not weight or not price): print() @@ -22,7 +27,7 @@ print(name) continue - print('{} {} {}'.format(name, weight, price)) + print(f"{name} {weight} {price}") # Таблицы в меню дублируются break diff --git a/office__word__doc_docx/table__hyperlink_cell.py b/office__word__doc_docx/table__hyperlink_cell.py index c27c8a51c..6448fec24 100644 --- a/office__word__doc_docx/table__hyperlink_cell.py +++ b/office__word__doc_docx/table__hyperlink_cell.py @@ -1,33 +1,65 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import os + # pip install python-docx import docx from hyperlink import add_hyperlink -headers = ('NAME', 'DESCRIPTION') +headers = ("NAME", "DESCRIPTION") rows = [ - ('php', 'PHP — скриптовый язык программирования общего назначения, активно применяемый для разработки веб-приложений. Используйте эту метку, если у Вас возникли вопросы по применению данного языка или о самом языке.'), - ('javascript', 'JavaScript (не путать с Java) — динамический, интерпретируемый язык со слабой типизацией, обычно используемый для написания скриптов на стороне клиента. Эта метка предназначена для вопросов, связанных с ECMAScript, его различными диалектами и реализациями (за исключением ActionScript). Если нет меток, относящихся к фреймворкам, предполагается, что код в ответах также не должен требовать сторонних библиотек.'), - ('java', 'Java (не путать с JavaScript) — строго типизированный объектно-ориентированный язык программирования. Приложения Java обычно транслируются в специальный байт-код, поэтому они могут работать на любой компьютерной архитектуре ,с помощью виртуальной Java-машины (JVM). Используйте эту метку для вопросов, относящихся к языку Java или инструментам из платформы Java.'), - ('android', 'Android — это операционная система от Google, основанная на ядре Linux, для цифровых устройств: телефонов, планшетов, автомобилей, телевизоров, часов и очков Google Glass. Пожалуйста, используйте специфические для Android метки, например [android-intent], а не просто [intent].'), - ('c#', 'C# (произносится «си шарп») — мультипарадигменный язык программирования, флагманский язык фреймворка .NET. В основе его лежит объектно-ориентированный подход, но он поддерживает элементы функционального программирования, взаимодействие с COM, нативным кодом и скриптовыми языками. Язык и платформа .NET обладают огромной стандартной библиотекой, а также многочисленными фреймворками.'), - ('html', 'Стандартный язык разметки гипертекста, повсеместно применяемый в интернете. Последняя действующая версия спецификации HTML5.1. Как правило, используется совместно с шаблонизатором, серверными и клиентскими скриптами, каскадными таблицами стилей для динамической генерации удобочитаемых веб страниц из информации, хранящейся в БД сервера. '), - ('c++', 'C++ — язык программирования общего назначения, синтаксис которого основан на языке C.'), - ('jquery', 'Популярная JavaScript-библиотека. Метка применяетcя к вопросам использования JQuery и ее дополнений. Должна быть дополнена меткой [javascript].'), - ('css', 'CSS, или каскадные таблицы стилей, — формальный язык описания внешнего вида документа, язык стилей, определяющий отображение HTML-документов.'), - ('mysql', 'Популярная реляционная СУБД, полностью поддерживающая стандартный SQL. Используйте метку только для вопросов, специфических для MySQL, иначе используйте [sql].') + ( + "php", + "PHP — скриптовый язык программирования общего назначения, активно применяемый для разработки веб-приложений. Используйте эту метку, если у Вас возникли вопросы по применению данного языка или о самом языке.", + ), + ( + "javascript", + "JavaScript (не путать с Java) — динамический, интерпретируемый язык со слабой типизацией, обычно используемый для написания скриптов на стороне клиента. Эта метка предназначена для вопросов, связанных с ECMAScript, его различными диалектами и реализациями (за исключением ActionScript). Если нет меток, относящихся к фреймворкам, предполагается, что код в ответах также не должен требовать сторонних библиотек.", + ), + ( + "java", + "Java (не путать с JavaScript) — строго типизированный объектно-ориентированный язык программирования. Приложения Java обычно транслируются в специальный байт-код, поэтому они могут работать на любой компьютерной архитектуре ,с помощью виртуальной Java-машины (JVM). Используйте эту метку для вопросов, относящихся к языку Java или инструментам из платформы Java.", + ), + ( + "android", + "Android — это операционная система от Google, основанная на ядре Linux, для цифровых устройств: телефонов, планшетов, автомобилей, телевизоров, часов и очков Google Glass. Пожалуйста, используйте специфические для Android метки, например [android-intent], а не просто [intent].", + ), + ( + "c#", + "C# (произносится «си шарп») — мультипарадигменный язык программирования, флагманский язык фреймворка .NET. В основе его лежит объектно-ориентированный подход, но он поддерживает элементы функционального программирования, взаимодействие с COM, нативным кодом и скриптовыми языками. Язык и платформа .NET обладают огромной стандартной библиотекой, а также многочисленными фреймворками.", + ), + ( + "html", + "Стандартный язык разметки гипертекста, повсеместно применяемый в интернете. Последняя действующая версия спецификации HTML5.1. Как правило, используется совместно с шаблонизатором, серверными и клиентскими скриптами, каскадными таблицами стилей для динамической генерации удобочитаемых веб страниц из информации, хранящейся в БД сервера. ", + ), + ( + "c++", + "C++ — язык программирования общего назначения, синтаксис которого основан на языке C.", + ), + ( + "jquery", + "Популярная JavaScript-библиотека. Метка применяетcя к вопросам использования JQuery и ее дополнений. Должна быть дополнена меткой [javascript].", + ), + ( + "css", + "CSS, или каскадные таблицы стилей, — формальный язык описания внешнего вида документа, язык стилей, определяющий отображение HTML-документов.", + ), + ( + "mysql", + "Популярная реляционная СУБД, полностью поддерживающая стандартный SQL. Используйте метку только для вопросов, специфических для MySQL, иначе используйте [sql].", + ), ] document = docx.Document() -table = document.add_table(rows=1, cols=len(headers), style='Table Grid') +table = document.add_table(rows=1, cols=len(headers), style="Table Grid") heading_cells = table.rows[0].cells for i, value in enumerate(headers): @@ -42,13 +74,12 @@ p = cells[0].paragraphs[0] - add_hyperlink(p, "https://ru.stackoverflow.com/tags/{}/info".format(name), name) + add_hyperlink(p, f"https://ru.stackoverflow.com/tags/{name}/info", name) cells[1].text = description # Save -file_name_doc = 'word.docx' +file_name_doc = "word.docx" document.save(file_name_doc) # Open file -import os os.startfile(file_name_doc) diff --git a/office__word__doc_docx/table__merge_cells.py b/office__word__doc_docx/table__merge_cells.py index 6e5e3a969..ff316e499 100644 --- a/office__word__doc_docx/table__merge_cells.py +++ b/office__word__doc_docx/table__merge_cells.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import os + # pip install python-docx import docx @@ -11,14 +13,15 @@ COL_COUNT = 5 ROW_COUNT = 5 -HEADERS = ['TABLE HEADER', '', '', '', ''] +HEADERS = ["TABLE HEADER", "", "", "", ""] ROWS = [ - ['{}x{}'.format(i, j) for j in range(1, COL_COUNT + 1)] for i in range(1, ROW_COUNT + 1) + [f"{i}x{j}" for j in range(1, COL_COUNT + 1)] + for i in range(1, ROW_COUNT + 1) ] def fill_table(document, headers, rows): - table = document.add_table(rows=1, cols=len(headers), style='Table Grid') + table = document.add_table(rows=1, cols=len(headers), style="Table Grid") heading_cells = table.rows[0].cells for i, value in enumerate(headers): @@ -54,9 +57,8 @@ def fill_table(document, headers, rows): table_2.cell(2, 2).merge(table_2.cell(2, 4)) # Save -file_name_doc = 'word.docx' +file_name_doc = "word.docx" document.save(file_name_doc) # Open file -import os os.startfile(file_name_doc) diff --git a/office__word__doc_docx/table__set_size_column.py b/office__word__doc_docx/table__set_size_column.py index 663e61079..26a28347f 100644 --- a/office__word__doc_docx/table__set_size_column.py +++ b/office__word__doc_docx/table__set_size_column.py @@ -1,32 +1,64 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import os + # pip install python-docx import docx from docx.shared import Cm -headers = ('NAME', 'DESCRIPTION') +headers = ("NAME", "DESCRIPTION") rows = [ - ('php', 'PHP — скриптовый язык программирования общего назначения, активно применяемый для разработки веб-приложений. Используйте эту метку, если у Вас возникли вопросы по применению данного языка или о самом языке.'), - ('javascript', 'JavaScript (не путать с Java) — динамический, интерпретируемый язык со слабой типизацией, обычно используемый для написания скриптов на стороне клиента. Эта метка предназначена для вопросов, связанных с ECMAScript, его различными диалектами и реализациями (за исключением ActionScript). Если нет меток, относящихся к фреймворкам, предполагается, что код в ответах также не должен требовать сторонних библиотек.'), - ('java', 'Java (не путать с JavaScript) — строго типизированный объектно-ориентированный язык программирования. Приложения Java обычно транслируются в специальный байт-код, поэтому они могут работать на любой компьютерной архитектуре ,с помощью виртуальной Java-машины (JVM). Используйте эту метку для вопросов, относящихся к языку Java или инструментам из платформы Java.'), - ('android', 'Android — это операционная система от Google, основанная на ядре Linux, для цифровых устройств: телефонов, планшетов, автомобилей, телевизоров, часов и очков Google Glass. Пожалуйста, используйте специфические для Android метки, например [android-intent], а не просто [intent].'), - ('c#', 'C# (произносится «си шарп») — мультипарадигменный язык программирования, флагманский язык фреймворка .NET. В основе его лежит объектно-ориентированный подход, но он поддерживает элементы функционального программирования, взаимодействие с COM, нативным кодом и скриптовыми языками. Язык и платформа .NET обладают огромной стандартной библиотекой, а также многочисленными фреймворками.'), - ('html', 'Стандартный язык разметки гипертекста, повсеместно применяемый в интернете. Последняя действующая версия спецификации HTML5.1. Как правило, используется совместно с шаблонизатором, серверными и клиентскими скриптами, каскадными таблицами стилей для динамической генерации удобочитаемых веб страниц из информации, хранящейся в БД сервера. '), - ('c++', 'C++ — язык программирования общего назначения, синтаксис которого основан на языке C.'), - ('jquery', 'Популярная JavaScript-библиотека. Метка применяетcя к вопросам использования JQuery и ее дополнений. Должна быть дополнена меткой [javascript].'), - ('css', 'CSS, или каскадные таблицы стилей, — формальный язык описания внешнего вида документа, язык стилей, определяющий отображение HTML-документов.'), - ('mysql', 'Популярная реляционная СУБД, полностью поддерживающая стандартный SQL. Используйте метку только для вопросов, специфических для MySQL, иначе используйте [sql].') + ( + "php", + "PHP — скриптовый язык программирования общего назначения, активно применяемый для разработки веб-приложений. Используйте эту метку, если у Вас возникли вопросы по применению данного языка или о самом языке.", + ), + ( + "javascript", + "JavaScript (не путать с Java) — динамический, интерпретируемый язык со слабой типизацией, обычно используемый для написания скриптов на стороне клиента. Эта метка предназначена для вопросов, связанных с ECMAScript, его различными диалектами и реализациями (за исключением ActionScript). Если нет меток, относящихся к фреймворкам, предполагается, что код в ответах также не должен требовать сторонних библиотек.", + ), + ( + "java", + "Java (не путать с JavaScript) — строго типизированный объектно-ориентированный язык программирования. Приложения Java обычно транслируются в специальный байт-код, поэтому они могут работать на любой компьютерной архитектуре ,с помощью виртуальной Java-машины (JVM). Используйте эту метку для вопросов, относящихся к языку Java или инструментам из платформы Java.", + ), + ( + "android", + "Android — это операционная система от Google, основанная на ядре Linux, для цифровых устройств: телефонов, планшетов, автомобилей, телевизоров, часов и очков Google Glass. Пожалуйста, используйте специфические для Android метки, например [android-intent], а не просто [intent].", + ), + ( + "c#", + "C# (произносится «си шарп») — мультипарадигменный язык программирования, флагманский язык фреймворка .NET. В основе его лежит объектно-ориентированный подход, но он поддерживает элементы функционального программирования, взаимодействие с COM, нативным кодом и скриптовыми языками. Язык и платформа .NET обладают огромной стандартной библиотекой, а также многочисленными фреймворками.", + ), + ( + "html", + "Стандартный язык разметки гипертекста, повсеместно применяемый в интернете. Последняя действующая версия спецификации HTML5.1. Как правило, используется совместно с шаблонизатором, серверными и клиентскими скриптами, каскадными таблицами стилей для динамической генерации удобочитаемых веб страниц из информации, хранящейся в БД сервера. ", + ), + ( + "c++", + "C++ — язык программирования общего назначения, синтаксис которого основан на языке C.", + ), + ( + "jquery", + "Популярная JavaScript-библиотека. Метка применяетcя к вопросам использования JQuery и ее дополнений. Должна быть дополнена меткой [javascript].", + ), + ( + "css", + "CSS, или каскадные таблицы стилей, — формальный язык описания внешнего вида документа, язык стилей, определяющий отображение HTML-документов.", + ), + ( + "mysql", + "Популярная реляционная СУБД, полностью поддерживающая стандартный SQL. Используйте метку только для вопросов, специфических для MySQL, иначе используйте [sql].", + ), ] document = docx.Document() -table = document.add_table(rows=1, cols=len(headers), style='Table Grid') +table = document.add_table(rows=1, cols=len(headers), style="Table Grid") heading_cells = table.rows[0].cells for i, value in enumerate(headers): @@ -51,9 +83,8 @@ cell.width = size # Save -file_name_doc = 'word.docx' +file_name_doc = "word.docx" document.save(file_name_doc) # Open file -import os os.startfile(file_name_doc) diff --git a/office__word__doc_docx/table__split_cells.py b/office__word__doc_docx/table__split_cells.py index 2e7f44901..841757774 100644 --- a/office__word__doc_docx/table__split_cells.py +++ b/office__word__doc_docx/table__split_cells.py @@ -1,23 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import os + # pip install python-docx import docx -headers = ['VALUE'] -rows = [ - [''], - [''] -] +headers = ["VALUE"] +rows = [[""], [""]] document = docx.Document() -table = document.add_table(rows=1, cols=len(headers), style='Table Grid') +table = document.add_table(rows=1, cols=len(headers), style="Table Grid") heading_cells = table.rows[0].cells for i, value in enumerate(headers): @@ -34,21 +33,20 @@ # Row 1 sub_table = table.cell(1, 0).add_table(rows=1, cols=3) -sub_table.style = 'Table Grid' +sub_table.style = "Table Grid" for i, col in enumerate(sub_table.row_cells(0), 1): - col.text = str(2 ** i) + col.text = str(2**i) # Row 2 sub_table = table.cell(2, 0).add_table(rows=1, cols=3) for i, col in enumerate(sub_table.row_cells(0), 1): - col.text = str(2 ** i) + col.text = str(2**i) # Save -file_name_doc = 'word.docx' +file_name_doc = "word.docx" document.save(file_name_doc) # Open file -import os os.startfile(file_name_doc) diff --git a/online_anidub_com/get_video_list.py b/online_anidub_com/get_video_list.py index 0f1221e1f..5c29ae269 100644 --- a/online_anidub_com/get_video_list.py +++ b/online_anidub_com/get_video_list.py @@ -1,13 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys import re -from typing import List from pathlib import Path from bs4 import BeautifulSoup @@ -16,8 +15,9 @@ ROOT_DIR = Path(__file__).resolve().parent -sys.path.append(str(ROOT_DIR.parent / 'using_proxy')) +sys.path.append(str(ROOT_DIR.parent / "using_proxy")) import proxy_requests__upgraded + ProxyRequests = proxy_requests__upgraded.ProxyRequests @@ -27,28 +27,28 @@ def _get_title(el) -> str: title = el.get_text(strip=True) - return re.sub(r'\s{2,}', ' ', title) + return re.sub(r"\s{2,}", " ", title) -def search_video_list(text: str) -> List[str]: - url = 'https://online.anidub.com/index.php?do=search' +def search_video_list(text: str) -> list[str]: + url = "https://online.anidub.com/index.php?do=search" data = { - 'do': 'search', - 'subaction': 'search', - 'search_start': '0', - 'full_search': '0', - 'result_from': '1', - 'story': text, + "do": "search", + "subaction": "search", + "search_start": "0", + "full_search": "0", + "result_from": "1", + "story": text, } headers = { - 'Host': 'online.anidub.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', - 'Accept-Encoding': 'gzip, deflate', - 'Origin': 'https://online.anidub.com', - 'Referer': 'https://online.anidub.com/index.php?do=search', + "Host": "online.anidub.com", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3", + "Accept-Encoding": "gzip, deflate", + "Origin": "https://online.anidub.com", + "Referer": "https://online.anidub.com/index.php?do=search", } while True: @@ -57,24 +57,33 @@ def search_video_list(text: str) -> List[str]: content = rs.content - if b'Attention Required! | Cloudflare' in content \ - or b'Access denied | online.anidub.com used Cloudflare to restrict access' in content: - DEBUG_LOG and print('Fail! Cloudflare!') + if ( + b"Attention Required! | Cloudflare" in content + or b"Access denied | online.anidub.com used Cloudflare to restrict access" + in content + ): + DEBUG_LOG and print("Fail! Cloudflare!") continue break - root = BeautifulSoup(content, 'html.parser') - return [_get_title(a) for a in root.select('.th-title')] + root = BeautifulSoup(content, "html.parser") + return [_get_title(a) for a in root.select(".th-title")] -if __name__ == '__main__': - text = 'Моя геройская академия' +if __name__ == "__main__": + text = "Моя геройская академия" items = search_video_list(text) - print(f'Items ({len(items)}):') + print(f"Items ({len(items)}):") for x in items: - print(f' {x}') + print(f" {x}") import json - json.dump(items, open('video_list.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=4) + + json.dump( + items, + open("video_list.json", "w", encoding="utf-8"), + ensure_ascii=False, + indent=4, + ) diff --git a/open_tab_webbrowser/open_tab_webbrowser.py b/open_tab_webbrowser/open_tab_webbrowser.py index 5926507d2..da2a80285 100644 --- a/open_tab_webbrowser/open_tab_webbrowser.py +++ b/open_tab_webbrowser/open_tab_webbrowser.py @@ -1,11 +1,11 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" """Пример открытия вкладки в браузере.""" -if __name__ == '__main__': +if __name__ == "__main__": import webbrowser # Вернет True и откроет вкладку - webbrowser.open_new_tab("https://github.com/gil9red") \ No newline at end of file + webbrowser.open_new_tab("https://github.com/gil9red") diff --git a/opencv (cv2) vs pil (pillow)/main.py b/opencv (cv2) vs pil (pillow)/main.py index 4f5e51d98..9b80cc660 100644 --- a/opencv (cv2) vs pil (pillow)/main.py +++ b/opencv (cv2) vs pil (pillow)/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python @@ -9,93 +9,148 @@ import timeit -FILE_NAME = 'input.jpg' +FILE_NAME = "input.jpg" NUMBER = 100 -GLOBALS = { - 'FILE_NAME': FILE_NAME -} +GLOBALS = {"FILE_NAME": FILE_NAME} -print('Read image from file:') -print('PIL', - timeit.timeit("img = Image.open(FILE_NAME)", setup="from PIL import Image", number=NUMBER, globals=GLOBALS) +print("Read image from file:") +print( + "PIL", + timeit.timeit( + "img = Image.open(FILE_NAME)", + setup="from PIL import Image", + number=NUMBER, + globals=GLOBALS, + ), ) -print('CV2', - timeit.timeit("img = cv2.imread(FILE_NAME)", setup="import cv2", number=NUMBER, globals=GLOBALS) +print( + "CV2", + timeit.timeit( + "img = cv2.imread(FILE_NAME)", + setup="import cv2", + number=NUMBER, + globals=GLOBALS, + ), ) print() -print('Read image from file and get image bytes:') -print('PIL', timeit.timeit( - """\ +print("Read image from file and get image bytes:") +print( + "PIL", + timeit.timeit( + """\ img = Image.open(FILE_NAME) bytes_io = io.BytesIO() img.save(bytes_io, format='JPEG') img_data = bytes_io.getvalue() """, - setup="from PIL import Image\nimport io", number=NUMBER, globals=GLOBALS -)) -print('CV2', timeit.timeit( - """\ + setup="from PIL import Image\nimport io", + number=NUMBER, + globals=GLOBALS, + ), +) +print( + "CV2", + timeit.timeit( + """\ img = cv2.imread(FILE_NAME) img_data = cv2.imencode('.jpg', img)[1].tostring() """, - setup="import cv2\nimport io", number=NUMBER, globals=GLOBALS -)) + setup="import cv2\nimport io", + number=NUMBER, + globals=GLOBALS, + ), +) print() -print('Invert:') -print('PIL', timeit.timeit( - """\ +print("Invert:") +print( + "PIL", + timeit.timeit( + """\ img = Image.open(FILE_NAME) img_invert = ImageOps.invert(img) """, - setup="from PIL import Image, ImageOps", number=NUMBER, globals=GLOBALS -)) -print('CV2', timeit.timeit( - """\ + setup="from PIL import Image, ImageOps", + number=NUMBER, + globals=GLOBALS, + ), +) +print( + "CV2", + timeit.timeit( + """\ img = cv2.imread(FILE_NAME) img_invert = cv2.bitwise_not(img) """, - setup="import cv2", number=NUMBER, globals=GLOBALS -)) + setup="import cv2", + number=NUMBER, + globals=GLOBALS, + ), +) print() -print('Grayscale:') -print('PIL ', timeit.timeit( - """\ +print("Grayscale:") +print( + "PIL ", + timeit.timeit( + """\ img = Image.open(FILE_NAME) img_gray = ImageOps.grayscale(img) """, - setup="from PIL import Image, ImageOps", number=NUMBER, globals=GLOBALS -)) -print('CV2_1', timeit.timeit( - """\ + setup="from PIL import Image, ImageOps", + number=NUMBER, + globals=GLOBALS, + ), +) +print( + "CV2_1", + timeit.timeit( + """\ img = cv2.imread(FILE_NAME) img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) """, - setup="import cv2", number=NUMBER, globals=GLOBALS -)) -print('CV2_2', timeit.timeit( - """\ + setup="import cv2", + number=NUMBER, + globals=GLOBALS, + ), +) +print( + "CV2_2", + timeit.timeit( + """\ img_gray = cv2.imread(FILE_NAME, cv2.IMREAD_GRAYSCALE) """, - setup="import cv2", number=NUMBER, globals=GLOBALS -)) + setup="import cv2", + number=NUMBER, + globals=GLOBALS, + ), +) print() -print('GaussianBlur:') -print('PIL', timeit.timeit( - """\ +print("GaussianBlur:") +print( + "PIL", + timeit.timeit( + """\ img = Image.open(FILE_NAME) img_blur = img.filter(ImageFilter.GaussianBlur(2)) """, - setup="from PIL import Image, ImageFilter", number=NUMBER, globals=GLOBALS -)) -print('CV2', timeit.timeit( - """\ + setup="from PIL import Image, ImageFilter", + number=NUMBER, + globals=GLOBALS, + ), +) +print( + "CV2", + timeit.timeit( + """\ img = cv2.imread(FILE_NAME) img_blur = cv2.GaussianBlur(img, (15, 15), 0) """, - setup="import cv2", number=NUMBER, globals=GLOBALS -)) -print() \ No newline at end of file + setup="import cv2", + number=NUMBER, + globals=GLOBALS, + ), +) +print() diff --git a/opencv__Color_Detection_Tool/main.py b/opencv__Color_Detection_Tool/main.py index a96de84d0..2c70c1616 100644 --- a/opencv__Color_Detection_Tool/main.py +++ b/opencv__Color_Detection_Tool/main.py @@ -1,75 +1,109 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://itnotesblog.ru/note.php?id=272#sthash.e5tCuHk0.dpbs -from PyQt5 import Qt +import sys +import traceback + +from PyQt5.QtCore import Qt, QFileInfo, QSettings +from PyQt5.QtGui import QImage, qRgb, QColor, QPixmap, QPalette, QPainter, QPen +from PyQt5.QtWidgets import ( + QWidget, + QMessageBox, + QApplication, + QSlider, + QSpinBox, + QFileDialog, + QColorDialog, + QComboBox, + QRadioButton, + QDoubleSpinBox, + QCheckBox, +) from PyQt5 import uic # pip install opencv-python import cv2 + import numpy as np -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - Qt.QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions CONFIG_FILE_NAME = "config.ini" -WINDOW_TITLE = 'Color Detection Tool' -GRAY_COLOR_TABLE = [Qt.qRgb(i, i, i) for i in range(256)] +WINDOW_TITLE = "Color Detection Tool" +GRAY_COLOR_TABLE = [qRgb(i, i, i) for i in range(256)] -def numpy_array_to_QImage(numpy_array): +def numpy_array_to_QImage(numpy_array: np.ndarray) -> QImage | None: if numpy_array.dtype != np.uint8: return height, width = numpy_array.shape[:2] if len(numpy_array.shape) == 2: - img = Qt.QImage(numpy_array.data, width, height, numpy_array.strides[0], Qt.QImage.Format_Indexed8) + img = QImage( + numpy_array.data, + width, + height, + numpy_array.strides[0], + QImage.Format_Indexed8, + ) img.setColorTable(GRAY_COLOR_TABLE) return img elif len(numpy_array.shape) == 3: if numpy_array.shape[2] == 3: - img = Qt.QImage(numpy_array.data, width, height, numpy_array.strides[0], Qt.QImage.Format_RGB888) + img = QImage( + numpy_array.data, + width, + height, + numpy_array.strides[0], + QImage.Format_RGB888, + ) return img elif numpy_array.shape[2] == 4: - img = Qt.QImage(numpy_array.data, width, height, numpy_array.strides[0], Qt.QImage.Format_ARGB32) + img = QImage( + numpy_array.data, + width, + height, + numpy_array.strides[0], + QImage.Format_ARGB32, + ) return img -class MainWindow(Qt.QWidget): - def __init__(self): +class MainWindow(QWidget): + def __init__(self) -> None: super().__init__() - uic.loadUi('mainwidget.ui', self) + uic.loadUi("mainwidget.ui", self) self.setWindowTitle(WINDOW_TITLE) - self.cbPenStyle.addItem('Solid', Qt.Qt.SolidLine) - self.cbPenStyle.addItem('Dash', Qt.Qt.DashLine) - self.cbPenStyle.addItem('Dot', Qt.Qt.DotLine) - self.cbPenStyle.addItem('Dash Dot', Qt.Qt.DashDotLine) - self.cbPenStyle.addItem('Dash Dot Dot', Qt.Qt.DashDotDotLine) + self.cbPenStyle.addItem("Solid", Qt.SolidLine) + self.cbPenStyle.addItem("Dash", Qt.DashLine) + self.cbPenStyle.addItem("Dot", Qt.DotLine) + self.cbPenStyle.addItem("Dash Dot", Qt.DashDotLine) + self.cbPenStyle.addItem("Dash Dot Dot", Qt.DashDotDotLine) - self.pen_color = Qt.QColor(Qt.Qt.green) + self.pen_color = QColor(Qt.green) self.last_load_path = "." self.image_source = None @@ -77,11 +111,11 @@ def __init__(self): self.load_settings() - for w in self.findChildren(Qt.QSlider): + for w in self.findChildren(QSlider): w.sliderMoved.connect(self.refresh_HSV) name = w.objectName() - sp = self.findChild(Qt.QSpinBox, "sp" + name[2:]) + sp = self.findChild(QSpinBox, "sp" + name[2:]) if not sp: continue @@ -108,51 +142,55 @@ def __init__(self): def on_load(self): image_filters = "Images (*.jpg *.jpeg *.png *.bmp)" - file_name = Qt.QFileDialog.getOpenFileName(self, "Load image", self.last_load_path, image_filters)[0] + file_name = QFileDialog.getOpenFileName( + self, "Load image", self.last_load_path, image_filters + )[0] if not file_name: return - self.last_load_path = Qt.QFileInfo(file_name).absolutePath() + self.last_load_path = QFileInfo(file_name).absolutePath() # Load image as bytes - with open(file_name, 'rb') as f: + with open(file_name, "rb") as f: img_data = f.read() nparr = np.frombuffer(img_data, np.uint8) self.image_source = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED) height, width, channels = self.image_source.shape - self.setWindowTitle(WINDOW_TITLE + '. {}x{} ({} channels). {}'.format(width, height, channels, file_name)) + self.setWindowTitle( + f"{WINDOW_TITLE}. {width}x{height} ({channels} channels). {file_name}" + ) # Трансформация BGR->RGB если 3 канала. У картинок с прозрачностью каналов 4 и для них почему if channels == 3: code = cv2.COLOR_BGR2RGB - elif channels == 4: code = cv2.COLOR_BGRA2RGB - else: - raise Exception('Unexpected number of channels: {}'.format(channels)) + raise Exception(f"Unexpected number of channels: {channels}") self.image_source = cv2.cvtColor(self.image_source, code) self.refresh_HSV() - def show_result(self): + def show_result(self) -> None: if not self.result_img: return size = self.lbView.size() - pixmap = Qt.QPixmap.fromImage(self.result_img).scaled(size, Qt.Qt.KeepAspectRatio, Qt.Qt.SmoothTransformation) + pixmap = QPixmap.fromImage(self.result_img).scaled( + size, Qt.KeepAspectRatio, Qt.SmoothTransformation + ) self.lbView.setPixmap(pixmap) - def _update_pen_color(self): + def _update_pen_color(self) -> None: palette = self.pbPenColor.palette() - palette.setColor(Qt.QPalette.Button, self.pen_color) + palette.setColor(QPalette.Button, self.pen_color) self.pbPenColor.setPalette(palette) - def _choose_color(self): - color = Qt.QColorDialog.getColor(self.pen_color) + def _choose_color(self) -> None: + color = QColorDialog.getColor(self.pen_color) if not color.isValid(): return @@ -161,13 +199,13 @@ def _choose_color(self): self._update_pen_color() self.refresh_HSV() - def _draw_contours(self, result_img, contours): + def _draw_contours(self, result_img, contours) -> None: line_size = self.sbPenWidth.value() line_type = self.cbPenStyle.currentData() line_color = self.pen_color - p = Qt.QPainter(result_img) - p.setPen(Qt.QPen(line_color, line_size, line_type)) + p = QPainter(result_img) + p.setPen(QPen(line_color, line_size, line_type)) for c in contours: x, y, width, height = cv2.boundingRect(c) @@ -175,7 +213,7 @@ def _draw_contours(self, result_img, contours): p.end() - def refresh_HSV(self): + def refresh_HSV(self) -> None: hue_from = self.slHueFrom.value() hue_to = max(hue_from, self.slHueTo.value()) @@ -188,17 +226,17 @@ def refresh_HSV(self): hsv_min = hue_from, saturation_from, value_from hsv_max = hue_to, saturation_to, value_to - color_hsv_min = Qt.QColor.fromHsv(*hsv_min) - color_hsv_max = Qt.QColor.fromHsv(*hsv_max) + color_hsv_min = QColor.fromHsv(*hsv_min) + color_hsv_max = QColor.fromHsv(*hsv_max) - self.label_hsv_from_text.setText(', '.join(map(str, hsv_min))) - self.label_hsv_to_text.setText(', '.join(map(str, hsv_max))) + self.label_hsv_from_text.setText(", ".join(map(str, hsv_min))) + self.label_hsv_to_text.setText(", ".join(map(str, hsv_max))) - pixmap = Qt.QPixmap(1, 1) + pixmap = QPixmap(1, 1) pixmap.fill(color_hsv_min) self.lbHsvMin.setPixmap(pixmap) - pixmap = Qt.QPixmap(1, 1) + pixmap = QPixmap(1, 1) pixmap.fill(color_hsv_max) self.lbHsvMax.setPixmap(pixmap) @@ -215,27 +253,23 @@ def refresh_HSV(self): thresholded_image = cv2.inRange( thresholded_image, np.array(hsv_min, np.uint8), - np.array(hsv_max, np.uint8) + np.array(hsv_max, np.uint8), ) # Убираем шум thresholded_image = cv2.erode( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) thresholded_image = cv2.dilate( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) # Замыкаем оставшиеся крупные объекты thresholded_image = cv2.dilate( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) thresholded_image = cv2.erode( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) if self.rbCanny.isChecked(): @@ -243,13 +277,15 @@ def refresh_HSV(self): thresholded_image = cv2.Canny(thresholded_image, 100, 50, 5) if self.rbResult.isChecked(): - mode = cv2.RETR_EXTERNAL if self.chOnlyExternal.isChecked() else cv2.RETR_TREE + mode = ( + cv2.RETR_EXTERNAL + if self.chOnlyExternal.isChecked() + else cv2.RETR_TREE + ) # Находим контуры contours, _ = cv2.findContours( - thresholded_image, - mode, - cv2.CHAIN_APPROX_SIMPLE + thresholded_image, mode, cv2.CHAIN_APPROX_SIMPLE ) result_img = self.image_source.copy() @@ -266,43 +302,43 @@ def refresh_HSV(self): self.show_result() - def save_settings(self): - settings = Qt.QSettings(CONFIG_FILE_NAME, Qt.QSettings.IniFormat) + def save_settings(self) -> None: + settings = QSettings(CONFIG_FILE_NAME, QSettings.IniFormat) settings.setValue(self.objectName(), self.saveGeometry()) # TODO: рефакторинг циклов - for w in self.findChildren(Qt.QComboBox): + for w in self.findChildren(QComboBox): name = w.objectName() settings.setValue(name, w.currentIndex()) - for w in self.findChildren(Qt.QRadioButton): + for w in self.findChildren(QRadioButton): name = w.objectName() settings.setValue(name, int(w.isChecked())) - for w in self.findChildren(Qt.QSlider): + for w in self.findChildren(QSlider): name = w.objectName() settings.setValue(name, w.value()) - for w in self.findChildren(Qt.QDoubleSpinBox): + for w in self.findChildren(QDoubleSpinBox): name = w.objectName() settings.setValue(name, w.value()) - for w in self.findChildren(Qt.QSpinBox): + for w in self.findChildren(QSpinBox): name = w.objectName() settings.setValue(name, w.value()) - for w in self.findChildren(Qt.QCheckBox): + for w in self.findChildren(QCheckBox): name = w.objectName() settings.setValue(name, int(w.isChecked())) - settings.setValue('PenColor', self.pen_color.name()) + settings.setValue("PenColor", self.pen_color.name()) settings.setValue("lastLoadPath", self.last_load_path) - def load_settings(self): - settings = Qt.QSettings(CONFIG_FILE_NAME, Qt.QSettings.IniFormat) + def load_settings(self) -> None: + settings = QSettings(CONFIG_FILE_NAME, QSettings.IniFormat) geometry = settings.value(self.objectName()) if geometry: @@ -310,59 +346,59 @@ def load_settings(self): self.last_load_path = settings.value("lastLoadPath", ".") - self.pen_color = Qt.QColor(settings.value('PenColor', Qt.Qt.green)) + self.pen_color = QColor(settings.value("PenColor", Qt.green)) # TODO: рефакторинг циклов - for w in self.findChildren(Qt.QComboBox): + for w in self.findChildren(QComboBox): name = w.objectName() value = int(settings.value(name, w.currentIndex())) w.setCurrentIndex(value) - for w in self.findChildren(Qt.QRadioButton): + for w in self.findChildren(QRadioButton): name = w.objectName() value = bool(int(settings.value(name, w.isChecked()))) w.setChecked(value) - for w in self.findChildren(Qt.QSpinBox): + for w in self.findChildren(QSpinBox): name = w.objectName() value = int(settings.value(name, w.value())) w.setValue(value) - for w in self.findChildren(Qt.QDoubleSpinBox): + for w in self.findChildren(QDoubleSpinBox): name = w.objectName() value = float(settings.value(name, w.value())) w.setValue(value) - for w in self.findChildren(Qt.QCheckBox): + for w in self.findChildren(QCheckBox): name = w.objectName() value = bool(int(settings.value(name, w.isChecked()))) w.setChecked(value) - for w in self.findChildren(Qt.QSlider): + for w in self.findChildren(QSlider): name = w.objectName() value = int(settings.value(name, w.value())) w.setValue(value) - sp = self.findChild(Qt.QSpinBox, "sp" + name[2:]) + sp = self.findChild(QSpinBox, "sp" + name[2:]) if sp: sp.setMinimum(w.minimum()) sp.setMaximum(w.maximum()) sp.setValue(w.value()) - def resizeEvent(self, e): + def resizeEvent(self, e) -> None: super().resizeEvent(e) self.show_result() - def closeEvent(self, e): + def closeEvent(self, e) -> None: self.save_settings() super().closeEvent(e) -if __name__ == '__main__': - app = Qt.QApplication([]) +if __name__ == "__main__": + app = QApplication([]) mw = MainWindow() mw.show() diff --git a/opencv__examples/Find center of contour/main.py b/opencv__examples/Find center of contour/main.py index bfd4ca705..cc3aaab0b 100644 --- a/opencv__examples/Find center of contour/main.py +++ b/opencv__examples/Find center of contour/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://www.pyimagesearch.com/2016/02/01/opencv-center-of-contour/ @@ -9,8 +9,9 @@ # pip install opencv-python import cv2 -img = cv2.imread('example.jpg') -cv2.imshow('img', img) + +img = cv2.imread("example.jpg") +cv2.imshow("img", img) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5, 5), 0) @@ -30,13 +31,21 @@ # Draw the contour and center of the shape on the image cv2.drawContours(img, [c], -1, (0, 255, 0), 2) cv2.circle(img, (cX, cY), 7, (255, 255, 255), -1) - cv2.putText(img, "center", (cX - 20, cY - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1) + cv2.putText( + img, + "center", + (cX - 20, cY - 20), + cv2.FONT_HERSHEY_SIMPLEX, + 0.5, + (255, 255, 255), + 1, + ) except ZeroDivisionError: pass # Show the image cv2.imshow("result", img) -cv2.imwrite('output.jpg', img) +cv2.imwrite("output.jpg", img) cv2.waitKey() diff --git a/opencv__examples/Finding books in images/main.py b/opencv__examples/Finding books in images/main.py index ae756c27e..d4dc3703a 100644 --- a/opencv__examples/Finding books in images/main.py +++ b/opencv__examples/Finding books in images/main.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://tproger.ru/translations/finding-books-python-opencv/ + # pip install opencv-python import cv2 + # Загрузка изображения image = cv2.imread("example.jpg") # cv2.imshow('image', image) @@ -56,10 +58,18 @@ cX = int(M["m10"] / M["m00"]) cY = int(M["m01"] / M["m00"]) - cv2.putText(image_result, str(total), (cX, cY), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 2) - -print("На картинке {0} книг(и)".format(total)) + cv2.putText( + image_result, + str(total), + (cX, cY), + cv2.FONT_HERSHEY_SIMPLEX, + 1.5, + (0, 255, 0), + 2, + ) + +print(f"На картинке {total} книг(и)") cv2.imwrite("output.jpg", image_result) -cv2.imshow('image_result', image_result) +cv2.imshow("image_result", image_result) cv2.waitKey() diff --git a/opencv__examples/decode_fourcc.py b/opencv__examples/decode_fourcc.py index 3e812021f..1ec804d51 100644 --- a/opencv__examples/decode_fourcc.py +++ b/opencv__examples/decode_fourcc.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def decode_fourcc(v) -> str: @@ -9,9 +9,9 @@ def decode_fourcc(v) -> str: return "".join(chr((v >> 8 * i) & 0xFF) for i in range(4)) -if __name__ == '__main__': - assert decode_fourcc(828601953) == 'avc1' +if __name__ == "__main__": + assert decode_fourcc(828601953) == "avc1" import cv2 - assert decode_fourcc(cv2.VideoWriter_fourcc(*'XVID')) == 'XVID' - assert decode_fourcc(cv2.VideoWriter_fourcc(*'avc1')) == 'avc1' + assert decode_fourcc(cv2.VideoWriter_fourcc(*"XVID")) == "XVID" + assert decode_fourcc(cv2.VideoWriter_fourcc(*"avc1")) == "avc1" diff --git a/opencv__examples/draw_overlay_current_datetime/main.py b/opencv__examples/draw_overlay_current_datetime/main.py index 25e6776db..cec6777b6 100644 --- a/opencv__examples/draw_overlay_current_datetime/main.py +++ b/opencv__examples/draw_overlay_current_datetime/main.py @@ -1,18 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt import sys # pip install opencv-python import cv2 + import numpy -def draw_overlay(img: numpy.ndarray, text: str, color_text=(255, 255, 255), max_text_height_percent=5): +def draw_overlay( + img: numpy.ndarray, text: str, color_text=(255, 255, 255), max_text_height_percent=5 +) -> None: h, w, _ = img.shape max_text_height = (h / 100) * max_text_height_percent @@ -33,25 +36,29 @@ def draw_overlay(img: numpy.ndarray, text: str, color_text=(255, 255, 255), max_ text_y = h - text_rect_h // 2 # cv2.LINE_AA -- Anti aliased line - cv2.putText(img, text, (0, text_y), font, scale, color_text, thickness, lineType=cv2.LINE_AA) + cv2.putText( + img, text, (0, text_y), font, scale, color_text, thickness, lineType=cv2.LINE_AA + ) def draw_overlay_current_datetime( - img: numpy.ndarray, datetime_fmt='%d/%m/%y %H:%M:%S', - color_text=(255, 255, 255), max_text_height_percent=5 -): - text = DT.datetime.now().strftime(datetime_fmt) + img: numpy.ndarray, + datetime_fmt="%d/%m/%y %H:%M:%S", + color_text=(255, 255, 255), + max_text_height_percent=5, +) -> None: + text = dt.datetime.now().strftime(datetime_fmt) draw_overlay(img, text, color_text, max_text_height_percent) -if __name__ == '__main__': - img = cv2.imread('example.jpg') +if __name__ == "__main__": + img = cv2.imread("../gaussian_blur/example.jpg") while True: img_overlay = img.copy() draw_overlay_current_datetime(img_overlay) - cv2.imshow('With overlay', img_overlay) + cv2.imshow("With overlay", img_overlay) if cv2.waitKey(25) == 27: # Esc to quit sys.exit() diff --git a/opencv__examples/find_objects/main.py b/opencv__examples/find_objects/main.py index 60c1bce2c..9367e2fdb 100644 --- a/opencv__examples/find_objects/main.py +++ b/opencv__examples/find_objects/main.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 -img = cv2.imread('image.png') -cv2.imshow('img', img) + + +img = cv2.imread("image.png") +cv2.imshow("img", img) gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) @@ -29,7 +31,7 @@ cv2.rectangle(img_with_rect, (x, y), (x + w, y + h), (0, 0, 255)) -cv2.imshow('img_with_rect', img_with_rect) +cv2.imshow("img_with_rect", img_with_rect) # # Draw img_with_rect_rect @@ -47,7 +49,7 @@ cv2.rectangle(img_with_rect_rect, (x, y), (x + w, y + h), (0, 0, 255)) -cv2.imshow('img_with_rect_rect', img_with_rect_rect) +cv2.imshow("img_with_rect_rect", img_with_rect_rect) cv2.waitKey() cv2.destroyAllWindows() diff --git a/opencv__examples/find_objects_by_color__hsv/main.py b/opencv__examples/find_objects_by_color__hsv/main.py index c31fdc80f..aaecb44b1 100644 --- a/opencv__examples/find_objects_by_color__hsv/main.py +++ b/opencv__examples/find_objects_by_color__hsv/main.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 + import numpy as np @@ -14,36 +15,28 @@ def find_contours(image_source_hsv, hsv_min, hsv_max): # Отфильтровываем только то, что нужно, по диапазону цветов thresholded_image = cv2.inRange( - thresholded_image, - np.array(hsv_min, np.uint8), - np.array(hsv_max, np.uint8) + thresholded_image, np.array(hsv_min, np.uint8), np.array(hsv_max, np.uint8) ) # Убираем шум thresholded_image = cv2.erode( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) thresholded_image = cv2.dilate( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) # Замыкаем оставшиеся крупные объекты thresholded_image = cv2.dilate( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) thresholded_image = cv2.erode( - thresholded_image, - cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + thresholded_image, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) ) # Находим контуры contours, _ = cv2.findContours( - thresholded_image, - cv2.RETR_EXTERNAL, - cv2.CHAIN_APPROX_SIMPLE + thresholded_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) return contours @@ -65,20 +58,20 @@ def draw_rect_contours(img, hsv_min, hsv_max): return img -img = cv2.imread('example.jpg') -cv2.imshow('img', img) +img = cv2.imread("example.jpg") +cv2.imshow("img", img) # Find yellow img_yellow = draw_rect_contours(img, hsv_min=(25, 113, 220), hsv_max=(66, 255, 255)) -cv2.imshow('img_yellow', img_yellow) +cv2.imshow("img_yellow", img_yellow) # Find green img_green = draw_rect_contours(img, hsv_min=(59, 128, 130), hsv_max=(99, 255, 255)) -cv2.imshow('img_green', img_green) +cv2.imshow("img_green", img_green) # Find red img_red = draw_rect_contours(img, hsv_min=(119, 159, 184), hsv_max=(179, 255, 255)) -cv2.imshow('img_red', img_red) +cv2.imshow("img_red", img_red) cv2.waitKey() cv2.destroyAllWindows() diff --git a/opencv__examples/gaussian_blur/main.py b/opencv__examples/gaussian_blur/main.py index 40fb48e5e..9f702f6cd 100644 --- a/opencv__examples/gaussian_blur/main.py +++ b/opencv__examples/gaussian_blur/main.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 -img = cv2.imread('example.jpg') -cv2.imshow('img', img) + +img = cv2.imread("example.jpg") +cv2.imshow("img", img) img_blur = cv2.GaussianBlur(img, (5, 5), 0) -cv2.imshow('img_blur', img_blur) +cv2.imshow("img_blur", img_blur) cv2.waitKey() diff --git a/opencv__examples/gray/main.py b/opencv__examples/gray/main.py index cbc02a822..0e8403ae9 100644 --- a/opencv__examples/gray/main.py +++ b/opencv__examples/gray/main.py @@ -1,20 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 -img = cv2.imread('example.jpg') -cv2.imshow('img', img) + +img = cv2.imread("example.jpg") +cv2.imshow("img", img) # Gray img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) -cv2.imshow('img_gray', img_gray) +cv2.imshow("img_gray", img_gray) -img_2 = cv2.imread('example.jpg', cv2.IMREAD_GRAYSCALE) -cv2.imshow('img_gray_2', img_2) +img_2 = cv2.imread("example.jpg", cv2.IMREAD_GRAYSCALE) +cv2.imshow("img_gray_2", img_2) cv2.waitKey() diff --git a/opencv__examples/hello.py b/opencv__examples/hello.py index 99bd4168b..b5f936443 100644 --- a/opencv__examples/hello.py +++ b/opencv__examples/hello.py @@ -1,15 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://docs.opencv.org/3.1.0/dc/da5/tutorial_py_drawing_functions.html + # pip install opencv-python import numpy as np + import cv2 + # Create a black image img = np.zeros((512, 512, 3), np.uint8) @@ -25,7 +28,7 @@ cv2.polylines(img, [pts], True, (0, 255, 255)) font = cv2.FONT_HERSHEY_SIMPLEX -cv2.putText(img, 'OpenCV', (10, 500), font, 4, (255, 255, 255), 2, cv2.LINE_AA) +cv2.putText(img, "OpenCV", (10, 500), font, 4, (255, 255, 255), 2, cv2.LINE_AA) cv2.imshow("Image", img) cv2.waitKey(10000) diff --git a/opencv__examples/image_to_bytes/main.py b/opencv__examples/image_to_bytes/main.py index 5f75ebec3..a0be39500 100644 --- a/opencv__examples/image_to_bytes/main.py +++ b/opencv__examples/image_to_bytes/main.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 -img = cv2.imread('example.jpg') + +img = cv2.imread("example.jpg") # cv2.imshow('img', img) -img_data = cv2.imencode('.jpg', img)[1].tostring() +img_data = cv2.imencode(".jpg", img)[1].tostring() print(len(img_data), img_data) diff --git a/opencv__examples/invert/main.py b/opencv__examples/invert/main.py index 2e065e90f..3877c609d 100644 --- a/opencv__examples/invert/main.py +++ b/opencv__examples/invert/main.py @@ -1,23 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 -img = cv2.imread('example.jpg') -cv2.imshow('img', img) + +img = cv2.imread("example.jpg") +cv2.imshow("img", img) # Invert img_invert = cv2.bitwise_not(img) -cv2.imshow('img_invert', img_invert) -cv2.imwrite('img_invert.jpg', img_invert) +cv2.imshow("img_invert", img_invert) +cv2.imwrite("img_invert.jpg", img_invert) # Gray img_invert_gray = cv2.cvtColor(img_invert, cv2.COLOR_BGR2GRAY) -cv2.imshow('img_invert_gray', img_invert_gray) -cv2.imwrite('img_invert_gray.jpg', img_invert_gray) +cv2.imshow("img_invert_gray", img_invert_gray) +cv2.imwrite("img_invert_gray.jpg", img_invert_gray) cv2.waitKey() diff --git a/opencv__examples/pil_image__to__opencv_image.py b/opencv__examples/pil_image__to__opencv_image.py index 377fa7e01..7badecb24 100644 --- a/opencv__examples/pil_image__to__opencv_image.py +++ b/opencv__examples/pil_image__to__opencv_image.py @@ -1,22 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# pip install opencv-python -# pip install Pillow - import pyautogui + +# pip install opencv-python import cv2 + import numpy + pil_image = pyautogui.screenshot(region=(200, 200, 200, 200)) print(pil_image.size) -pil_image.show('pil_image') +pil_image.show("pil_image") opencv_image = cv2.cvtColor(numpy.array(pil_image), cv2.COLOR_RGB2BGR) print(opencv_image.shape[:2]) -cv2.imshow('opencv_image', opencv_image) +cv2.imshow("opencv_image", opencv_image) cv2.waitKey() diff --git a/opencv__examples/remove_contours/main.py b/opencv__examples/remove_contours/main.py index 311fb174b..bc7de737e 100644 --- a/opencv__examples/remove_contours/main.py +++ b/opencv__examples/remove_contours/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://www.pyimagesearch.com/2015/02/09/removing-contours-image-using-python-opencv/ @@ -9,6 +9,7 @@ # pip install opencv-python import cv2 + import numpy as np diff --git a/opencv__examples/screenshot__full.py b/opencv__examples/screenshot__full.py index 49c2f12c8..546e993c6 100644 --- a/opencv__examples/screenshot__full.py +++ b/opencv__examples/screenshot__full.py @@ -1,16 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 + import numpy as np import pyautogui + img = pyautogui.screenshot() img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) -cv2.imshow('img', img) +cv2.imshow("img", img) cv2.waitKey() diff --git a/opencv__examples/screenshot__resize.py b/opencv__examples/screenshot__resize.py index 6daaef996..1ed1dbf02 100644 --- a/opencv__examples/screenshot__resize.py +++ b/opencv__examples/screenshot__resize.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 + import numpy as np import pyautogui @@ -13,6 +14,6 @@ img = pyautogui.screenshot() img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) img = cv2.resize(img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_CUBIC) -cv2.imshow('img', img) +cv2.imshow("img", img) cv2.waitKey() diff --git a/opencv__examples/screenshots_in_window.py b/opencv__examples/screenshots_in_window.py index 8c12bef7b..9c0229788 100644 --- a/opencv__examples/screenshots_in_window.py +++ b/opencv__examples/screenshots_in_window.py @@ -1,22 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/questions/926556/ import threading + import cv2 import numpy as np + from PIL import ImageGrab target_array = None -def go(): +def go() -> None: global target_array while True: if target_array is not None: @@ -28,7 +30,7 @@ def go(): break -if __name__ == '__main__': +if __name__ == "__main__": thread = threading.Thread(target=go) thread.start() diff --git a/opencv__examples/show_edges_Canny/main.py b/opencv__examples/show_edges_Canny/main.py index 2413a32d4..0c204c21a 100644 --- a/opencv__examples/show_edges_Canny/main.py +++ b/opencv__examples/show_edges_Canny/main.py @@ -1,23 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install opencv-python import cv2 -img = cv2.imread('example.jpg') -cv2.imshow('img', img) + +img = cv2.imread("example.jpg") +cv2.imshow("img", img) # Edges Canny edges = cv2.Canny(img, 50, 100) -cv2.imshow('Edges Canny', edges) -cv2.imwrite('edges_canny.jpg', edges) +cv2.imshow("Edges Canny", edges) +cv2.imwrite("edges_canny.jpg", edges) # Edges Canny Invert edges_invert = cv2.bitwise_not(edges) -cv2.imshow('Edges Canny Invert', edges_invert) -cv2.imwrite('edges_canny_invert.jpg', edges_invert) +cv2.imshow("Edges Canny Invert", edges_invert) +cv2.imwrite("edges_canny_invert.jpg", edges_invert) cv2.waitKey() diff --git a/opencv__examples/show_fullscreen_window.py b/opencv__examples/show_fullscreen_window.py new file mode 100644 index 000000000..ef7738d25 --- /dev/null +++ b/opencv__examples/show_fullscreen_window.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +import cv2 + + +window_name = "window" +img = cv2.imread("gaussian_blur/example.jpg") + +while True: + cv2.namedWindow(window_name, cv2.WND_PROP_FULLSCREEN) + cv2.setWindowProperty(window_name, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN) + + cv2.imshow(window_name, img) + + if cv2.waitKey(25) == 27: # Esc to quit + sys.exit() diff --git a/opencv__examples/show_with_pyqt5.py b/opencv__examples/show_with_pyqt5.py index 3e4506550..b91b975a8 100644 --- a/opencv__examples/show_with_pyqt5.py +++ b/opencv__examples/show_with_pyqt5.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -16,12 +16,12 @@ class ThreadOpenCV(QThread): changePixmap = pyqtSignal(QImage) - def __init__(self, source): + def __init__(self, source) -> None: super().__init__() self.source = source - def run(self): + def run(self) -> None: # SOURCE: https://stackoverflow.com/a/44404713/5909792 cap = cv2.VideoCapture(self.source) while True: @@ -31,7 +31,9 @@ def run(self): rgbImage = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgbImage.shape bytesPerLine = ch * w - convertToQtFormat = QImage(rgbImage.data, w, h, bytesPerLine, QImage.Format_RGB888) + convertToQtFormat = QImage( + rgbImage.data, w, h, bytesPerLine, QImage.Format_RGB888 + ) p = convertToQtFormat.scaled(640, 480, Qt.KeepAspectRatio) self.changePixmap.emit(p) @@ -39,13 +41,13 @@ def run(self): class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.label_video = QLabel() self.label_video.setMinimumSize(600, 480) - self.pb_play = QPushButton('Play') + self.pb_play = QPushButton("Play") self.pb_play.clicked.connect(self.playVideo) self.thread = ThreadOpenCV("video.mp4") @@ -57,14 +59,14 @@ def __init__(self): self.setLayout(main_layout) - def playVideo(self): + def playVideo(self) -> None: self.thread.start() - def setImage(self, image): + def setImage(self, image) -> None: self.label_video.setPixmap(QPixmap.fromImage(image)) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) mw = Widget() diff --git a/ordered_json.py b/ordered_json.py index 017175816..5c9737043 100644 --- a/ordered_json.py +++ b/ordered_json.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/6921760/5909792 @@ -10,17 +10,18 @@ import json from collections import OrderedDict + json_data = {"foo": 1, "bar": 2, "abc": {"a": 1, "b": 2, "c": 3}} ordered_json_data = OrderedDict() -ordered_json_data['foo'] = 1 -ordered_json_data['bar'] = 2 -ordered_json_data['abc'] = OrderedDict() -ordered_json_data['abc']['a'] = 1 -ordered_json_data['abc']['b'] = 2 -ordered_json_data['abc']['c'] = 3 +ordered_json_data["foo"] = 1 +ordered_json_data["bar"] = 2 +ordered_json_data["abc"] = OrderedDict() +ordered_json_data["abc"]["a"] = 1 +ordered_json_data["abc"]["b"] = 2 +ordered_json_data["abc"]["c"] = 3 -print('Dumps:') +print("Dumps:") print(json.dumps(json_data, indent=4)) # { @@ -46,7 +47,7 @@ # } print() -print('Loads:') +print("Loads:") data = json.loads(ordered_json_str) print(json.dumps(data, indent=4)) diff --git a/pad__unpad__example.py b/pad__unpad__example.py index ce2422f06..0868339c7 100644 --- a/pad__unpad__example.py +++ b/pad__unpad__example.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def pad(s: bytes, bs=8) -> bytes: @@ -14,27 +14,28 @@ def unpad(s: bytes) -> bytes: return s[:-pad_size] -if __name__ == '__main__': - data = b'Hello' +if __name__ == "__main__": + data = b"Hello" padded_data = pad(data) - print(padded_data) # b'Hello\x03\x03\x03' - print(unpad(padded_data)) # b'Hello' - print(unpad(padded_data).decode('utf-8')) # Hello + print(padded_data) # b'Hello\x03\x03\x03' + print(unpad(padded_data)) # b'Hello' + print(unpad(padded_data).decode("utf-8")) # Hello assert data == unpad(pad(data)) print() - assert b'123' == unpad(pad(b'123')) - assert b'123' * 9999 == unpad(pad(b'123' * 9999)) - assert b'11111111' == unpad(pad(b'11111111')) - assert b'abcd123' == unpad(pad(b'abcd123')) + assert b"123" == unpad(pad(b"123")) + assert b"123" * 9999 == unpad(pad(b"123" * 9999)) + assert b"11111111" == unpad(pad(b"11111111")) + assert b"abcd123" == unpad(pad(b"abcd123")) - print(unpad(b'12\x02\x02')) # b'12' - print(unpad(b'1\x01')) # b'1' + print(unpad(b"12\x02\x02")) # b'12' + print(unpad(b"1\x01")) # b'1' print() - data = 'Привет!'.encode('utf-8') + data = "Привет!".encode("utf-8") padded_data = pad(data) - print(padded_data) # b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!\x03\x03\x03' + print(padded_data) + # b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!\x03\x03\x03' print(unpad(padded_data)) # b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!' - print(unpad(padded_data).decode('utf-8')) # Привет! + print(unpad(padded_data).decode("utf-8")) # Привет! assert data == unpad(pad(data)) diff --git a/paint with turtle/example.py b/paint with turtle/example.py index b6e92c848..d374a5370 100644 --- a/paint with turtle/example.py +++ b/paint with turtle/example.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import turtle -turtle.color('red', 'yellow') + + +turtle.color("red", "yellow") turtle.begin_fill() while True: diff --git a/pandas__examples/Analysis jira/download_jira_log.py b/pandas__examples/Analysis jira/download_jira_log.py index 4463bd26c..fab52a3f5 100644 --- a/pandas__examples/Analysis jira/download_jira_log.py +++ b/pandas__examples/Analysis jira/download_jira_log.py @@ -1,24 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pathlib -from urllib.parse import urlparse + from datetime import datetime +from urllib.parse import urlparse +from bs4 import BeautifulSoup -URL = 'https://jira.compassplus.ru/sr/jira.issueviews:searchrequest-xml/24381/SearchRequest-24381.xml?tempMax=1000' + +URL = "https://helpdesk.compassluxe.com/sr/jira.issueviews:searchrequest-xml/24381/SearchRequest-24381.xml?tempMax=1000" HEADERS = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0', + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0", } # NOTE. Get : openssl pkcs12 -nodes -out key.pem -in file.p12 -PEM_FILE_NAME = 'ipetrash.pem' +PEM_FILE_NAME = "ipetrash.pem" -FILE_NAME = 'jira_log__clear.xml' -FILE_NAME_FULL = 'jira_log.xml' +FILE_NAME = "jira_log__clear.xml" +FILE_NAME_FULL = "jira_log.xml" def generate_file_name(url: str) -> str: @@ -32,63 +35,64 @@ def generate_file_name(url: str) -> str: result = urlparse(url) path = pathlib.Path(result.path) - current_date_str = datetime.now().date().strftime('%Y-%m-%d') + current_date_str = datetime.now().date().strftime("%Y-%m-%d") - return f'{path.name}__{result.query}__{current_date_str}.xml' + return f"{path.name}__{result.query}__{current_date_str}.xml" def clear_jira_xml(xml_content: bytes) -> bytes: - xml_content = xml_content\ - .replace(b'TranzAxis', b'FooBar')\ - .replace(b'compassplus', b'foobar')\ - .replace(b'Compass Plus', b'Foo Bar') + xml_content = ( + xml_content.replace(b"TranzAxis", b"FooBar") + .replace(b"compassplus", b"foobar") + .replace(b"Compass Plus", b"Foo Bar") + ) - from bs4 import BeautifulSoup - root = BeautifulSoup(xml_content, 'html.parser') + root = BeautifulSoup(xml_content, "html.parser") - def mask_tag_content(css_selector, attr=None): + def mask_tag_content(css_selector, attr=None) -> None: for x in root.select(css_selector): - x.string = '...' + x.string = "..." if attr: - x[attr] = '...' + x[attr] = "..." - def delete_tag(css_selector): + def delete_tag(css_selector) -> None: for x in root.select(css_selector): x.decompose() - delete_tag('comments > comment') - delete_tag('attachments > attachment') - delete_tag('customfields') - delete_tag('issuelinks') - delete_tag('subtasks') - - mask_tag_content('[username]', 'username') - mask_tag_content('item > title') - mask_tag_content('item > summary') - mask_tag_content('item > description') - mask_tag_content('item > link') - mask_tag_content('item > key') - mask_tag_content('item > version') - mask_tag_content('item > fixversion') - mask_tag_content('item > component') - mask_tag_content('item > environment') - mask_tag_content('item > project', 'key') + delete_tag("comments > comment") + delete_tag("attachments > attachment") + delete_tag("customfields") + delete_tag("issuelinks") + delete_tag("subtasks") + + mask_tag_content("[username]", "username") + mask_tag_content("item > title") + mask_tag_content("item > summary") + mask_tag_content("item > description") + mask_tag_content("item > link") + mask_tag_content("item > key") + mask_tag_content("item > version") + mask_tag_content("item > fixversion") + mask_tag_content("item > component") + mask_tag_content("item > environment") + mask_tag_content("item > project", "key") # return root.prettify(encoding='utf-8') - return str(root).encode('utf-8') + return str(root).encode("utf-8") -if __name__ == '__main__': +if __name__ == "__main__": import requests + rs = requests.get(URL, headers=HEADERS, cert=PEM_FILE_NAME) print(rs) print(len(rs.text), repr(rs.text[:50])) # file_name = generate_file_name(URL) - with open(FILE_NAME_FULL, mode='wb') as f: + with open(FILE_NAME_FULL, mode="wb") as f: f.write(rs.content) - with open(FILE_NAME, mode='wb') as f: + with open(FILE_NAME, mode="wb") as f: f.write(clear_jira_xml(rs.content)) diff --git a/pandas__examples/Analysis jira/main.py b/pandas__examples/Analysis jira/main.py index 4591e2b11..39207e39c 100644 --- a/pandas__examples/Analysis jira/main.py +++ b/pandas__examples/Analysis jira/main.py @@ -1,21 +1,30 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from datetime import datetime -from download_jira_log import FILE_NAME -data_file = FILE_NAME +import matplotlib.pyplot as plt +from matplotlib.pyplot import FormatStrFormatter + +import pandas as pd from bs4 import BeautifulSoup -root = BeautifulSoup(open(data_file, mode='rb'), 'lxml') + +from download_jira_log import FILE_NAME + + +data_file = FILE_NAME +root = BeautifulSoup(open(data_file, mode="rb"), "lxml") records = [] for resolved in root.select("item > resolved"): # date local is en_US - resolved_date_time = datetime.strptime(resolved.text.strip(), '%a, %d %b %Y %H:%M:%S %z') + resolved_date_time = datetime.strptime( + resolved.text.strip(), "%a, %d %b %Y %H:%M:%S %z" + ) resolved_year = resolved_date_time.year resolved_year_month = datetime(resolved_date_time.year, resolved_date_time.month, 1) @@ -23,39 +32,39 @@ records.sort(key=lambda x: x[0]) -import pandas as pd -df = pd.DataFrame(data=records, columns=['resolved_date_time', 'resolved_year', 'resolved_year_month']) +df = pd.DataFrame( + data=records, columns=["resolved_date_time", "resolved_year", "resolved_year_month"] +) print(df) -print('Total rows:', len(df)) +print("Total rows:", len(df)) -df_month = pd.DataFrame({'count': df.groupby("resolved_year_month").size()}).reset_index() +df_month = pd.DataFrame( + {"count": df.groupby("resolved_year_month").size()} +).reset_index() print(df_month) print() -df_year = pd.DataFrame({'count': df.groupby("resolved_year").size()}).reset_index() +df_year = pd.DataFrame({"count": df.groupby("resolved_year").size()}).reset_index() print(df_year) -import matplotlib.pyplot as plt -from matplotlib.pyplot import FormatStrFormatter - fig = plt.figure(1) -fig.suptitle('Analysis jira', fontsize=14, fontweight='bold') +fig.suptitle("Analysis jira", fontsize=14, fontweight="bold") ax1 = fig.add_subplot(121) -ax1.plot(df_month['resolved_year_month'], df_month['count']) +ax1.plot(df_month["resolved_year_month"], df_month["count"]) ax1.grid() -ax1.set_title('Jira by month') -ax1.set_xlabel('Date') -ax1.set_ylabel('Count') +ax1.set_title("Jira by month") +ax1.set_xlabel("Date") +ax1.set_ylabel("Count") plt.gcf().autofmt_xdate() ax2 = fig.add_subplot(122) -ax2.plot(df_year['resolved_year'], df_year['count']) +ax2.plot(df_year["resolved_year"], df_year["count"]) ax2.ticklabel_format(useOffset=False) -ax2.xaxis.set_major_formatter(FormatStrFormatter('%d')) -ax2.set_title('Jira by year') -ax2.set_xlabel('Date') -ax2.set_ylabel('Count') +ax2.xaxis.set_major_formatter(FormatStrFormatter("%d")) +ax2.set_title("Jira by year") +ax2.set_xlabel("Date") +ax2.set_ylabel("Count") ax2.grid() plt.gcf().autofmt_xdate() diff --git a/pandas__examples/Analysis of account detail (html)/main.py b/pandas__examples/Analysis of account detail (html)/main.py index 9193eabf0..304685cb7 100644 --- a/pandas__examples/Analysis of account detail (html)/main.py +++ b/pandas__examples/Analysis of account detail (html)/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # TODO: сделать детализацию счета и заказать в html/excel @@ -9,11 +9,16 @@ # сделать обработку excel на pandas: Analysis of account detail (excel) import zipfile -with zipfile.ZipFile('Doc_df7c89c378c04e8daf69257ea95d9a2e.zip') as f: - data_file = f.read('Doc_df7c89c378c04e8daf69257ea95d9a2e.html') +from datetime import datetime +import pandas as pd from bs4 import BeautifulSoup -root = BeautifulSoup(data_file, 'lxml') + + +with zipfile.ZipFile("Doc_df7c89c378c04e8daf69257ea95d9a2e.zip") as f: + data_file = f.read("Doc_df7c89c378c04e8daf69257ea95d9a2e.html") + +root = BeautifulSoup(data_file, "lxml") records = [] for tr in root.select("table > tbody > tr"): @@ -27,59 +32,76 @@ # td_list[6].text, # 'Зона направления вызова/номер сессии' td_list[7].text, # 'Услуга' td_list[9].text, # 'Длительность/Объем (мин.:сек.)/(Kb)' - float(td_list[10].text.replace(',', '.')), # 'Стоимость руб. без НДС' + float(td_list[10].text.replace(",", ".")), # 'Стоимость руб. без НДС' ] records.append(record) columns = [ - 'Дата', 'Время', 'GMT', 'Номер', + "Дата", + "Время", + "GMT", + "Номер", # 'Зона вызова', 'Зона направления вызова/номер сессии', - 'Услуга', 'Длительность/Объем (мин.:сек.)/(Kb)', 'Стоимость руб. без НДС' + "Услуга", + "Длительность/Объем (мин.:сек.)/(Kb)", + "Стоимость руб. без НДС", ] -import pandas as pd df = pd.DataFrame(data=records, columns=columns) # print(df) -print('Total rows:', len(df)) +print("Total rows:", len(df)) -df_with_null_price = df[df['Стоимость руб. без НДС'] == 0] -print('With null price:', len(df_with_null_price)) +df_with_null_price = df[df["Стоимость руб. без НДС"] == 0] +print("With null price:", len(df_with_null_price)) -df_with_price = df[df['Стоимость руб. без НДС'] > 0] -print('With price:', len(df_with_price)) +df_with_price = df[df["Стоимость руб. без НДС"] > 0] +print("With price:", len(df_with_price)) -phone_list = sorted(set(df['Номер'].tolist())) -print('\nPhones ({}): {}'.format(len(phone_list), phone_list)) +phone_list = sorted(set(df["Номер"].tolist())) +print(f"\nPhones ({len(phone_list)}): {phone_list}") -print('\nShow target phone:') -df_dns_shop = df[df['Номер'].str.contains('sms:DNS-SHOP')] -print(' DNS-SHOP: number: {}, total price: {}'.format(len(df_dns_shop), df_dns_shop['Стоимость руб. без НДС'].sum())) +print("\nShow target phone:") +df_dns_shop = df[df["Номер"].str.contains("sms:DNS-SHOP")] +print( + f" DNS-SHOP: number: {len(df_dns_shop)}, total price: {df_dns_shop['Стоимость руб. без НДС'].sum()}" +) -df_maginfo = df[df['Номер'].str.contains('sms:Maginfo')] -print(' Maginfo: number: {}, total price: {}'.format(len(df_maginfo), df_maginfo['Стоимость руб. без НДС'].sum())) +df_maginfo = df[df["Номер"].str.contains("sms:Maginfo")] +print( + f" Maginfo: number: {len(df_maginfo)}, total price: {df_maginfo['Стоимость руб. без НДС'].sum()}" +) -df_sms_ru = df[df['Номер'].str.contains('sms:SMS.RU')] -print(' SMS.RU: number: {}, total price: {}'.format(len(df_sms_ru), df_sms_ru['Стоимость руб. без НДС'].sum())) +df_sms_ru = df[df["Номер"].str.contains("sms:SMS.RU")] +print( + f" SMS.RU: number: {len(df_sms_ru)}, total price: {df_sms_ru['Стоимость руб. без НДС'].sum()}" +) -print('\nPrint details (Maginfo):') +print("\nPrint details (Maginfo):") print(df_maginfo.to_string()) # Посчитать сумму за июнь, июль и август -print('\nPrice for months 06, 07 and 08:') -print(' ' + str(df[df['Дата'].str.contains('.06.')]['Стоимость руб. без НДС'].sum())) -print(' ' + str(df[df['Дата'].str.contains('.07.')]['Стоимость руб. без НДС'].sum())) -print(' ' + str(df[df['Дата'].str.contains('.08.')]['Стоимость руб. без НДС'].sum())) - -print('\nPrint details sorting by price (top 10):') -df_with_price_sorted = df_with_price.sort_values(by='Стоимость руб. без НДС', ascending=True).tail(10) +print("\nPrice for months 06, 07 and 08:") +print(" " + str(df[df["Дата"].str.contains(".06.")]["Стоимость руб. без НДС"].sum())) +print(" " + str(df[df["Дата"].str.contains(".07.")]["Стоимость руб. без НДС"].sum())) +print(" " + str(df[df["Дата"].str.contains(".08.")]["Стоимость руб. без НДС"].sum())) + +print("\nPrint details sorting by price (top 10):") +df_with_price_sorted = df_with_price.sort_values( + by="Стоимость руб. без НДС", ascending=True +).tail(10) print(df_with_price_sorted.to_string()) -df_by_phone = df_with_price[df_with_price['Номер'].str.contains('79510000000')] -print('\nPrint total price by phone:', df_by_phone['Стоимость руб. без НДС'].sum()) +df_by_phone = df_with_price[df_with_price["Номер"].str.contains("79510000000")] +print("\nPrint total price by phone:", df_by_phone["Стоимость руб. без НДС"].sum()) -df_months_06_07_08 = df_with_price[df_with_price['Дата'].str.contains('|'.join(['.06.', '.07.', '.08.']))] -print('\nTotal price for months 06, 07 and 08:', df_months_06_07_08['Стоимость руб. без НДС'].sum()) +df_months_06_07_08 = df_with_price[ + df_with_price["Дата"].str.contains("|".join([".06.", ".07.", ".08."])) +] +print( + "\nTotal price for months 06, 07 and 08:", + df_months_06_07_08["Стоимость руб. без НДС"].sum(), +) print(df_months_06_07_08.to_string()) # @@ -87,20 +109,24 @@ # показать. # Таблице нужно поле Итого # -print('\n\nPrint internet info:') -df_internet = df[df['Номер'] == 'internet.mts.ru'] +print("\n\nPrint internet info:") +df_internet = df[df["Номер"] == "internet.mts.ru"] -from datetime import datetime -data_list = sorted(set(df_internet['Дата'].tolist()), key=lambda data: datetime.strptime(data, '%d.%m.%Y')) +data_list = sorted( + set(df_internet["Дата"].tolist()), + key=lambda data: datetime.strptime(data, "%d.%m.%Y"), +) total_mb = 0 for data in data_list: - kb_list = df_internet[df_internet['Дата'] == data]['Длительность/Объем (мин.:сек.)/(Kb)'].tolist() - kb_list = [int(kb.replace('Kb', '')) for kb in kb_list] + kb_list = df_internet[df_internet["Дата"] == data][ + "Длительность/Объем (мин.:сек.)/(Kb)" + ].tolist() + kb_list = [int(kb.replace("Kb", "")) for kb in kb_list] sum_mb = sum(kb_list) // 1024 total_mb += sum_mb - print(data, sum_mb, 'MB') + print(data, sum_mb, "MB") -print('Total:', total_mb, 'MB') +print("Total:", total_mb, "MB") diff --git a/pandas__examples/json_to_csv.py b/pandas__examples/json_to_csv.py index 28905b570..80c4800f0 100644 --- a/pandas__examples/json_to_csv.py +++ b/pandas__examples/json_to_csv.py @@ -1,21 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import pandas as pd # SOURCE: https://ru.stackoverflow.com/a/846931/201445 json_data = { - 'items': [ - {'first_name': 'Оля', 'id': 111111, 'last_name': 'Сущенко'}, - {'first_name': 'Георгий', 'id': 222222, 'last_name': 'Голосов'}, - {'first_name': 'Максим', 'id': 333333, 'home_phone': '+79909999999', 'last_name': 'Тупиченков'} + "items": [ + {"first_name": "Оля", "id": 111111, "last_name": "Сущенко"}, + {"first_name": "Георгий", "id": 222222, "last_name": "Голосов"}, + { + "first_name": "Максим", + "id": 333333, + "home_phone": "+79909999999", + "last_name": "Тупиченков", + }, ] } - -import pandas as pd -df = pd.io.json.json_normalize(json_data['items']) +df = pd.io.json.json_normalize(json_data["items"]) print(df) -df.to_csv('out.csv', index=False) +df.to_csv("out.csv", index=False) diff --git a/pandas__examples/json_to_excel.py b/pandas__examples/json_to_excel.py index 0e3933d11..b4ffe815d 100644 --- a/pandas__examples/json_to_excel.py +++ b/pandas__examples/json_to_excel.py @@ -1,7 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import json +import pandas as pd json_str = """\ @@ -67,10 +71,9 @@ } """ -import json data = json.loads(json_str) # SOURCE: https://ru.stackoverflow.com/questions/671333 -import pandas as pd -df = pd.DataFrame(data['d']['Items']) -df.set_index('CouponCode')['TotalPrizeValue'].reset_index().to_excel('result.xlsx') + +df = pd.DataFrame(data["d"]["Items"]) +df.set_index("CouponCode")["TotalPrizeValue"].reset_index().to_excel("result.xlsx") diff --git a/pandas__examples/print_characters_tekken__who_were_in_all_series.py b/pandas__examples/print_characters_tekken__who_were_in_all_series.py index 75bcb071d..6f8f990af 100644 --- a/pandas__examples/print_characters_tekken__who_were_in_all_series.py +++ b/pandas__examples/print_characters_tekken__who_were_in_all_series.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from table_from__html_by_url__wiki__tekken import get_df @@ -10,8 +10,8 @@ df = get_df() # Столбцы-серии игр: ['Tekken', 'Tekken 2', 'Tekken 3', 'Tekken 4', 'Tekken 5', 'Tekken 6', 'Tekken 7'] -cols = [col for col in df.columns if col.startswith('Tekken')] +cols = [col for col in df.columns if col.startswith("Tekken")] for index, row in df.iterrows(): - if all(row[col] == 'Y' for col in cols): + if all(row[col] == "Y" for col in cols): print(row[0]) diff --git a/pandas__examples/process_txt_two_columns/main.py b/pandas__examples/process_txt_two_columns/main.py index 12d7f9459..2860aac5e 100644 --- a/pandas__examples/process_txt_two_columns/main.py +++ b/pandas__examples/process_txt_two_columns/main.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pandas as pd + df = pd.read_csv("001.txt", sep=":", header=None, dtype="str") for index, row in df.iterrows(): - row[1] = ''.join('1' if x == '0' else '0' for x in row[1].strip()) + row[1] = "".join("1" if x == "0" else "0" for x in row[1].strip()) df.to_csv("002.txt", index=None, header=None, sep=":") diff --git a/pandas__examples/show dataframe in QTableWidget.py b/pandas__examples/show dataframe in QTableWidget.py index ca82d2b8b..05dbc9130 100644 --- a/pandas__examples/show dataframe in QTableWidget.py +++ b/pandas__examples/show dataframe in QTableWidget.py @@ -1,26 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5.QtWidgets import * +import pandas as pd def generate_test_data(): records = [ - (1, 'Vasya', 16), - (2, 'Vasya2', 18), - (3, 'Vasya3', 34), - (4, 'Vasya4', 10), - (5, 'Vasya5', 19), + (1, "Vasya", 16), + (2, "Vasya2", 18), + (3, "Vasya3", 34), + (4, "Vasya4", 10), + (5, "Vasya5", 19), ] + return pd.DataFrame(data=records, columns=["ID", "NAME", "AGE"]) - import pandas as pd - return pd.DataFrame(data=records, columns=['ID', 'NAME', 'AGE']) +if __name__ == "__main__": + from PyQt5.QtWidgets import * -if __name__ == '__main__': # Подготовка данных df = generate_test_data() headers = df.columns.values.tolist() diff --git a/pandas__examples/table_from__html_by_url.py b/pandas__examples/table_from__html_by_url.py index d050ad142..2c3da99b3 100644 --- a/pandas__examples/table_from__html_by_url.py +++ b/pandas__examples/table_from__html_by_url.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pandas import pandas as pd -url = 'http://www.gks.ru/bgd/regl/b12_14p/IssWWW.exe/Stg/d01/05-02.htm' + +url = "http://www.gks.ru/bgd/regl/b12_14p/IssWWW.exe/Stg/d01/05-02.htm" df = pd.read_html(url, header=0, index_col=0)[0] print(df) -df.to_excel('table.xlsx') +df.to_excel("table.xlsx") diff --git a/pandas__examples/table_from__html_by_url__wiki__tekken.py b/pandas__examples/table_from__html_by_url__wiki__tekken.py index 7707a3eab..835780c2e 100644 --- a/pandas__examples/table_from__html_by_url__wiki__tekken.py +++ b/pandas__examples/table_from__html_by_url__wiki__tekken.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pandas @@ -15,21 +15,21 @@ def get_df(): - url = 'https://ru.wikipedia.org/wiki/Список_персонажей_Tekken' + url = "https://ru.wikipedia.org/wiki/Список_персонажей_Tekken" rs = requests.get(url) # Удаление сносок -- из-за них имена персонажей и значения присутствия в конкретной серии показывается # невалидно, например "Акума14", а не "Акума" - root = BeautifulSoup(rs.content, 'html.parser') - for sub in root.select('sup'): + root = BeautifulSoup(rs.content, "html.parser") + for sub in root.select("sup"): sub.decompose() df_list = pd.read_html(str(root), header=0) return df_list[0] -if __name__ == '__main__': +if __name__ == "__main__": df = get_df() print(df) - df.to_excel('tekken_table.xlsx', index=False) + df.to_excel("tekken_table.xlsx", index=False) diff --git a/parse__radix__CacheContent_log/main.py b/parse__radix__CacheContent_log/main.py deleted file mode 100644 index 7dd1a4543..000000000 --- a/parse__radix__CacheContent_log/main.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -__author__ = 'ipetrash' - - -import typing - - -def get_cache_object_info(file_name) -> typing.Dict[str, typing.Dict[str, int]]: - # new_object: Tran[180409000010273756] - # existing_object: Hold[13708] - - import re - pattern_object = re.compile('^(\w+_object): (.+)\[.+$') - - object_by_number = dict() - - for line in open(file_name): - match = pattern_object.search(line) - if match: - type_cache = match[1] - name = match[2] - - if type_cache not in object_by_number: - object_by_number[type_cache] = dict() - - if name not in object_by_number[type_cache]: - object_by_number[type_cache][name] = 0 - - object_by_number[type_cache][name] += 1 - - return object_by_number - - -def get_cache_existing_object_list(file_name, top_values=None) -> typing.List[typing.Tuple[str, int]]: - object_by_number = get_cache_object_info(file_name) - - existing_object_list = sorted(object_by_number['existing_object'].items(), key=lambda x: x[1], reverse=True) - if top_values: - existing_object_list = existing_object_list[:top_values] - - rows = [(name, number) for name, number in existing_object_list] - return rows - - -if __name__ == '__main__': - file_name = 'CacheContent_log.txt' - print(file_name) - - rows = get_cache_existing_object_list(file_name, top_values=5) - - # pip install tabulate - from tabulate import tabulate - print(tabulate(rows, headers=('NAME', 'NUMBER'), tablefmt="grid")) - print() diff --git a/parse_command_with_phone__doska.ru.py b/parse_command_with_phone__doska.ru.py index cbfe4d54b..362dd0652 100644 --- a/parse_command_with_phone__doska.ru.py +++ b/parse_command_with_phone__doska.ru.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -20,21 +20,22 @@ """ -def _ph_dec(g, r, k): - from base64 import b64decode - decode_base64 = b64decode(g).decode('utf-8') +import re +from base64 import b64decode + - import re - g = re.sub(r'%([a-fA-F0-9]{2})', lambda m: chr(int(m.group(1), 16)), decode_base64) +def _ph_dec(g, r, k): + decode_base64 = b64decode(g).decode("utf-8") + g = re.sub(r"%([a-fA-F0-9]{2})", lambda m: chr(int(m.group(1), 16)), decode_base64) n = len(r) d = len(g) - c = '' + c = "" f = 0 while f < d: - q = g[f: f + 1] - p = r[f % n: f % n + 1] + q = g[f : f + 1] + p = r[f % n : f % n + 1] if k == 1: q = ord(q[0]) - ord(p[0]) @@ -60,44 +61,56 @@ def gpzd(data, key): def get_phones_from_command(cmd): phones = list() - cmd = cmd.split('\t') + cmd = cmd.split("\t") for i in cmd: - data = i.split('|') + data = i.split("|") b = gpzd(data[1], data[2]) - phone = _ph_dec(b, 'K0dbVwzGrpLa-wRs2', 2) + phone = _ph_dec(b, "K0dbVwzGrpLa-wRs2", 2) phones.append(phone) return phones -if __name__ == '__main__': - cmd = '1|JTg0JTkxbyU5NnVqJTdDJThGc2drWnR6JTkxWA==|22481814 ' \ - '2|JTg1JTk3cCU5NHZscyU5NHloeVlyJTdCJTk0aXIlN0UlOEUlNjAlODQlOTElN0MlNUR1aWclNjA=|31950540 ' \ - '3|JTg1JTk0cCU5NnVmJTg0JTk2dWhuJTVCdHolOEQlNjA=|44314626' +if __name__ == "__main__": + cmd = ( + "1|JTg0JTkxbyU5NnVqJTdDJThGc2drWnR6JTkxWA==|22481814 " + "2|JTg1JTk3cCU5NHZscyU5NHloeVlyJTdCJTk0aXIlN0UlOEUlNjAlODQlOTElN0MlNUR1aWclNjA=|31950540 " + "3|JTg1JTk0cCU5NnVmJTg0JTk2dWhuJTVCdHolOEQlNjA=|44314626" + ) print(get_phones_from_command(cmd)) # ['-00-00', '11-822-14', '-00-00'] - cmd = '1|JTg3JTkyJTdDJTk2dmZxJTk2c2hwWHQlN0IlOEVmdHglOEUlNUIlODQlN0QlN0NXciVBMWElNjA=|23649509 ' \ - '2|JThBJTk1JTdEJTk0dGtvJTk3cWx3WnJ6bCU5QnV2JTkyJTVFJTg2a3olNUN0bF9k|95446977 ' \ - '3|JTg1JTkybyU5N3pvJTgzJTkycWhsWnUlN0YlOTZf|40841102' + cmd = ( + "1|JTg3JTkyJTdDJTk2dmZxJTk2c2hwWHQlN0IlOEVmdHglOEUlNUIlODQlN0QlN0NXciVBMWElNjA=|23649509 " + "2|JThBJTk1JTdEJTk0dGtvJTk3cWx3WnJ6bCU5QnV2JTkyJTVFJTg2a3olNUN0bF9k|95446977 " + "3|JTg1JTkybyU5N3pvJTgzJTkycWhsWnUlN0YlOTZf|40841102" + ) print(get_phones_from_command(cmd)) # ['94-711-03', '54-829-39', '-00-00'] - cmd = '1|JTg4V2slOTN1ZnYlOTV1aSU3QiU4RXVWdyU5NXglN0J2JTkycSU3QiU5MiU5OXMlN0QlOTFa|56764591 ' \ - '2|JThBJTk1dSU4RXlpcCU5NXFsbiU5NHBabiU4Rnh3cCU5NXd2JTk2JTk3bSU3RCU4RCU1Qw==|96021657 ' \ - '3|JTg4JThGbiU4RnZsJTgzJTkzcWtpWW0lN0IlOTNf|85533937' + cmd = ( + "1|JTg4V2slOTN1ZnYlOTV1aSU3QiU4RXVWdyU5NXglN0J2JTkycSU3QiU5MiU5OXMlN0QlOTFa|56764591 " + "2|JThBJTk1dSU4RXlpcCU5NXFsbiU5NHBabiU4Rnh3cCU5NXd2JTk2JTk3bSU3RCU4RCU1Qw==|96021657 " + "3|JTg4JThGbiU4RnZsJTgzJTkzcWtpWW0lN0IlOTNf|85533937" + ) print(get_phones_from_command(cmd)) # ['60-164-75', '51-632-25', '-00-00'] - cmd = '1|JTg3JTdDJTgwVXklOEV6JTVCbyU5RCU3RV9yJUExell0dXpreGclN0RadCU3RSU4RCU5MHglODB1ZQ==|46551687 ' \ - '2|JTg3ZyU3Q1QlN0MlOEV5JTVFcCU5RCU3Q1clN0RoeSU1RHQlOEQlN0NYJTdEeHlfciU3QiU5RSU4Rnp6bWc=|2094350 ' \ - '3|JTg0JTk2byU5NnVtJTdDJTkycm5sJTVFcCU3RCU4RSU1Qg==|3088581' + cmd = ( + "1|JTg3JTdDJTgwVXklOEV6JTVCbyU5RCU3RV9yJUExell0dXpreGclN0RadCU3RSU4RCU5MHglODB1ZQ==|46551687 " + "2|JTg3ZyU3Q1QlN0MlOEV5JTVFcCU5RCU3Q1clN0RoeSU1RHQlOEQlN0NYJTdEeHlfciU3QiU5RSU4Rnp6bWc=|2094350 " + "3|JTg0JTk2byU5NnVtJTdDJTkycm5sJTVFcCU3RCU4RSU1Qg==|3088581" + ) print(get_phones_from_command(cmd)) # ['49-397-09', '79-935-37', '-00-00'] - cmd = '1|JTg4JTkwayU5M3htJTdFJThGdWtqVnElN0QlOTRa|86770681 ' \ - '2|JTg5JTk2biU5MnZndiU5NndqJThFJTVCcCU3Q2glQTF0JTdDJTkxXyU4N3klN0NYdyU3QmVj|80583093 ' \ - '3|JTg3JTkycSU4RXFtJTdCJTk1d2psJTVDbHYlOTRX|74342308' + cmd = ( + "1|JTg4JTkwayU5M3htJTdFJThGdWtqVnElN0QlOTRa|86770681 " + "2|JTg5JTk2biU5MnZndiU5NndqJThFJTVCcCU3Q2glQTF0JTdDJTkxXyU4N3klN0NYdyU3QmVj|80583093 " + "3|JTg3JTkycSU4RXFtJTdCJTk1d2psJTVDbHYlOTRX|74342308" + ) print(get_phones_from_command(cmd)) # ['-00-00', '50-328-41', '-00-00'] - cmd = '1|JTg3JThFbyU4RXlpJTdGJThFc2poWmwlN0UlOTAlNUI=|67355029 ' \ - '2|JTg5cHclOTB2bnMlOTN1aiU4NFduJTdCJTk2aXF6JTkxXyU4NCU4RCU3Q193JTlFY2M=|80050871 ' \ - '3|JTg4JThFcyU5M3dvJTgwJTk3eWtoJTVFcSU3QyU5NiU1Qw==|84769395' + cmd = ( + "1|JTg3JThFbyU4RXlpJTdGJThFc2poWmwlN0UlOTAlNUI=|67355029 " + "2|JTg5cHclOTB2bnMlOTN1aiU4NFduJTdCJTk2aXF6JTkxXyU4NCU4RCU3Q193JTlFY2M=|80050871 " + "3|JTg4JThFcyU5M3dvJTgwJTk3eWtoJTVFcSU3QyU5NiU1Qw==|84769395" + ) print(get_phones_from_command(cmd)) # ['-00-00', '33-212-58', '-00-00'] diff --git a/parse_en_ru_words/iturizmo.ru_razgovorniki_russko-anglijskij-razgovornik/main.py b/parse_en_ru_words/iturizmo.ru_razgovorniki_russko-anglijskij-razgovornik/main.py deleted file mode 100644 index d6a32d714..000000000 --- a/parse_en_ru_words/iturizmo.ru_razgovorniki_russko-anglijskij-razgovornik/main.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import requests -rs = requests.get('http://iturizmo.ru/razgovorniki/russko-anglijskij-razgovornik.html') - -from bs4 import BeautifulSoup -root = BeautifulSoup(rs.content, 'html.parser') - -en_ru_items = [] - -for tr in root.select(".entry-content tr"): - td_list = [td.text.strip() for td in tr.select('td')] - if not td_list: - continue - - ru, en = td_list[:2] - en_ru_items.append((en, ru)) - -print(len(en_ru_items), en_ru_items) - -import json -json.dump(en_ru_items, open('en_ru.json', 'w', encoding='utf-8'), indent=4, ensure_ascii=False) diff --git a/parse_en_ru_words/www.7english.ru_dictionary.php_id=2000_letter=all/main.py b/parse_en_ru_words/www.7english.ru_dictionary.php_id=2000_letter=all/main.py deleted file mode 100644 index 30875ca0a..000000000 --- a/parse_en_ru_words/www.7english.ru_dictionary.php_id=2000_letter=all/main.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import json - -import requests -from bs4 import BeautifulSoup - - -rs = requests.get('http://www.7english.ru/dictionary.php?id=2000&letter=all') -root = BeautifulSoup(rs.content, 'html.parser') - -en_ru_items = [] - -for tr in root.select('tr[onmouseover]'): - td_list = [td.text.strip() for td in tr.select('td')] - - # Количество ячеек в таблице со словами -- 9 - if len(td_list) != 9 or not td_list[1] or not td_list[5]: - continue - - en = td_list[1] - - # Русские слова могут быть перечислены через запятую 'ты, вы', - # а нам достаточно одного слова - # 'ты, вы' -> 'ты' - ru = td_list[5].split(', ')[0] - - en_ru_items.append((en, ru)) - -print(len(en_ru_items), en_ru_items) - -json.dump(en_ru_items, open('en_ru.json', 'w', encoding='utf-8'), indent=4, ensure_ascii=False) diff --git a/parse_games_mail_ru__future_hits.py b/parse_games_mail_ru__future_hits.py deleted file mode 100644 index 163a2aa0e..000000000 --- a/parse_games_mail_ru__future_hits.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def get_game_list(): - from urllib.parse import urljoin - - import requests - rs = requests.get('https://games.mail.ru/pc/games/future_hits/') - - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'lxml') - - items = [] - - # Перебор табличек с играми - for item in root.select('.b-pc__entities-item'): - a = item.select_one('.b-pc__entities-title > a') - title = a.text.strip() - - url = urljoin(rs.url, a['href']) - - description = item.select_one('.b-pc__entities-descr').text.strip() - img_url = item.select_one('.b-pc__entities-img')['src'] - release_date = item.select_one('.b-pc__entities-author').text.strip() - - items.append((title, description, release_date, url, img_url)) - - return items - - -if __name__ == '__main__': - game_list = get_game_list() - - def print_list(items): - for i, (title, description, release_date, url, img_url) in enumerate(items, 1): - print('{:2}. "{}" ({}): {} [{}]\n{}\n'.format(i, title, release_date, url, img_url, description)) - - # Full - print_list(game_list) - - # # First 5 - # new_game_list = game_list[:5] - # print_list(new_game_list) - # - # # Sorted by title - # new_game_list = sorted(game_list, key=lambda x: x[0]) - # print_list(new_game_list) - - # # Sorted by year, reverse - # import re - # get_year = lambda text: int(re.search('\d{4}', text).group()) - # new_game_list = sorted(game_list, key=lambda x: get_year(x[2]), reverse=True) - # print_list(new_game_list) diff --git a/parse_jira_logged_time/favicon.ico b/parse_jira_logged_time/favicon.ico deleted file mode 100644 index b5cb32db6..000000000 Binary files a/parse_jira_logged_time/favicon.ico and /dev/null differ diff --git a/parse_jira_logged_time/gui.py b/parse_jira_logged_time/gui.py deleted file mode 100644 index a4ef4d853..000000000 --- a/parse_jira_logged_time/gui.py +++ /dev/null @@ -1,269 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import json -import io -import sys -import traceback -import webbrowser - -from contextlib import redirect_stdout -from datetime import datetime - -from PyQt5.Qt import ( - QApplication, QMessageBox, QThread, pyqtSignal, QMainWindow, QPushButton, QCheckBox, QPlainTextEdit, - QVBoxLayout, QHBoxLayout, QTextOption, QTableWidget, QWidget, QSizePolicy, QSplitter, Qt, QTableWidgetItem, - QProgressDialog, QHeaderView, QSystemTrayIcon, QIcon, QEvent, QTimer -) - -from main import ( - DIR, get_rss_jira_log, parse_logged_dict, get_logged_list_by_now_utc_date, get_logged_total_seconds, - get_sorted_logged, seconds_to_str -) - - -def log_uncaught_exceptions(ex_cls, ex, tb): - text = f'{ex_cls.__name__}: {ex}:\n' - text += ''.join(traceback.format_tb(tb)) - - print(text) - QMessageBox.critical(None, 'Error', text) - sys.exit(1) - - -sys.excepthook = log_uncaught_exceptions - - -class RunFuncThread(QThread): - run_finished = pyqtSignal(object) - - def __init__(self, func): - super().__init__() - - self.func = func - - def run(self): - self.run_finished.emit(self.func()) - - -WINDOW_TITLE = 'parse_jira_logged_time' - - -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() - - self.setWindowTitle(WINDOW_TITLE) - - file_name = str(DIR / 'favicon.ico') - icon = QIcon(file_name) - - self.setWindowIcon(icon) - - self.tray = QSystemTrayIcon(icon) - self.tray.setToolTip(self.windowTitle()) - self.tray.activated.connect(self._on_tray_activated) - self.tray.show() - - self.logged_dict = dict() - - self.pb_refresh = QPushButton('REFRESH') - self.pb_refresh.clicked.connect(self.refresh) - - self.cb_show_log = QCheckBox() - self.cb_show_log.setChecked(True) - - self.log = QPlainTextEdit() - self.log.setReadOnly(True) - self.log.setWordWrapMode(QTextOption.NoWrap) - log_font = self.log.font() - log_font.setFamily('Courier New') - self.log.setFont(log_font) - - self.cb_show_log.clicked.connect(self.log.setVisible) - self.log.setVisible(self.cb_show_log.isChecked()) - - header_labels = ['DATE', 'TOTAL LOGGED TIME'] - self.table_logged = QTableWidget() - self.table_logged.setEditTriggers(QTableWidget.NoEditTriggers) - self.table_logged.setSelectionBehavior(QTableWidget.SelectRows) - self.table_logged.setSelectionMode(QTableWidget.SingleSelection) - self.table_logged.setColumnCount(len(header_labels)) - self.table_logged.setHorizontalHeaderLabels(header_labels) - self.table_logged.horizontalHeader().setStretchLastSection(True) - self.table_logged.itemClicked.connect(self._on_table_logged_item_clicked) - - header_labels = ['TIME', 'LOGGED', 'JIRA'] - self.table_logged_info = QTableWidget() - self.table_logged_info.setEditTriggers(QTableWidget.NoEditTriggers) - self.table_logged_info.setSelectionBehavior(QTableWidget.SelectRows) - self.table_logged_info.setSelectionMode(QTableWidget.SingleSelection) - self.table_logged_info.setColumnCount(len(header_labels)) - self.table_logged_info.setHorizontalHeaderLabels(header_labels) - self.table_logged_info.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) - self.table_logged_info.horizontalHeader().setStretchLastSection(True) - self.table_logged_info.itemDoubleClicked.connect(self._on_table_logged_info_item_double_clicked) - - main_layout = QVBoxLayout() - - central_widget = QWidget() - central_widget.setLayout(main_layout) - - self.setCentralWidget(central_widget) - - self.pb_refresh.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)) - - h_layout = QHBoxLayout() - h_layout.addWidget(self.pb_refresh) - h_layout.addWidget(self.cb_show_log) - - layout_table_widget = QVBoxLayout() - layout_table_widget.setContentsMargins(0, 0, 0, 0) - layout_table_widget.addWidget(self.table_logged) - layout_table_widget.addWidget(self.table_logged_info) - - table_widget = QWidget() - table_widget.setLayout(layout_table_widget) - - splitter = QSplitter(Qt.Horizontal) - splitter.addWidget(table_widget) - splitter.addWidget(self.log) - - main_layout.addLayout(h_layout) - main_layout.addWidget(splitter) - - def _fill_tables(self, xml_data: bytes): - buffer_io = io.StringIO() - try: - with redirect_stdout(buffer_io): - print(len(xml_data), repr(xml_data[:50])) - - # Структура документа -- xml - self.logged_dict = parse_logged_dict(xml_data) - print(self.logged_dict) - - if not self.logged_dict: - return - - print(json.dumps(self.logged_dict, indent=4, ensure_ascii=False)) - print() - - logged_list = get_logged_list_by_now_utc_date(self.logged_dict) - - logged_total_seconds = get_logged_total_seconds(logged_list) - logged_total_seconds_str = seconds_to_str(logged_total_seconds) - print('entry_logged_list:', logged_list) - print('today seconds:', logged_total_seconds) - print('today time:', logged_total_seconds_str) - print() - - # Для красоты выводим результат в табличном виде - lines = [] - - # Удаление строк таблицы - while self.table_logged.rowCount(): - self.table_logged.removeRow(0) - - for i, (date_str, logged_list) in enumerate(get_sorted_logged(self.logged_dict)): - total_seconds = get_logged_total_seconds(logged_list) - total_seconds_str = seconds_to_str(total_seconds) - row = date_str, total_seconds_str, total_seconds - lines.append(row) - - self.table_logged.setRowCount(self.table_logged.rowCount() + 1) - - self.table_logged.setItem(i, 0, QTableWidgetItem(date_str)) - - item = QTableWidgetItem(total_seconds_str) - item.setToolTip('Total seconds: {}'.format(total_seconds)) - self.table_logged.setItem(i, 1, item) - - self.table_logged.setCurrentCell(0, 0) - self.table_logged.setFocus() - self._on_table_logged_item_clicked(self.table_logged.currentItem()) - - # Список строк станет списком столбцов, у каждого столбца подсчитается максимальная длина - max_len_columns = [max(map(len, map(str, col))) for col in zip(*lines)] - - # Создание строки форматирования: [30, 14, 5] -> "{:<30} | {:<14} | {:<5}" - my_table_format = ' | '.join('{:<%s}' % max_len for max_len in max_len_columns) - - for line in lines: - print(my_table_format.format(*line)) - - finally: - text = buffer_io.getvalue() - self.log.setPlainText(text) - - print(text) - - def refresh(self): - progress_dialog = QProgressDialog(self) - - thread = RunFuncThread(func=get_rss_jira_log) - thread.run_finished.connect(self._fill_tables) - thread.run_finished.connect(progress_dialog.close) - thread.start() - - progress_dialog.setWindowTitle('Please wait...') - progress_dialog.setLabelText(progress_dialog.windowTitle()) - progress_dialog.setRange(0, 0) - progress_dialog.exec() - - self.setWindowTitle(WINDOW_TITLE + ". Last refresh date: " + datetime.now().strftime('%d/%m/%Y %H:%M:%S')) - - def _on_table_logged_item_clicked(self, item: QTableWidgetItem): - # Удаление строк таблицы - while self.table_logged_info.rowCount(): - self.table_logged_info.removeRow(0) - - row = item.row() - date_str = self.table_logged.item(row, 0).text() - logged_list = self.logged_dict[date_str] - logged_list = reversed(logged_list) - - for i, logged in enumerate(logged_list): - self.table_logged_info.setRowCount(self.table_logged_info.rowCount() + 1) - - self.table_logged_info.setItem(i, 0, QTableWidgetItem(logged['time'])) - self.table_logged_info.setItem(i, 1, QTableWidgetItem(logged['logged_human_time'])) - - item = QTableWidgetItem(logged['jira_id']) - item.setToolTip(logged['jira_title']) - self.table_logged_info.setItem(i, 2, item) - - def _on_table_logged_info_item_double_clicked(self, item: QTableWidgetItem): - row = item.row() - jira_id = self.table_logged_info.item(row, 2).text() - - url = 'https://jira.compassplus.ru/browse/' + jira_id - webbrowser.open(url) - - def _on_tray_activated(self, reason): - self.setVisible(not self.isVisible()) - - if self.isVisible(): - self.showNormal() - self.activateWindow() - - def changeEvent(self, event: QEvent): - if event.type() == QEvent.WindowStateChange: - # Если окно свернули - if self.isMinimized(): - # Прячем окно с панели задач - QTimer.singleShot(0, self.hide) - - -if __name__ == '__main__': - app = QApplication([]) - - mw = MainWindow() - mw.resize(1200, 800) - mw.show() - - mw.refresh() - - app.exec() diff --git a/parse_jira_logged_time/main.py b/parse_jira_logged_time/main.py deleted file mode 100644 index 16a7c28de..000000000 --- a/parse_jira_logged_time/main.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import re -import sys - -from collections import defaultdict -from datetime import datetime, timezone -from pathlib import Path -from typing import Dict, List, Tuple - -import requests -from bs4 import BeautifulSoup - -DIR = Path(__file__).resolve().parent - -sys.path.append(str(DIR.parent)) -from logged_human_time_to_seconds import logged_human_time_to_seconds -from seconds_to_str import seconds_to_str - - -URL = 'https://jira.compassplus.ru/activity?maxResults=100&streams=user+IS+ipetrash&os_authType=basic&title=undefined' -HEADERS = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0', -} - -# NOTE: Get : openssl pkcs12 -nodes -out ipetrash.pem -in ipetrash.p12 -PEM_FILE_NAME = str(DIR / 'ipetrash.pem') - - -# SOURCE: https://stackoverflow.com/a/13287083/5909792 -def utc_to_local(utc_dt: datetime) -> datetime: - return utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None) - - -def get_rss_jira_log() -> bytes: - rs = requests.get(URL, headers=HEADERS, cert=PEM_FILE_NAME) - rs.raise_for_status() - return rs.content - - -def get_logged_dict(root) -> Dict[str, List[Dict]]: - logged_dict = defaultdict(list) - - for entry in root.select('entry'): - # Ищем в строку с логированием - match = re.search("logged '(.+?)'", entry.text, flags=re.IGNORECASE) - if not match: - continue - - logged_human_time = match.group(1) - logged_seconds = logged_human_time_to_seconds(logged_human_time) - - jira_id = entry.object.title.text.strip() - jira_title = entry.object.summary.text.strip() - - entry_dt = datetime.strptime(entry.published.text, "%Y-%m-%dT%H:%M:%S.%fZ") - - # Переменная entry_dt имеет время в UTC, и желательно его привести в локальное время - entry_dt = utc_to_local(entry_dt) - - entry_date = entry_dt.date() - date_str = entry_date.strftime('%d/%m/%Y') - - logged_dict[date_str].append({ - 'date_time': entry_dt.strftime('%d/%m/%Y %H:%M:%S'), - 'date': entry_dt.strftime('%d/%m/%Y'), - 'time': entry_dt.strftime('%H:%M:%S'), - 'logged_human_time': logged_human_time, - 'logged_seconds': logged_seconds, - 'jira_id': jira_id, - 'jira_title': jira_title, - }) - - return logged_dict - - -def parse_logged_dict(xml_data: bytes) -> Dict[str, List[Dict]]: - # Структура документа -- xml - root = BeautifulSoup(xml_data, 'xml') - - return get_logged_dict(root) - - -def get_sorted_logged(date_str_by_logged_list: Dict, reverse=True) -> List[Tuple[str, List[Dict]]]: - sorted_items = date_str_by_logged_list.items() - sorted_items = sorted(sorted_items, key=lambda x: datetime.strptime(x[0], '%d/%m/%Y'), reverse=reverse) - - return list(sorted_items) - - -def get_logged_list_by_now_utc_date(date_str_by_entry_logged_list: Dict[str, List[Dict]]) -> List[Dict]: - current_utc_date_str = datetime.utcnow().strftime('%d/%m/%Y') - return date_str_by_entry_logged_list.get(current_utc_date_str, []) - - -def get_logged_total_seconds(entry_logged_list: List[Dict]) -> int: - return sum(entry['logged_seconds'] for entry in entry_logged_list) - - -if __name__ == '__main__': - xml_data = get_rss_jira_log() - print(len(xml_data), repr(xml_data[:50])) - - # open('rs.xml', 'wb').write(xml_data) - # xml_data = open('rs.xml', 'rb').read() - - # Структура документа -- xml - logged_dict = parse_logged_dict(xml_data) - print(logged_dict) - - import json - print(json.dumps(logged_dict, indent=4, ensure_ascii=False)) - print() - - logged_list = get_logged_list_by_now_utc_date(logged_dict) - logged_total_seconds = get_logged_total_seconds(logged_list) - print('entry_logged_list:', logged_list) - print('today seconds:', logged_total_seconds) - print('today time:', seconds_to_str(logged_total_seconds)) - print() - - # Для красоты выводим результат в табличном виде - lines = [] - for date_str, logged_list in get_sorted_logged(logged_dict): - total_seconds = get_logged_total_seconds(logged_list) - lines.append((date_str, total_seconds, seconds_to_str(total_seconds))) - - # Список строк станет списком столбцов, у каждого столбца подсчитается максимальная длина - max_len_columns = [max(map(len, map(str, col))) for col in zip(*lines)] - - # Создание строки форматирования: [30, 14, 5] -> "{:<30} | {:<14} | {:<5}" - my_table_format = ' | '.join('{:<%s}' % max_len for max_len in max_len_columns) - - for line in lines: - print(my_table_format.format(*line)) diff --git a/parse_magnet_links.py b/parse_magnet_links.py index 0a04b20ae..1e6a09090 100644 --- a/parse_magnet_links.py +++ b/parse_magnet_links.py @@ -1,29 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # https://en.wikipedia.org/wiki/Magnet_URI_scheme # https://ru.wikipedia.org/wiki/Magnet-ссылка + +from urllib.parse import urlparse, parse_qs + + def parse(url: str) -> dict: - from urllib.parse import urlparse, parse_qs result = urlparse(url) return parse_qs(result.query) -if __name__ == '__main__': +if __name__ == "__main__": + import json + links = [ - 'magnet:?xt=urn:btih:19270c27523f428d6b9d23c264e4a0ff57869275&dn=rutor.info_%D0%94%D0%BE%D0%BA%D1%82%D0%BE%D1%80+%D0%A8%D0%B0%D0%BD%D1%81+%2F+Chance+%5B02x01-04+%D0%B8%D0%B7+10%5D+%282017%29+WEB-DLRip+1080p+%7C+Profix+Media&tr=udp://opentor.org:2710&tr=udp://opentor.org:2710&tr=http://retracker.local/announce', - 'magnet:?xt=urn:btih:1ff5a51d2f65e7c2fa200ada0896075543398a73&dn=rutor.info_Heroes+%26+Generals+%5B12.10.17%5D+%282016%29+PC+%7C+Online-only&tr=udp://opentor.org:2710&tr=udp://opentor.org:2710&tr=http://retracker.local/announce', - 'magnet:?xt=urn:btih:08c3a30e88b38ccf6926c073ecd41f983cbf2b20&dn=rutor.info_%D0%A4%D0%B8%D0%B7%D1%80%D1%83%D0%BA+%5B04%D1%8501-10+%D0%B8%D0%B7+21%5D+%282017%29+HDTVRip-AVC+%D0%BE%D1%82+GeneralFilm&tr=udp://opentor.org:2710&tr=udp://opentor.org:2710&tr=http://retracker.local/announce', - 'magnet:?xl=10826029&dn=mediawiki-1.15.1.tar.gz&xt=urn:sha1:XRX2PEFXOOEJFRVUCX6HMZMKS5TWG4K5&as=https%3A%2F%2Freleases.wikimedia.org%2Fmediawiki%2F1.15%2Fmediawiki-1.15.1.tar.gz', + "magnet:?xt=urn:btih:19270c27523f428d6b9d23c264e4a0ff57869275&dn=rutor.info_%D0%94%D0%BE%D0%BA%D1%82%D0%BE%D1%80+%D0%A8%D0%B0%D0%BD%D1%81+%2F+Chance+%5B02x01-04+%D0%B8%D0%B7+10%5D+%282017%29+WEB-DLRip+1080p+%7C+Profix+Media&tr=udp://opentor.org:2710&tr=udp://opentor.org:2710&tr=http://retracker.local/announce", + "magnet:?xt=urn:btih:1ff5a51d2f65e7c2fa200ada0896075543398a73&dn=rutor.info_Heroes+%26+Generals+%5B12.10.17%5D+%282016%29+PC+%7C+Online-only&tr=udp://opentor.org:2710&tr=udp://opentor.org:2710&tr=http://retracker.local/announce", + "magnet:?xt=urn:btih:08c3a30e88b38ccf6926c073ecd41f983cbf2b20&dn=rutor.info_%D0%A4%D0%B8%D0%B7%D1%80%D1%83%D0%BA+%5B04%D1%8501-10+%D0%B8%D0%B7+21%5D+%282017%29+HDTVRip-AVC+%D0%BE%D1%82+GeneralFilm&tr=udp://opentor.org:2710&tr=udp://opentor.org:2710&tr=http://retracker.local/announce", + "magnet:?xl=10826029&dn=mediawiki-1.15.1.tar.gz&xt=urn:sha1:XRX2PEFXOOEJFRVUCX6HMZMKS5TWG4K5&as=https%3A%2F%2Freleases.wikimedia.org%2Fmediawiki%2F1.15%2Fmediawiki-1.15.1.tar.gz", ] - import json pretty = lambda data: json.dumps(data, indent=4, ensure_ascii=False) for i, link in enumerate(links, 1): data = parse(link) - print('{}.\n{}\n'.format(i, pretty(data))) + print(f"{i}.\n{pretty(data)}\n") diff --git a/parse_molecule__Molecule to atoms.py b/parse_molecule__Molecule to atoms.py deleted file mode 100644 index 15cea451d..000000000 --- a/parse_molecule__Molecule to atoms.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -""" -My solution from codewars. - -""" - - -def get_sub_formula(formula): - """ - "(SO3)2" -> ("(SO3)2", "(SO3)", "2") - "(SO3)" -> ("(SO3)", "(SO3)", "") - - """ - - import re - match = re.search('(\([a-zA-Z\d]+\))(\d*)', formula) - if not match: - return - - full_sub_formula = match.group(0) - sub_formula = match.group(1) - multiplier = match.group(2) - - return full_sub_formula, sub_formula, multiplier - - -def split_by_full_tokens(formula): - """ - 'Fe7S10FBO3' -> ['Fe7', 'S10', 'F', 'B', 'O3'] - - """ - - import re - return re.findall('([A-Z][a-z]*\d*)', formula) - - -def split_by_tokens(formula): - """ - 'Fe7S10F1B1O3' -> ['Fe', '7', 'S', '10', 'F', '1', 'B', '1', 'O', '3'] - - """ - - import re - return re.findall('([A-Z][a-z]*|\d+)', formula) - - -def complete_element_atom_number_with_one_atom(formula): - """ - Append '1' for elements without atom number: 'FeK' -> 'Fe1K1' - - """ - - tokens = split_by_full_tokens(formula) - tokens = [token if token[-1] in '0123456789' else token + '1' for token in tokens] - return ''.join(tokens) - - -def parse_molecule(formula): - formula = formula.replace('[', '(').replace(']', ')') - formula = formula.replace('{', '(').replace('}', ')') - - while '(' in formula: - match = get_sub_formula(formula) - if not match: - break - - full_sub_formula, sub_formula, multiplier = match - new_sub_formula = complete_element_atom_number_with_one_atom(sub_formula) - - if multiplier: - multiplier = int(multiplier) - - tokens = split_by_tokens(new_sub_formula) - - # ['K', '4', 'O', '2'] * 2 -> ['K', '8', 'O', '4'] - tokens = [str(int(token) * multiplier) if token.isdigit() else token for token in tokens] - new_sub_formula = ''.join(tokens) - - formula = formula.replace(full_sub_formula, new_sub_formula) - - formula = complete_element_atom_number_with_one_atom(formula) - tokens = split_by_tokens(formula) - - from collections import defaultdict - element_by_number_atom = defaultdict(int) - - for i in range(0, len(tokens), 2): - element_by_number_atom[tokens[i]] += int(tokens[i + 1]) - - return element_by_number_atom - - -if __name__ == '__main__': - def equals_atomically(obj1, obj2): - if len(obj1) != len(obj2): - return False - for k in obj1: - if obj1[k] != obj2[k]: - return False - return True - - assert equals_atomically(parse_molecule("H2O"), {'H': 2, 'O': 1}), "Should parse water" - assert equals_atomically(parse_molecule("Mg(OH)2"), {'Mg': 1, 'O' : 2, 'H': 2}), "Should parse magnesium hydroxide: Mg(OH)2" - assert equals_atomically(parse_molecule("K4[ON(SO3)2]2"), {'K': 4, 'O': 14, 'N': 2, 'S': 4}), "Should parse Fremy's salt: K4[ON(SO3)2]2" diff --git a/parse_test_progress.py b/parse_test_progress.py index af2e0d2a0..306f1b0bd 100644 --- a/parse_test_progress.py +++ b/parse_test_progress.py @@ -1,10 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def parse(text, append_test_case_list=True): +import re + + +def parse(text, append_test_case_list=True) -> None: total_value = 0 total_max_value = 0 @@ -14,18 +17,17 @@ def parse(text, append_test_case_list=True): print(line) continue - import re - match = re.search(r'{@} +/ +(\d+)', line) + match = re.search(r"{@} +/ +(\d+)", line) if not match: print(line) continue max_value = int(match.group(1)) - match = re.search('{#}:(.*)$', line) + match = re.search("{#}:(.*)$", line) if match: # Получение списка значений - values = match.group(1).split(',') + values = match.group(1).split(",") # Удаление пустых символов values = map(str.strip, values) @@ -41,21 +43,23 @@ def parse(text, append_test_case_list=True): total_value += value total_max_value += max_value - line = line.replace('{@}', str(value)) - line = line.replace('{#}', "({}%)".format(int(value / max_value * 100))) + line = line.replace("{@}", str(value)) + line = line.replace("{#}", f"({int(value / max_value * 100)}%)") if not append_test_case_list: - index = line.rfind(':') + index = line.rfind(":") if index != -1: line = line[:index] print(line) print() - print("Итого: {} / {} ({}%)".format(total_value, total_max_value, int(total_value / total_max_value * 100))) + print( + f"Итого: {total_value} / {total_max_value} ({int(total_value / total_max_value * 100)}%)" + ) -if __name__ == '__main__': +if __name__ == "__main__": text = """\ Набивка тестов: ATM - VSDC Issuing: {@} / 48 {#}: diff --git a/parse_text_http_headers.py b/parse_text_http_headers.py index bafe092e8..7132c30ee 100644 --- a/parse_text_http_headers.py +++ b/parse_text_http_headers.py @@ -1,21 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re -from typing import Dict -HTTP_HEADER_PATTERN = re.compile(r'([\w-]+): (.*)', flags=re.IGNORECASE) +HTTP_HEADER_PATTERN = re.compile(r"([\w-]+): (.*)", flags=re.IGNORECASE) -def parse(text: str) -> Dict[str, str]: +def parse(text: str) -> dict[str, str]: return dict(HTTP_HEADER_PATTERN.findall(text)) -if __name__ == '__main__': +if __name__ == "__main__": text_http_headers = """ POST /index.php?do=search HTTP/1.1 @@ -39,18 +38,18 @@ def parse(text: str) -> Dict[str, str]: headers = parse(text_http_headers) print(headers) assert headers == { - 'Host': 'online.anidub.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', - 'Accept-Encoding': 'gzip, deflate, br', - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': '112', - 'Origin': 'https://online.anidub.com', - 'Connection': 'keep-alive', - 'Referer': 'https://online.anidub.com/index.php?do=search', - 'Upgrade-Insecure-Requests': '1', - 'TE': 'Trailers', - 'Pragma': 'no-cache', - 'Cache-Control': 'no-cache' + "Host": "online.anidub.com", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3", + "Accept-Encoding": "gzip, deflate, br", + "Content-Type": "application/x-www-form-urlencoded", + "Content-Length": "112", + "Origin": "https://online.anidub.com", + "Connection": "keep-alive", + "Referer": "https://online.anidub.com/index.php?do=search", + "Upgrade-Insecure-Requests": "1", + "TE": "Trailers", + "Pragma": "no-cache", + "Cache-Control": "no-cache", } diff --git a/parse_torrent_sites/rutor/download_torrent_file_from_torrent_url.py b/parse_torrent_sites/rutor/download_torrent_file_from_torrent_url.py deleted file mode 100644 index 4dea2cb26..000000000 --- a/parse_torrent_sites/rutor/download_torrent_file_from_torrent_url.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def download_torrent_file(torrent_file_url): - """ - Функция скачает по ссылке торрент файл и вернет его название. - Если не получится, вернет None - - """ - - user_agent = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' - - import requests - rs = requests.get(torrent_file_url, headers={'User-Agent': user_agent}) - if not rs.ok: - print('Не получилось скачать: {}\n\n{}'.format(rs.status_code, rs.text)) - return - - # Теперь нужно вытащить название торрент-файла - file_name = rs.headers['Content-Disposition'] - file_name = file_name.replace('attachment; filename=', '').replace('"', '') - - with open(file_name, 'wb') as f: - f.write(rs.text.encode()) - - return file_name - - -import re -GET_MINIMAL_TORRENT_URL = re.compile(r'(https?://.+/torrent/\d+/?)') - - -def get_download_url_from_torrent_url(torrent_url): - """ - Функция по ссылке на торрент вернет ссылку на торрент-файл. - Если не получится, вернется None. - - """ - - match = GET_MINIMAL_TORRENT_URL.search(torrent_url) - if match: - torrent_url = match.group(1) - download_torrent_url = torrent_url.replace('/torrent/', '/download/') - return download_torrent_url - - -if __name__ == '__main__': - torrent_url = 'http://anti-tor.org/torrent/539888/7-days-to-die-v-15.1-2013-pc-repack-ot-pioneer' - torrent_file_url = get_download_url_from_torrent_url(torrent_url) - file_name = download_torrent_file(torrent_file_url) - print(torrent_file_url, file_name) - - torrent_url = 'http://anti-tor.org/torrent/474477/hyperdimension-neptunia-rebirth2-sisters-generation-2015-pc-repack-ot-r.g-games' - torrent_file_url = get_download_url_from_torrent_url(torrent_url) - file_name = download_torrent_file(torrent_file_url) - print(torrent_file_url, file_name) - - torrent_url = 'http://anti-tor.org/torrent/543446/the-binding-of-isaac-rebirth-complete-bundle-v.1.??-2014-pc-steam-rip-ot-letsrlay' - torrent_file_url = get_download_url_from_torrent_url(torrent_url) - file_name = download_torrent_file(torrent_file_url) - print(torrent_file_url, file_name) diff --git a/parse_torrent_sites/rutor/get_info_from_torrent_page.py b/parse_torrent_sites/rutor/get_info_from_torrent_page.py deleted file mode 100644 index 494b789a5..000000000 --- a/parse_torrent_sites/rutor/get_info_from_torrent_page.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# TODO: парсинг страницы торрент, вытаскивание характеристик, описания, скриншотов, торрент-файла и магнет-ссылки - -from bs4 import BeautifulSoup -root = BeautifulSoup(open('1.htm', 'rb'), 'lxml') - -details = root.select_one('#details') -print(details.text.strip()) diff --git a/parse_torrent_sites/rutor/get_rutor_torrent_download_info.py b/parse_torrent_sites/rutor/get_rutor_torrent_download_info.py deleted file mode 100644 index 8e348f164..000000000 --- a/parse_torrent_sites/rutor/get_rutor_torrent_download_info.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def get_rutor_torrent_download_info(torrent_url): - """ - Parse torrent url and return tuple: (torrent_file_url, magnet_url, info_hash) - - """ - - import requests - rs = requests.get(torrent_url) - - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'lxml') - - magnet_url = root.select_one('#download > a[href^="magnet"]')['href'] - - # For get info hash from magnet url - import re - match = re.compile(r'btih:([abcdef\d]+?)&', flags=re.IGNORECASE).search(magnet_url) - if match: - info_hash = match.group(1) - - return torrent_url.replace('/torrent/', '/download/'), magnet_url, info_hash - - -if __name__ == '__main__': - torrent_url = 'http://anti-tor.org/torrent/544942' - torrent_file_url, magnet_url, info_hash = get_rutor_torrent_download_info(torrent_url) - print(torrent_file_url) - print(magnet_url) - print(info_hash) diff --git a/parsing_profile_twitter.py b/parsing_profile_twitter.py deleted file mode 100644 index 9a93b2a87..000000000 --- a/parsing_profile_twitter.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# TODO: from dev_window - -# from PySide.QtWebKit import * -# -# QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, True) -# -# self.view = QWebView() -# self.setCentralWidget(self.view) -# -# self.view.load('https://twitter.com/Misty_Shadow') -# self.doc = self.view.page().mainFrame().documentElement() -# -# def el_text(el, css_selector): -# return el.findFirst(css_selector).toPlainText() -# -# profile = self.doc.findFirst('.ProfileHeaderCard') -# -# profile_info = { -# 'name': el_text(profile, '.ProfileHeaderCard-name'), -# 'screenname': el_text(profile, '.ProfileHeaderCard-screenname'), -# 'location': el_text(profile, '.ProfileHeaderCard-location'), -# 'url': el_text(profile, '.ProfileHeaderCard-url'), -# 'joinDate': el_text(profile, '.ProfileHeaderCard-joinDate'), -# 'birthdate': el_text(profile, '.ProfileHeaderCard-birthdate'), -# } -# -# text_mess = '' -# -# for k in sorted(profile_info.keys()): -# text_mess += '{}: {}'.format(k, profile_info[k].strip()) + '\n' -# -# print(text_mess) -# -# QMessageBox.information(self, None, text_mess) \ No newline at end of file diff --git a/password_hash.py b/password_hash.py index 2a85fc1b8..f54011003 100644 --- a/password_hash.py +++ b/password_hash.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import hashlib @@ -14,13 +14,19 @@ def get_password_hash(name: str, password: str) -> str: if not password: raise ValueError('"password" is empty!') - data = (name.upper() + "-" + password).encode('UTF-8') + data = (name.upper() + "-" + password).encode("UTF-8") return hashlib.sha256(data).hexdigest().upper() -if __name__ == '__main__': - print(get_password_hash('abc', '123')) - assert get_password_hash('abc', '123') == '4D156D7BC9C38C000AA1E5B06E46E1F40CAFE651F3CABFAE9BAD48803BAF17BF' +if __name__ == "__main__": + print(get_password_hash("abc", "123")) + assert ( + get_password_hash("abc", "123") + == "4D156D7BC9C38C000AA1E5B06E46E1F40CAFE651F3CABFAE9BAD48803BAF17BF" + ) - print(get_password_hash('123', 'abc')) - assert get_password_hash('123', 'abc') == '84A990F85C3CDD4A5FE71B027996AF81E3F87062BE029FAC4C09DDC40B3C42F0' + print(get_password_hash("123", "abc")) + assert ( + get_password_hash("123", "abc") + == "84A990F85C3CDD4A5FE71B027996AF81E3F87062BE029FAC4C09DDC40B3C42F0" + ) diff --git a/pathlib__examples/create_file_in_user_directory.py b/pathlib__examples/create_file_in_user_directory.py index 32fabc505..53f16591d 100644 --- a/pathlib__examples/create_file_in_user_directory.py +++ b/pathlib__examples/create_file_in_user_directory.py @@ -1,20 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pathlib import Path -f = Path('~/test_file.txt').expanduser() + + +f = Path("~/test_file.txt").expanduser() print(f.exists()) # False print(f) # C:\Users\ipetrash\test_file.txt print(f.exists()) # False -print(f.write_text('Hello World', encoding='utf-8')) # 11 +print(f.write_text("Hello World", encoding="utf-8")) # 11 print(f.exists()) # True -print(f.read_text(encoding='utf-8')) # Hello World +print(f.read_text(encoding="utf-8")) # Hello World print(f.unlink()) # None print(f.exists()) # False diff --git a/pathlib__examples/get_filename__without_suffix.py b/pathlib__examples/get_filename__without_suffix.py index 69a42188d..e5a62f038 100644 --- a/pathlib__examples/get_filename__without_suffix.py +++ b/pathlib__examples/get_filename__without_suffix.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pathlib -file_name = 'C:/Users/111/video.mp4' + +file_name = "C:/Users/111/video.mp4" path = pathlib.Path(file_name) print(path.name) # video.mp4 print(path.stem) # video print() -file_name = 'video.mp4' +file_name = "video.mp4" path = pathlib.Path(file_name) print(path.name) # video.mp4 print(path.stem) # video diff --git a/pathlib__examples/get_user_directory.py b/pathlib__examples/get_user_directory.py index 499e27370..25ee45bd3 100644 --- a/pathlib__examples/get_user_directory.py +++ b/pathlib__examples/get_user_directory.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pathlib import Path -print(Path('~').expanduser()) -print(str(Path('~').expanduser())) + + +print(Path("~").expanduser()) +print(str(Path("~").expanduser())) diff --git a/pathlib__examples/print_current_files.py b/pathlib__examples/print_current_files.py index b2052c4a6..2baca32b5 100644 --- a/pathlib__examples/print_current_files.py +++ b/pathlib__examples/print_current_files.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pathlib + + current_dir = pathlib.Path(__file__).parent print(current_dir) -for file_name in current_dir.glob('*.py'): - print(' {}:\n {}\n'.format(file_name, file_name.read_bytes())) +for file_name in current_dir.glob("*.py"): + print(f" {file_name}:\n {file_name.read_bytes()}\n") diff --git a/pathlib__examples/search_and_rename_directory_in_hierarchy.py b/pathlib__examples/search_and_rename_directory_in_hierarchy.py index d36883e8f..7d10927c3 100644 --- a/pathlib__examples/search_and_rename_directory_in_hierarchy.py +++ b/pathlib__examples/search_and_rename_directory_in_hierarchy.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os.path -os.makedirs('./dir1/dir2/dir3/dir4', exist_ok=True) - from pathlib import Path -n = 'dir3' -m = 'new_dir3' + +os.makedirs("./dir1/dir2/dir3/dir4", exist_ok=True) + +n = "dir3" +m = "new_dir3" # Рекурсивный поиск -items = list(Path('.').rglob(n)) +items = list(Path(".").rglob(n)) # Если нашли папку if items: diff --git a/pathlib__examples/search_with_glob.py b/pathlib__examples/search_with_glob.py index 7470c27bc..d99333c86 100644 --- a/pathlib__examples/search_with_glob.py +++ b/pathlib__examples/search_with_glob.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pathlib # Print current folders and files -for file_name in pathlib.Path().glob('*'): +for file_name in pathlib.Path().glob("*"): # print abs path print(file_name.resolve()) print() # Print current folders and files with filter -for file_name in pathlib.Path().glob('get_*'): +for file_name in pathlib.Path().glob("get_*"): # print abs path print(file_name.resolve()) @@ -23,6 +23,6 @@ # for file_name in pathlib.Path('../').rglob('*'): # OR: -for file_name in pathlib.Path('../').glob('**/*'): +for file_name in pathlib.Path("../").glob("**/*"): # print abs path print(file_name.resolve()) diff --git a/pathlib__examples/syntax_path.py b/pathlib__examples/syntax_path.py index 8d1167ca4..c1957a6e2 100644 --- a/pathlib__examples/syntax_path.py +++ b/pathlib__examples/syntax_path.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# C:\Windows\System32\Boot import pathlib -path = pathlib.Path('C:/') / 'Windows' / 'System32' / 'Boot' + + +path = pathlib.Path("C:/") / "Windows" / "System32" / "Boot" print(path) # C:\Windows\System32\Boot diff --git a/pdf/extract_text__PyMuPDF.py b/pdf/extract_text__PyMuPDF.py new file mode 100644 index 000000000..9f655e124 --- /dev/null +++ b/pdf/extract_text__PyMuPDF.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install PyMuPDF +import fitz + + +with fitz.open("merger__PyPDF2/1.pdf") as doc: + items = [] + for page in doc: + items.append(page.get_text()) + text = "\n".join(items) + +print(text) diff --git a/pdf_merger__PyPDF2/1.pdf b/pdf/merger__PyPDF2/1.pdf similarity index 100% rename from pdf_merger__PyPDF2/1.pdf rename to pdf/merger__PyPDF2/1.pdf diff --git a/pdf_merger__PyPDF2/2.pdf b/pdf/merger__PyPDF2/2.pdf similarity index 100% rename from pdf_merger__PyPDF2/2.pdf rename to pdf/merger__PyPDF2/2.pdf diff --git a/pdf_merger__PyPDF2/3.pdf b/pdf/merger__PyPDF2/3.pdf similarity index 100% rename from pdf_merger__PyPDF2/3.pdf rename to pdf/merger__PyPDF2/3.pdf diff --git a/pdf/merger__PyPDF2/main.py b/pdf/merger__PyPDF2/main.py new file mode 100644 index 000000000..41f4f5516 --- /dev/null +++ b/pdf/merger__PyPDF2/main.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pypdf2 +from PyPDF2 import PdfFileMerger + + +pdfs = ["1.pdf", "2.pdf", "3.pdf"] + +merger = PdfFileMerger() +for pdf in pdfs: + merger.append(pdf) + +merger.write("result.pdf") +merger.close() diff --git a/pdf_merger__PyPDF2/result.pdf b/pdf/merger__PyPDF2/result.pdf similarity index 100% rename from pdf_merger__PyPDF2/result.pdf rename to pdf/merger__PyPDF2/result.pdf diff --git a/pdf_merger__PyPDF2/main.py b/pdf_merger__PyPDF2/main.py deleted file mode 100644 index 0bad6fa63..000000000 --- a/pdf_merger__PyPDF2/main.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# pip install pypdf2 -from PyPDF2 import PdfFileMerger - - -pdfs = ['1.pdf', '2.pdf', '3.pdf'] - -merger = PdfFileMerger() -for pdf in pdfs: - merger.append(pdf) - -merger.write("result.pdf") -merger.close() diff --git a/peewee__examples/SqliteQueueDatabase/config.py b/peewee__examples/SqliteQueueDatabase/config.py new file mode 100644 index 000000000..0c98459a0 --- /dev/null +++ b/peewee__examples/SqliteQueueDatabase/config.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +DIR: Path = Path(__file__).resolve().parent + +DB_DIR_NAME: Path = DIR / "database" +DB_DIR_NAME.mkdir(parents=True, exist_ok=True) + +DB_FILE_NAME: Path = DB_DIR_NAME / "db.sqlite" diff --git a/peewee__examples/SqliteQueueDatabase/db.py b/peewee__examples/SqliteQueueDatabase/db.py new file mode 100644 index 000000000..2ea9957cd --- /dev/null +++ b/peewee__examples/SqliteQueueDatabase/db.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum +import sys +import time +from typing import Type, Iterable, Self, Any + +# pip install peewee +from peewee import ( + Model, + TextField, + ForeignKeyField, + CharField, +) +from playhouse.shortcuts import model_to_dict +from playhouse.sqliteq import SqliteQueueDatabase + +from config import DB_FILE_NAME, DIR + +sys.path.append(str(DIR.parent)) +from shorten import shorten + + +# This working with multithreading +# SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqliteq +db = SqliteQueueDatabase( + DB_FILE_NAME, + pragmas={ + "foreign_keys": 1, + "journal_mode": "wal", # WAL-mode + "cache_size": -1024 * 64, # 64MB page-cache + }, + use_gevent=False, # Use the standard library "threading" module. + autostart=True, + queue_max_size=64, # Max. # of pending writes that can accumulate. + results_timeout=5.0, # Max. time to wait for query to be executed. +) + + +class BaseModel(Model): + class Meta: + database = db + + def get_new(self) -> Self: + return type(self).get(self._pk_expr()) + + @classmethod + def get_first(cls) -> Self: + return cls.select().first() + + @classmethod + def get_last(cls) -> Self: + return cls.select().order_by(cls.id.desc()).first() + + @classmethod + def get_inherited_models(cls) -> list[Type[Self]]: + return sorted(cls.__subclasses__(), key=lambda x: x.__name__) + + @classmethod + def print_count_of_tables(cls) -> None: + items = [] + for sub_cls in cls.get_inherited_models(): + name = sub_cls.__name__ + count = sub_cls.select().count() + items.append(f"{name}: {count}") + + print(", ".join(items)) + + @classmethod + def count(cls, filters: Iterable = None) -> int: + query = cls.select() + if filters: + query = query.filter(*filters) + return query.count() + + def to_dict(self) -> dict[str, Any]: + return model_to_dict(self, recurse=False) + + def __str__(self) -> str: + fields = [] + for k, field in self._meta.fields.items(): + v = getattr(self, k) + + if isinstance(field, (TextField, CharField)): + if isinstance(v, enum.Enum): + v = v.value + + if v: + v = repr(shorten(v, length=30)) + + elif isinstance(field, ForeignKeyField): + k = f"{k}_id" + if v: + v = v.id + + fields.append(f"{k}={v}") + + return self.__class__.__name__ + "(" + ", ".join(fields) + ")" + + +class Parameter(BaseModel): + name = TextField(primary_key=True) + value = TextField() + description = TextField(null=True) + + @classmethod + def get_by_name(cls, name: str) -> Self | None: + return cls.get_or_none(cls.name == name) + + @classmethod + def add( + cls, + name: str, + value: str, + description: str = None, + ) -> Self: + obj = cls.get_by_name(name) + if obj: + return obj + + return cls.create( + name=name, + value=value, + description=description, + ) + + +db.connect() +db.create_tables(BaseModel.get_inherited_models()) + + +# Задержка в 50мс, чтобы дать время на запуск SqliteQueueDatabase и создание таблиц +# Т.к. в SqliteQueueDatabase запросы на чтение выполняются сразу, а на запись попадают в очередь +time.sleep(0.050) + + +if __name__ == "__main__": + BaseModel.print_count_of_tables() diff --git a/peewee__examples/SqliteQueueDatabase/main.py b/peewee__examples/SqliteQueueDatabase/main.py new file mode 100644 index 000000000..389edd94b --- /dev/null +++ b/peewee__examples/SqliteQueueDatabase/main.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import concurrent.futures +import db + + +def create_parameter(name: str, value: str) -> db.Parameter: + return db.Parameter.add(name, value) + + +with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: + N = 10000 + + futures = [ + executor.submit(create_parameter, f"name_{i}", f"value_{i}") + for i in range(N) + ] + + items = [] + for future in concurrent.futures.as_completed(futures): + data: db.Parameter = future.result() + items.append(data) + + print("items:", len(items)) + print("count:", db.Parameter.count()) + assert len(items) == N diff --git a/peewee__examples/backup__examples/backup_via_api.py b/peewee__examples/backup__examples/backup_via_api.py new file mode 100644 index 000000000..bde8de1dd --- /dev/null +++ b/peewee__examples/backup__examples/backup_via_api.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sqlite3 +from pathlib import Path + +from peewee import SqliteDatabase + + +def backup(db: SqliteDatabase, file_name: Path | str) -> None: + dst = sqlite3.connect(file_name) + db.connection().backup(dst) + dst.close() + + +if __name__ == "__main__": + from common import run_test + + run_test( + backup=backup, + file_name_backup=Path(__file__).resolve().name + ".db", + ) diff --git a/peewee__examples/backup__examples/backup_via_vacuum_into.py b/peewee__examples/backup__examples/backup_via_vacuum_into.py new file mode 100644 index 000000000..522700bce --- /dev/null +++ b/peewee__examples/backup__examples/backup_via_vacuum_into.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from peewee import SqliteDatabase + + +def backup(db: SqliteDatabase, file_name: Path | str) -> None: + Path(file_name).unlink(missing_ok=True) + + db.connection().execute("VACUUM INTO ?", (str(file_name),)) + + +if __name__ == "__main__": + from common import run_test + + run_test( + backup=backup, + file_name_backup=Path(__file__).resolve().name + ".db", + ) diff --git a/peewee__examples/backup__examples/common.py b/peewee__examples/backup__examples/common.py new file mode 100644 index 000000000..c1178a22a --- /dev/null +++ b/peewee__examples/backup__examples/common.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sqlite3 +from pathlib import Path +from typing import Callable + +# pip install peewee +from peewee import Model, SqliteDatabase, TextField + + +db = SqliteDatabase("db.sqlite") + + +class Info(Model): + first_name = TextField() + second_name = TextField() + + class Meta: + database = db + + @classmethod + def fill(cls) -> None: + count = cls.select().count() + for i in range(5): + cls.create( + first_name=f"first_name_{i + count}", + second_name=f"second_name_{i + count}", + ) + + @classmethod + def print(cls) -> None: + for info in cls.select(): + print(info) + + @classmethod + def print_from(cls, file_name: str | Path) -> None: + connection = sqlite3.connect(file_name) + try: + for info in connection.execute(f"SELECT * FROM {cls._meta.table_name}"): + print(info) + except Exception as e: + print(f"[#] {e}") + + def __str__(self) -> str: + return f"Info<#{self.id} first_name={self.first_name!r} second_name={self.second_name!r}>" + + +def run_test( + backup: Callable, + file_name_backup: Path | str, +) -> None: + print("[db backup] Print:") + Info.print_from(file_name_backup) + print() + + Info.fill() + + print("[db] After fill:") + Info.print() + + backup(db, file_name_backup) + + +db.connect() +db.create_tables([Info]) + + +if __name__ == "__main__": + Info.fill() + + Info.print() + """ + Info<#1 first_name='first_name_0' second_name='second_name_0'> + Info<#2 first_name='first_name_1' second_name='second_name_1'> + Info<#3 first_name='first_name_2' second_name='second_name_2'> + Info<#4 first_name='first_name_3' second_name='second_name_3'> + Info<#5 first_name='first_name_4' second_name='second_name_4'> + """ + + Info.print_from("db.sqlite") + """ + (1, 'first_name_0', 'second_name_0') + (2, 'first_name_1', 'second_name_1') + (3, 'first_name_2', 'second_name_2') + (4, 'first_name_3', 'second_name_3') + (5, 'first_name_4', 'second_name_4') + """ diff --git a/peewee__examples/custom_field__EnumField/db_enum_field.py b/peewee__examples/custom_field__EnumField/db_enum_field.py new file mode 100644 index 000000000..ca8c83c1e --- /dev/null +++ b/peewee__examples/custom_field__EnumField/db_enum_field.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum +from typing import Type, Any + +from peewee import CharField + + +class EnumField(CharField): + """ + This class enable an Enum like field for Peewee + """ + + def __init__(self, choices: Type[enum.Enum], *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + self.choices: Type[enum.Enum] = choices + self.max_length: int = 255 + + def db_value(self, value: Any) -> Any: + if value is None: + return + + if isinstance(value, enum.Enum): + return value.value + + return value + + def python_value(self, value: Any) -> Any: + if value is None: + return + + type_value_enum = type(list(self.choices)[0].value) + value_enum = type_value_enum(value) + return self.choices(value_enum) diff --git a/peewee__examples/custom_field__EnumField/main.py b/peewee__examples/custom_field__EnumField/main.py new file mode 100644 index 000000000..06fb225f5 --- /dev/null +++ b/peewee__examples/custom_field__EnumField/main.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum + +# pip install peewee +from peewee import SqliteDatabase, Model + +from db_enum_field import EnumField + + +@enum.unique +class TaskRunStatusEnum(enum.StrEnum): + PENDING = enum.auto() + RUNNING = enum.auto() + FINISHED = enum.auto() + STOPPED = enum.auto() + UNKNOWN = enum.auto() + ERROR = enum.auto() + + +db = SqliteDatabase(":memory:", pragmas={"foreign_keys": 1}) + + +class BaseModel(Model): + class Meta: + database = db + + +class TaskRun(BaseModel): + status = EnumField(choices=TaskRunStatusEnum, default=TaskRunStatusEnum.PENDING) + + +db.connect() +db.create_tables([TaskRun]) + + +if __name__ == "__main__": + run1 = TaskRun.create() + run2 = TaskRun.create(status=TaskRunStatusEnum.FINISHED) + + query = TaskRun.select().where(TaskRun.status == TaskRunStatusEnum.FINISHED) + print(query) + print(list(query)) + # SELECT "t1"."id", "t1"."status" FROM "taskrun" AS "t1" WHERE ("t1"."status" = 'finished') + # [] + + print() + + query = TaskRun.select().where(TaskRun.status.contains("ING")) + print(query) + print(list(query)) + # SELECT "t1"."id", "t1"."status" FROM "taskrun" AS "t1" WHERE ("t1"."status" LIKE '%ING%') + # [] diff --git a/peewee__examples/custom_field__ListField/main.py b/peewee__examples/custom_field__ListField/main.py index 97044217e..4f430cf86 100644 --- a/peewee__examples/custom_field__ListField/main.py +++ b/peewee__examples/custom_field__ListField/main.py @@ -1,29 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import json -from typing import List, Iterable, Optional +from typing import Iterable # pip install peewee -from peewee import * +from peewee import SqliteDatabase, Model, Field, CharField class ListField(Field): - def python_value(self, value: str) -> List: - return json.loads(value, encoding='utf-8') + def python_value(self, value: str) -> list: + return json.loads(value) - def db_value(self, value: Optional[Iterable]) -> str: + def db_value(self, value: Iterable | None) -> str: if value is not None: if not isinstance(value, list): - raise Exception('Type must be a list') + raise Exception("Type must be a list") return json.dumps(value, ensure_ascii=False) -db = SqliteDatabase('db.sqlite', pragmas={'foreign_keys': 1}) +db = SqliteDatabase("db.sqlite", pragmas={"foreign_keys": 1}) class BaseModel(Model): @@ -40,7 +40,7 @@ class KeyByList(BaseModel): db.create_tables([KeyByList]) -if __name__ == '__main__': +if __name__ == "__main__": if not KeyByList.get_or_none(key="a"): KeyByList.create(key="a", values=[1, 2, 3]) @@ -67,6 +67,6 @@ class KeyByList(BaseModel): # [1, 2, 3] if not KeyByList.get_or_none(key="d"): - KeyByList.create(key="d", values=list(str(i ** 2) for i in range(1, 10))) + KeyByList.create(key="d", values=list(str(i**2) for i in range(1, 10))) print(KeyByList.get(key="d").values) # ['1', '4', '9', '16', '25', '36', '49', '64', '81'] diff --git a/peewee__examples/custom_func__get_http_status.py b/peewee__examples/custom_func__get_http_status.py new file mode 100644 index 000000000..055b09db0 --- /dev/null +++ b/peewee__examples/custom_func__get_http_status.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import requests +from peewee import SqliteDatabase + + +db = SqliteDatabase(":memory:") + + +@db.func("get_http_status") +def get_http_status(url: str) -> int: + return requests.get(url).status_code + + +db.connect() + +with db.connection() as connect: + print(connect.execute("""SELECT get_http_status("https://ya.ru") """).fetchone()[0]) + # 200 + + print(connect.execute("""SELECT get_http_status("https://ya.ru/404") """).fetchone()[0]) + # 404 diff --git a/peewee__examples/custom_func__redefine_upper.py b/peewee__examples/custom_func__redefine_upper.py new file mode 100644 index 000000000..5fe5d73fd --- /dev/null +++ b/peewee__examples/custom_func__redefine_upper.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from peewee import SqliteDatabase + + +def run_sql(connect) -> None: + print(connect.execute("""SELECT "ы", UPPER("ы") """).fetchone()) + print(connect.execute("""SELECT "s", UPPER("s") """).fetchone()) + print(connect.execute("""SELECT 1 WHERE UPPER("ы") LIKE UPPER("Ы") """).fetchone()) + print(connect.execute("""SELECT 1 WHERE "ы" LIKE "Ы" """).fetchone()) + print(connect.execute("""SELECT 1 WHERE "s" LIKE "S" """).fetchone()) + + +def db_func() -> None: + print("[db_func]") + + db = SqliteDatabase(":memory:") + db.connect() + + with db.connection() as connect: + run_sql(connect) + """ + ('ы', 'ы') + ('s', 'S') + None + None + (1,) + """ + + print() + + @db.func("upper") + def upper(value: str) -> str | None: + if value is None: + return + return value.upper() + + with db.connection() as connect: + run_sql(connect) + """ + ('ы', 'Ы') + ('s', 'S') + (1,) + None + (1,) + """ + + +def db_create_function() -> None: + print("[db_create_function]") + + db = SqliteDatabase(":memory:") + db.connect() + + with db.connection() as connect: + run_sql(connect) + """ + ('ы', 'ы') + ('s', 'S') + None + None + (1,) + """ + + print() + connect.create_function("upper", narg=1, func=str.upper) + + run_sql(connect) + """ + ('ы', 'Ы') + ('s', 'S') + (1,) + None + (1,) + """ + + +db_create_function() + +print() + +db_func() diff --git a/peewee__examples/custom_func__redefine_upper__check_speed.py b/peewee__examples/custom_func__redefine_upper__check_speed.py new file mode 100644 index 000000000..cee35cd06 --- /dev/null +++ b/peewee__examples/custom_func__redefine_upper__check_speed.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# NOTE: Analog https://github.com/gil9red/SimplePyScripts/blob/7b0238202c6e6b5750ff6909d0a78b7892ee1767/sqlite3__examples/custom_func__redefine_upper__check_speed.py + + +import random + +from timeit import default_timer, timeit +from typing import Type, Iterable, Self, Any + +# pip install peewee +from peewee import ( + SqliteDatabase, + Model, + TextField, + DecimalField, + fn +) +from playhouse.shortcuts import model_to_dict + + +db = SqliteDatabase(":memory:") + + +class BaseModel(Model): + class Meta: + database = db + + @classmethod + def get_inherited_models(cls) -> list[Type[Self]]: + return sorted(cls.__subclasses__(), key=lambda x: x.__name__) + + @classmethod + def count(cls, filters: Iterable = None) -> int: + query = cls.select() + if filters: + query = query.filter(*filters) + return query.count() + + def to_dict(self) -> dict[str, Any]: + return model_to_dict(self, recurse=False) + + +class Stocks(BaseModel): + date = TextField() + trans = TextField() + symbol = TextField() + qty = DecimalField() + price = DecimalField() + + +db.connect() +db.create_tables(BaseModel.get_inherited_models()) + +print("Generate items...") +t = default_timer() +purchases = [ + Stocks( + date=random.choice(["2006-03-28", "2006-04-05", "2006-04-06"]), + trans=random.choice(["SELL", "BUY"]), + symbol=random.choice(["IBM", "MSFT"]), + qty=random.choice([500, 1000, 600]), + price=random.choice([53.00, 45.00, 72.00]), + ) + for _ in range(500_000) +] +print(f"Elapsed {default_timer() - t:.3f} secs") +# Elapsed 2.097 secs + +print() + +print("INSERT INTO...") +t = default_timer() + +with db.atomic(): + Stocks.bulk_create(purchases, batch_size=5_000) + +print(f"Elapsed {default_timer() - t:.3f} secs") +# Elapsed 6.842 secs + +print() + + +def run_test() -> None: + elapsed = timeit( + stmt="Stocks.select().where(fn.UPPER(Stocks.trans).ilike(fn.UPPER('%SELL%'))).count()", + globals=dict(Stocks=Stocks, fn=fn), + number=100, + ) + print(f"Elapsed {elapsed:.3f} secs") + + +print("SELECT COUNT DEFAULT UPPER...") +run_test() +# Elapsed 11.765 secs + +print() + + +@db.func("upper") +def upper(value: str) -> str | None: + if value is None: + return + return value.upper() + + +print("SELECT COUNT PYTHON UPPER...") +run_test() +# Elapsed 47.276 secs diff --git a/peewee__examples/db_peewee_meta_model.py b/peewee__examples/db_peewee_meta_model.py new file mode 100644 index 000000000..6564853ef --- /dev/null +++ b/peewee__examples/db_peewee_meta_model.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from enum import Enum +from typing import Type, Any, Iterable + +from peewee import CharField, TextField, ForeignKeyField, Model, Field +from playhouse.shortcuts import model_to_dict + + +def shorten(text: str, length: int = 30, placeholder: str = "...") -> str: + if not text: + return text + + if len(text) > length: + text = text[: length - len(placeholder)] + placeholder + return text + + +class MetaModel(Model): + def get_new(self) -> "MetaModel": + return type(self).get(self._pk_expr()) + + @classmethod + def get_first(cls) -> "MetaModel": + return cls.select().first() + + @classmethod + def get_last(cls) -> "MetaModel": + return cls.select().order_by(cls.id.desc()).first() + + @classmethod + def paginating( + cls, + page: int = 1, + items_per_page: int = 1, + filters: Iterable | None = None, + order_by: Field | None = None, + ) -> list["MetaModel"]: + query = cls.select() + + if filters: + query = query.filter(*filters) + + if order_by: + query = query.order_by(order_by) + + query = query.paginate(page, items_per_page) + return list(query) + + @classmethod + def get_inherited_models(cls) -> list[Type["MetaModel"]]: + return sorted(cls.__subclasses__(), key=lambda x: x.__name__) + + @classmethod + def print_count_of_tables(cls) -> None: + items = [] + for sub_cls in cls.get_inherited_models(): + name = sub_cls.__name__ + count = sub_cls.select().count() + items.append(f"{name}: {count}") + + print(", ".join(items)) + + @classmethod + def count(cls, filters: Iterable = None) -> int: + query = cls.select() + if filters: + query = query.filter(*filters) + return query.count() + + def to_dict(self) -> dict[str, Any]: + return model_to_dict(self, recurse=False) + + def __str__(self) -> str: + fields = [] + for k, field in self._meta.fields.items(): + v = getattr(self, k) + + if isinstance(field, (TextField, CharField)): + if isinstance(v, Enum): + v = v.value + + if v: + v = repr(shorten(v, length=30)) + + elif isinstance(field, ForeignKeyField): + k = f"{k}_id" + if v: + v = v.id + + fields.append(f"{k}={v}") + + return self.__class__.__name__ + "(" + ", ".join(fields) + ")" + + +if __name__ == "__main__": + from peewee import SqliteDatabase + + db = SqliteDatabase(":memory:") + + class BaseModel(MetaModel): + class Meta: + database = db + + class Data(BaseModel): + name = CharField() + value = TextField() + + db.connect() + db.create_tables(BaseModel.get_inherited_models()) + + BaseModel.print_count_of_tables() + # Data: 0 + + data = dict( + a="aAA", + b="Foo", + bar="!!!", + ) + for k, v in data.items(): + Data.create(name=k, value=v) + + BaseModel.print_count_of_tables() + # Data: 3 + + print() + + print(Data.get_first()) + print(Data.get_last()) + # Data(id=1, name='a', value='aAA') + # Data(id=3, name='bar', value='!!!') + + print() + + print(Data.get_last().to_dict()) + # {'id': 3, 'name': 'bar', 'value': '!!!'} + + print() + filters = [ + Data.name.in_(["a", "b"]), + ] + order_by = Data.name.desc() + print(Data.paginating(page=1, filters=filters, order_by=order_by)) + print(Data.paginating(page=2, filters=filters, order_by=order_by)) + print(Data.paginating(page=3, filters=filters, order_by=order_by)) + # [] + # [] + # [] diff --git a/peewee__examples/games_and_genres/main.py b/peewee__examples/games_and_genres/main.py index 85e834429..08b171d27 100644 --- a/peewee__examples/games_and_genres/main.py +++ b/peewee__examples/games_and_genres/main.py @@ -1,16 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import List - # pip install peewee -from peewee import * +from peewee import SqliteDatabase, Model, CharField, TextField, ForeignKeyField -db = SqliteDatabase('games.sqlite', pragmas={'foreign_keys': 1}) +db = SqliteDatabase("games.sqlite", pragmas={"foreign_keys": 1}) class BaseModel(Model): @@ -21,10 +19,10 @@ class Meta: class Game(BaseModel): name = CharField(unique=True) - def get_genres(self) -> List['Genre']: + def get_genres(self) -> list["Genre"]: return [link.genre for link in self.links_to_genres] - def append_genres(self, *genres: List['Genre']): + def append_genres(self, *genres: list["Genre"]) -> None: for genre in genres: GameToGenre.get_or_create(game=self, genre=genre) @@ -33,17 +31,17 @@ class Genre(BaseModel): name = CharField(unique=True) description = TextField(null=True) - def get_games(self) -> List[Game]: + def get_games(self) -> list[Game]: return [link.game for link in self.links_to_games] class GameToGenre(BaseModel): - game = ForeignKeyField(Game, backref='links_to_genres') - genre = ForeignKeyField(Genre, backref='links_to_games') + game = ForeignKeyField(Game, backref="links_to_genres") + genre = ForeignKeyField(Genre, backref="links_to_games") class Meta: indexes = ( - (('game', 'genre'), True), + (("game", "genre"), True), ) @@ -75,30 +73,32 @@ class Meta: game.append_genres(GENRE__ACTION, GENRE__RPG, GENRE__ACTION_ADVENTURE) -if __name__ == '__main__': - print(f'Genre {GENRE__SURVIVAL_HORROR.name!r}:') +if __name__ == "__main__": + print(f"Genre {GENRE__SURVIVAL_HORROR.name!r}:") for game in GENRE__SURVIVAL_HORROR.get_games(): - print(f' {game.name}') + print(f" {game.name}") # Genre 'Survival horror': # Dead Space # Dead Island # Dying Light - print('\n' + '-' * 10 + '\n') + print("\n" + "-" * 10 + "\n") game, _ = Game.get_or_create(name="Dying Light") - print(f'Game {game.name!r}:') + print(f"Game {game.name!r}:") for genre in game.get_genres(): - print(f' {genre.name}: {genre.description!r}') + print(f" {genre.name}: {genre.description!r}") # Game 'Dying Light': # Survival horror: None # RPG: 'Role playing game' # Action: None - print('\n' + '-' * 10 + '\n') + print("\n" + "-" * 10 + "\n") for game in Game.select(): - print(f"{game.name}\n Genres: {', '.join(genre.name for genre in game.get_genres())}") + print( + f"{game.name}\n Genres: {', '.join(genre.name for genre in game.get_genres())}" + ) print() # Dead Space diff --git a/peewee__examples/hello_world/main.py b/peewee__examples/hello_world/main.py index 4636e72ba..c84820266 100644 --- a/peewee__examples/hello_world/main.py +++ b/peewee__examples/hello_world/main.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install peewee -from peewee import * +from peewee import Model, SqliteDatabase, TextField, IntegerField -db = SqliteDatabase('my_database.sqlite') +db = SqliteDatabase("my_database.sqlite") class Info(Model): @@ -19,28 +19,33 @@ class Info(Model): class Meta: database = db - def __str__(self): - return f'Info<#{self.id} first_name={self.first_name} second_name={self.second_name} state={self.state}>' + def __str__(self) -> str: + return f"Info<#{self.id} first_name={self.first_name!r} second_name={self.second_name!r} state={self.state}>" db.connect() db.create_tables([Info]) -if __name__ == '__main__': +if __name__ == "__main__": # Вызываем в первый раз, чтобы заполнить таблицу if not Info.select().count(): for i in range(5): - Info.create(first_name="first_name_" + str(i), second_name="second_name_" + str(i), state=i) + Info.create( + first_name="first_name_" + str(i), + second_name="second_name_" + str(i), + state=i, + ) for info in Info.select(): print(info) - # - # Info<#1 first_name=first_name_0 second_name=second_name_0 state=0> - # Info<#2 first_name=first_name_1 second_name=second_name_1 state=1> - # Info<#3 first_name=first_name_2 second_name=second_name_2 state=2> - # Info<#4 first_name=first_name_3 second_name=second_name_3 state=3> - # Info<#5 first_name=first_name_4 second_name=second_name_4 state=4> + """ + Info<#1 first_name='Четный state!' second_name='second_name_0' state=0> + Info<#2 first_name='first_name_1' second_name='second_name_1' state=1> + Info<#3 first_name='Четный state!' second_name='second_name_2' state=2> + Info<#4 first_name='first_name_3' second_name='second_name_3' state=3> + Info<#5 first_name='Четный state!' second_name='second_name_4' state=4> + """ for info in Info.select(): if info.state % 2 == 0: @@ -51,9 +56,10 @@ def __str__(self): for info in Info.select(): print(info) - # - # Info<#1 first_name=Четный state! second_name=second_name_0 state=0> - # Info<#2 first_name=first_name_1 second_name=second_name_1 state=1> - # Info<#3 first_name=Четный state! second_name=second_name_2 state=2> - # Info<#4 first_name=first_name_3 second_name=second_name_3 state=3> - # Info<#5 first_name=Четный state! second_name=second_name_4 state=4> + """ + Info<#1 first_name='Четный state!' second_name='second_name_0' state=0> + Info<#2 first_name='first_name_1' second_name='second_name_1' state=1> + Info<#3 first_name='Четный state!' second_name='second_name_2' state=2> + Info<#4 first_name='first_name_3' second_name='second_name_3' state=3> + Info<#5 first_name='Четный state!' second_name='second_name_4' state=4> + """ diff --git a/peewee__examples/hello_world__diary/main.py b/peewee__examples/hello_world__diary/main.py index e45f93d04..22baaa69d 100644 --- a/peewee__examples/hello_world__diary/main.py +++ b/peewee__examples/hello_world__diary/main.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/coleifer/peewee/blob/afdf7b752dcadbf440faaa91a7fb0f403eac9a69/examples/diary.py -from collections import OrderedDict -import datetime as DT +import datetime as dt + from textwrap import shorten # pip install peewee -from peewee import * +from peewee import Model, SqliteDatabase, TextField, DateTimeField -db = SqliteDatabase('my_database.sqlite') +db = SqliteDatabase("my_database.sqlite") class BaseModel(Model): @@ -25,14 +25,14 @@ class Meta: class Diary(BaseModel): content = TextField() - created_date = DateTimeField(default=DT.datetime.now) + created_date = DateTimeField(default=dt.datetime.now) @staticmethod - def print_table(search_text: str = None): + def print_table(search_text: str = None) -> None: """Print all diaries""" - header_fmt = '{:<3} | {:<50} | {:<19}' - row_fmt = '#{id:<3} | {content:<50} | {created_date:%d/%m/%Y %H:%M:%S}' + header_fmt = "{:<3} | {:<50} | {:<19}" + row_fmt = "#{id:<3} | {content:<50} | {created_date:%d/%m/%Y %H:%M:%S}" print(header_fmt.format(*map(str.upper, Diary._meta.fields))) @@ -41,20 +41,24 @@ def print_table(search_text: str = None): query = query.where(Diary.content.contains(search_text)) for diary in query: - print(row_fmt.format( - id=diary.id, - content=shorten(diary.content, width=50, placeholder='...'), - created_date=diary.created_date, - )) + print( + row_fmt.format( + id=diary.id, + content=shorten(diary.content, width=50, placeholder="..."), + created_date=diary.created_date, + ) + ) print() - def __str__(self): - return f'Diary<' \ - f'#{self.id} ' \ - f'content={repr(shorten(self.content, width=50, placeholder="..."))} ' \ - f"created_date='{self.created_date:%d/%m/%Y %H:%M:%S}'" \ - f">" + def __str__(self) -> str: + return ( + f"Diary<" + f"#{self.id} " + f'content={repr(shorten(self.content, width=50, placeholder="..."))} ' + f"created_date='{self.created_date:%d/%m/%Y %H:%M:%S}'" + f">" + ) db.connect() @@ -67,16 +71,16 @@ def __str__(self): Diary.create(content="The quick brown fox jumps over the lazy dog.") -def add_diary(): +def add_diary() -> None: """Add diary""" - data = input('Enter your diary: ').strip() - if data and input('Save diary? [Y/n] ') != 'n': + data = input("Enter your diary: ").strip() + if data and input("Save diary? [Y/n] ") != "n": Diary.create(content=data) - print('Saved successfully.') + print("Saved successfully.") -def view_diaries(search_query=None): +def view_diaries(search_query=None) -> None: """View previous diaries""" query = Diary.select().order_by(Diary.created_date.desc()) @@ -85,42 +89,44 @@ def view_diaries(search_query=None): for diary in query: print() - timestamp = diary.created_date.strftime('%d/%m/%Y %H:%M:%S') + timestamp = diary.created_date.strftime("%d/%m/%Y %H:%M:%S") print(timestamp) - print('=' * len(timestamp)) + print("=" * len(timestamp)) print(diary.content) print() - print('n) next diary') - print('d) delete diary') - print('q) return to main menu') - action = input('Choice? (N/d/q) ').lower().strip() - if action == 'q': + print("n) next diary") + print("d) delete diary") + print("q) return to main menu") + action = input("Choice? (N/d/q) ").lower().strip() + if action == "q": break - elif action == 'd': + elif action == "d": diary.delete_instance() break -def search_diaries(): +def search_diaries() -> None: """Search diaries""" - view_diaries(input('Search query: ')) + view_diaries(input("Search query: ")) -MENU = OrderedDict([ - ('a', add_diary), - ('v', view_diaries), - ('s', search_diaries), - ('p', Diary.print_table), -]) +MENU = dict( + [ + ("a", add_diary), + ("v", view_diaries), + ("s", search_diaries), + ("p", Diary.print_table), + ] +) -def menu_loop(): +def menu_loop() -> None: choice = None - while choice != 'q': + while choice != "q": for key, value in MENU.items(): - print('%s) %s' % (key, value.__doc__)) - choice = input('Action: ').lower().strip() + print("%s) %s" % (key, value.__doc__)) + choice = input("Action: ").lower().strip() if choice in MENU: MENU[choice]() diff --git a/peewee__examples/hello_world__diary/my_database.sqlite b/peewee__examples/hello_world__diary/my_database.sqlite index 73b394087..d6aa5bfd4 100644 Binary files a/peewee__examples/hello_world__diary/my_database.sqlite and b/peewee__examples/hello_world__diary/my_database.sqlite differ diff --git a/peewee__examples/hello_world__diary__encryption_AES_key_for_evere_diary__with_master_key/main.py b/peewee__examples/hello_world__diary__encryption_AES_key_for_evere_diary__with_master_key/main.py index 1623f3e45..a3cb75b15 100644 --- a/peewee__examples/hello_world__diary__encryption_AES_key_for_evere_diary__with_master_key/main.py +++ b/peewee__examples/hello_world__diary__encryption_AES_key_for_evere_diary__with_master_key/main.py @@ -1,27 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/coleifer/peewee/blob/afdf7b752dcadbf440faaa91a7fb0f403eac9a69/examples/diary.py -import datetime as DT +import datetime as dt import hashlib import sys from getpass import getpass # pip install peewee -from peewee import * +from peewee import Model, SqliteDatabase, TextField, DateTimeField -sys.path.append('../hello_world__diary__encryption_all_in_one_AES') +sys.path.append("../hello_world__diary__encryption_all_in_one_AES") from utils.security import CryptoAES, AuthenticationError from utils.utils import shorten -db = SqliteDatabase('my_database.sqlite') +db = SqliteDatabase("my_database.sqlite") class BaseModel(Model): @@ -30,7 +30,7 @@ class Meta: # Password: 123 -ENCRYPT_MASTER_KEY = 'A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE3' +ENCRYPT_MASTER_KEY = "A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE3" # OR: # ENCRYPT_MASTER_KEY = None @@ -41,25 +41,27 @@ class Meta: # Need password password = getpass() if not password: - print('Required password!') + print("Required password!") sys.exit() - ENCRYPT_MASTER_KEY = hashlib.sha256(bytes(password, 'utf-8')).hexdigest().upper() + ENCRYPT_MASTER_KEY = hashlib.sha256(bytes(password, "utf-8")).hexdigest().upper() class Diary(BaseModel): encrypted_content = TextField() # Содержимое будет шифровано ключом - encrypted_key = TextField() # Ключ будет шифрован мастер ключом - created_date = DateTimeField(default=DT.datetime.now) + encrypted_key = TextField() # Ключ будет шифрован мастер ключом + created_date = DateTimeField(default=dt.datetime.now) @staticmethod - def create_encrypted_content(content: str, master_key: str) -> 'Diary': + def create_encrypted_content(content: str, master_key: str) -> "Diary": key = CryptoAES.get_random_key_hex() encrypted_content = CryptoAES(key).encrypt(content) encrypted_key = CryptoAES(master_key).encrypt(key) - return Diary(encrypted_content=encrypted_content, encrypted_key=encrypted_key).save() + return Diary( + encrypted_content=encrypted_content, encrypted_key=encrypted_key + ).save() def get_content(self, master_key: str) -> str: try: @@ -70,7 +72,7 @@ def get_content(self, master_key: str) -> str: return CryptoAES(key).decrypt(self.encrypted_content) except AuthenticationError as e: - return f'ERROR: {e}' + return f"ERROR: {e}" def get_key(self, master_key: str) -> str: try: @@ -78,26 +80,37 @@ def get_key(self, master_key: str) -> str: return CryptoAES(master_key).decrypt(self.encrypted_key) except AuthenticationError as e: - return f'ERROR: {e}' + return f"ERROR: {e}" @staticmethod - def print_table(master_key: str = ENCRYPT_MASTER_KEY): + def print_table(master_key: str = ENCRYPT_MASTER_KEY) -> None: """Print all diaries""" - header_fmt = '{:<3} | {:<50} | {:<50} | {:<50} | {:<50} | {:<19}' - row_fmt = '#{id:<3} | {encrypted_content:<50} | {content:<50} | {encrypted_key:<50} | {key:<50} | {created_date:%d/%m/%Y %H:%M:%S}' + header_fmt = "{:<3} | {:<50} | {:<50} | {:<50} | {:<50} | {:<19}" + row_fmt = "#{id:<3} | {encrypted_content:<50} | {content:<50} | {encrypted_key:<50} | {key:<50} | {created_date:%d/%m/%Y %H:%M:%S}" - print(header_fmt.format('ID', 'ENCRYPTED_CONTENT', 'CONTENT', 'ENCRYPTED_KEY', 'KEY', 'CREATED_DATE')) + print( + header_fmt.format( + "ID", + "ENCRYPTED_CONTENT", + "CONTENT", + "ENCRYPTED_KEY", + "KEY", + "CREATED_DATE", + ) + ) for diary in Diary.select(): - print(row_fmt.format( - id=diary.id, - encrypted_content=shorten(diary.encrypted_content), - content=shorten(diary.get_content(master_key)), - encrypted_key=shorten(diary.encrypted_key), - key=shorten(diary.get_key(master_key)), - created_date=diary.created_date, - )) + print( + row_fmt.format( + id=diary.id, + encrypted_content=shorten(diary.encrypted_content), + content=shorten(diary.get_content(master_key)), + encrypted_key=shorten(diary.encrypted_key), + key=shorten(diary.get_key(master_key)), + created_date=diary.created_date, + ) + ) print() @@ -108,56 +121,61 @@ def print_table(master_key: str = ENCRYPT_MASTER_KEY): # Вызываем в первый раз, чтобы заполнить таблицу if not Diary.select().count(): - Diary.create_encrypted_content(content="Hello World!", master_key=ENCRYPT_MASTER_KEY) - Diary.create_encrypted_content(content="The quick brown fox jumps over the lazy dog.", master_key=ENCRYPT_MASTER_KEY) + Diary.create_encrypted_content( + content="Hello World!", master_key=ENCRYPT_MASTER_KEY + ) + Diary.create_encrypted_content( + content="The quick brown fox jumps over the lazy dog.", + master_key=ENCRYPT_MASTER_KEY, + ) -def add_diary(master_key: str = ENCRYPT_MASTER_KEY): +def add_diary(master_key: str = ENCRYPT_MASTER_KEY) -> None: """Add diary""" - data = input('Enter your diary: ').strip() - if data and input('Save diary? [Y/n] ') != 'n': + data = input("Enter your diary: ").strip() + if data and input("Save diary? [Y/n] ") != "n": Diary.create_encrypted_content(content=data, master_key=master_key) - print('Saved successfully.') + print("Saved successfully.") print() -def view_diaries(master_key: str = ENCRYPT_MASTER_KEY): +def view_diaries(master_key: str = ENCRYPT_MASTER_KEY) -> None: """View previous diaries""" query = Diary.select().order_by(Diary.created_date.desc()) for diary in query: print() - timestamp = diary.created_date.strftime('%d/%m/%Y %H:%M:%S') + timestamp = diary.created_date.strftime("%d/%m/%Y %H:%M:%S") print(timestamp) - print('=' * len(timestamp)) + print("=" * len(timestamp)) print(diary.encrypted_content) print(diary.get_content(master_key)) print() - print('n) next diary') - print('d) delete diary') - print('q) return to main menu') - action = input('Choice? (N/d/q) ').lower().strip() - if action == 'q': + print("n) next diary") + print("d) delete diary") + print("q) return to main menu") + action = input("Choice? (N/d/q) ").lower().strip() + if action == "q": break - elif action == 'd': + elif action == "d": diary.delete_instance() break MENU = { - 'a': add_diary, - 'v': view_diaries, - 'p': Diary.print_table, + "a": add_diary, + "v": view_diaries, + "p": Diary.print_table, } -def menu_loop(): +def menu_loop() -> None: choice = None - while choice != 'q': + while choice != "q": for key, value in MENU.items(): - print('%s) %s' % (key, value.__doc__)) - choice = input('Action: ').lower().strip() + print("%s) %s" % (key, value.__doc__)) + choice = input("Action: ").lower().strip() if choice in MENU: MENU[choice]() diff --git a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/main.py b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/main.py index b95998296..892ae1c94 100644 --- a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/main.py +++ b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/main.py @@ -1,26 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/coleifer/peewee/blob/afdf7b752dcadbf440faaa91a7fb0f403eac9a69/examples/diary.py -import datetime as DT +import datetime as dt import hashlib import sys from getpass import getpass # pip install peewee -from peewee import * +from peewee import Model, SqliteDatabase, TextField, DateTimeField from utils.security import CryptoAES, AuthenticationError from utils.utils import shorten -db = SqliteDatabase('my_database.sqlite') +db = SqliteDatabase("my_database.sqlite") class BaseModel(Model): @@ -29,7 +29,7 @@ class Meta: # Password: 123 -ENCRYPT_KEY = 'A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE3' +ENCRYPT_KEY = "A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE3" # OR: # ENCRYPT_KEY = None @@ -40,18 +40,18 @@ class Meta: # Need password password = getpass() if not password: - print('Required password!') + print("Required password!") sys.exit() - ENCRYPT_KEY = hashlib.sha256(bytes(password, 'utf-8')).hexdigest().upper() + ENCRYPT_KEY = hashlib.sha256(bytes(password, "utf-8")).hexdigest().upper() class Diary(BaseModel): encrypted_content = TextField() - created_date = DateTimeField(default=DT.datetime.now) + created_date = DateTimeField(default=dt.datetime.now) @staticmethod - def create_encrypted_content(content: str, key: str) -> 'Diary': + def create_encrypted_content(content: str, key: str) -> "Diary": encrypted_content = CryptoAES(key).encrypt(content) return Diary(encrypted_content=encrypted_content).save() @@ -59,24 +59,26 @@ def get_content(self, key: str) -> str: try: return CryptoAES(key).decrypt(self.encrypted_content) except AuthenticationError as e: - return f'ERROR: {e}' + return f"ERROR: {e}" @staticmethod - def print_table(key: str = ENCRYPT_KEY): + def print_table(key: str = ENCRYPT_KEY) -> None: """Print all diaries""" - header_fmt = '{:<3} | {:<50} | {:<50} | {:<19}' - row_fmt = '#{id:<3} | {encrypted_content:<50} | {content:<50} | {created_date:%d/%m/%Y %H:%M:%S}' + header_fmt = "{:<3} | {:<50} | {:<50} | {:<19}" + row_fmt = "#{id:<3} | {encrypted_content:<50} | {content:<50} | {created_date:%d/%m/%Y %H:%M:%S}" - print(header_fmt.format('ID', 'ENCRYPTED_CONTENT', 'CONTENT', 'CREATED_DATE')) + print(header_fmt.format("ID", "ENCRYPTED_CONTENT", "CONTENT", "CREATED_DATE")) for diary in Diary.select(): - print(row_fmt.format( - id=diary.id, - encrypted_content=shorten(diary.encrypted_content), - content=shorten(diary.get_content(key)), - created_date=diary.created_date, - )) + print( + row_fmt.format( + id=diary.id, + encrypted_content=shorten(diary.encrypted_content), + content=shorten(diary.get_content(key)), + created_date=diary.created_date, + ) + ) print() @@ -88,55 +90,57 @@ def print_table(key: str = ENCRYPT_KEY): # Вызываем в первый раз, чтобы заполнить таблицу if not Diary.select().count(): Diary.create_encrypted_content(content="Hello World!", key=ENCRYPT_KEY) - Diary.create_encrypted_content(content="The quick brown fox jumps over the lazy dog.", key=ENCRYPT_KEY) + Diary.create_encrypted_content( + content="The quick brown fox jumps over the lazy dog.", key=ENCRYPT_KEY + ) -def add_diary(key: str = ENCRYPT_KEY): +def add_diary(key: str = ENCRYPT_KEY) -> None: """Add diary""" - data = input('Enter your diary: ').strip() - if data and input('Save diary? [Y/n] ') != 'n': + data = input("Enter your diary: ").strip() + if data and input("Save diary? [Y/n] ") != "n": Diary.create_encrypted_content(content=data, key=key) - print('Saved successfully.') + print("Saved successfully.") print() -def view_diaries(key: str = ENCRYPT_KEY): +def view_diaries(key: str = ENCRYPT_KEY) -> None: """View previous diaries""" query = Diary.select().order_by(Diary.created_date.desc()) for diary in query: print() - timestamp = diary.created_date.strftime('%d/%m/%Y %H:%M:%S') + timestamp = diary.created_date.strftime("%d/%m/%Y %H:%M:%S") print(timestamp) - print('=' * len(timestamp)) + print("=" * len(timestamp)) print(diary.encrypted_content) print(diary.get_content(key)) print() - print('n) next diary') - print('d) delete diary') - print('q) return to main menu') - action = input('Choice? (N/d/q) ').lower().strip() - if action == 'q': + print("n) next diary") + print("d) delete diary") + print("q) return to main menu") + action = input("Choice? (N/d/q) ").lower().strip() + if action == "q": break - elif action == 'd': + elif action == "d": diary.delete_instance() break MENU = { - 'a': add_diary, - 'v': view_diaries, - 'p': Diary.print_table, + "a": add_diary, + "v": view_diaries, + "p": Diary.print_table, } -def menu_loop(): +def menu_loop() -> None: choice = None - while choice != 'q': + while choice != "q": for key, value in MENU.items(): - print('%s) %s' % (key, value.__doc__)) - choice = input('Action: ').lower().strip() + print("%s) %s" % (key, value.__doc__)) + choice = input("Action: ").lower().strip() if choice in MENU: MENU[choice]() diff --git a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/__init__.py b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/__init__.py index 06334aa61..f25732790 100644 --- a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/__init__.py +++ b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' - +__author__ = "ipetrash" diff --git a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/security.py b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/security.py index 934863819..c91120fb1 100644 --- a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/security.py +++ b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/security.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/328be435db9872cfcab35d6df33751006e3e8a64/pycryptodome__examples__AES_DES_RSA/AES_with_password__verify_key.py @@ -19,9 +19,9 @@ class AuthenticationError(Exception): class CryptoAES: - def __init__(self, key: (str, bytes)): + def __init__(self, key: (str, bytes)) -> None: if isinstance(key, str): - key = key.encode('utf-8') + key = key.encode("utf-8") self.key = hashlib.sha256(key).digest() @@ -30,18 +30,22 @@ def get_random_key_hex() -> str: return get_random_bytes(32).hex() def encrypt(self, plain_text: str) -> str: - data = plain_text.encode('utf-8') + data = plain_text.encode("utf-8") cipher = AES.new(self.key, AES.MODE_EAX) cipher_text, tag = cipher.encrypt_and_digest(data) encrypted_data = cipher.nonce + tag + cipher_text - return base64.b64encode(encrypted_data).decode('utf-8') + return base64.b64encode(encrypted_data).decode("utf-8") def decrypt(self, encrypted_text: str) -> str: encrypted_data = base64.b64decode(encrypted_text) - nonce, tag, cipher_text = encrypted_data[:16], encrypted_data[16:32], encrypted_data[32:] + nonce, tag, cipher_text = ( + encrypted_data[:16], + encrypted_data[16:32], + encrypted_data[32:], + ) cipher = AES.new(self.key, AES.MODE_EAX, nonce) try: @@ -49,12 +53,12 @@ def decrypt(self, encrypted_text: str) -> str: except ValueError as e: raise AuthenticationError(e) - return data.decode('utf-8') + return data.decode("utf-8") -if __name__ == '__main__': - text = 'Hello World!' - password = '123' +if __name__ == "__main__": + text = "Hello World!" + password = "123" crypto = CryptoAES(password) encrypted_text = crypto.encrypt(text) @@ -65,6 +69,6 @@ def decrypt(self, encrypted_text: str) -> str: # Decrypt with invalid password try: - CryptoAES('abc').decrypt(encrypted_text) + CryptoAES("abc").decrypt(encrypted_text) except AuthenticationError as e: - assert str(e) == 'MAC check failed' + assert str(e) == "MAC check failed" diff --git a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/utils.py b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/utils.py index 70d753506..6aca51bbd 100644 --- a/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/utils.py +++ b/peewee__examples/hello_world__diary__encryption_all_in_one_AES/utils/utils.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def shorten(text: str, width: int = 50) -> str: if len(text) <= width: return text - placeholder = '...' - return text[:width - len(placeholder)] + placeholder + placeholder = "..." + return text[: width - len(placeholder)] + placeholder diff --git a/peewee__examples/persons/main.py b/peewee__examples/persons/main.py index ec993cf75..f949fcab0 100644 --- a/peewee__examples/persons/main.py +++ b/peewee__examples/persons/main.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt import json # pip install peewee -from peewee import * +from peewee import SqliteDatabase, Model, CharField, DateField, ForeignKeyField -db = SqliteDatabase('persons.sqlite', pragmas={'foreign_keys': 1}) +db = SqliteDatabase("persons.sqlite", pragmas={"foreign_keys": 1}) class BaseModel(Model): @@ -25,7 +25,7 @@ class Person(BaseModel): class Pet(BaseModel): - owner = ForeignKeyField(Person, backref='pets') + owner = ForeignKeyField(Person, backref="pets") name = CharField() animal_type = CharField() @@ -33,19 +33,21 @@ class Pet(BaseModel): db.connect() db.create_tables([Person, Pet]) -for person_data in json.load(open('persons.json', encoding='utf-8')): - birthday = DT.datetime.strptime(person_data['birthday'], '%Y-%M-%d') - person, _ = Person.get_or_create(name=person_data['name'], birthday=birthday) +for person_data in json.load(open("persons.json", encoding="utf-8")): + birthday = dt.datetime.strptime(person_data["birthday"], "%Y-%M-%d") + person, _ = Person.get_or_create(name=person_data["name"], birthday=birthday) - for pet in person_data['pets']: - Pet.get_or_create(owner=person, name=pet['name'], animal_type=pet['animal_type']) + for pet in person_data["pets"]: + Pet.get_or_create( + owner=person, name=pet["name"], animal_type=pet["animal_type"] + ) for person in Person.select(): - print(f'{person.name} ({person.birthday}). Pets: {person.pets.count()}') + print(f"{person.name} ({person.birthday}). Pets: {person.pets.count()}") for pet in person.pets: - print(f' {pet.name} ({pet.animal_type}). Owner: {pet.owner.name}') + print(f" {pet.name} ({pet.animal_type}). Owner: {pet.owner.name}") print() diff --git a/peewee__examples/serialization__model_to_dict__dict_to_model/main.py b/peewee__examples/serialization__model_to_dict__dict_to_model/main.py index 7cd6864da..85a10a0e5 100644 --- a/peewee__examples/serialization__model_to_dict__dict_to_model/main.py +++ b/peewee__examples/serialization__model_to_dict__dict_to_model/main.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt # pip install peewee -from peewee import * +from peewee import SqliteDatabase, Model, CharField, DateField, ForeignKeyField from playhouse.shortcuts import model_to_dict, dict_to_model -db = SqliteDatabase(':memory:', pragmas={'foreign_keys': 1}) +db = SqliteDatabase(":memory:", pragmas={"foreign_keys": 1}) class BaseModel(Model): @@ -24,27 +24,29 @@ class Person(BaseModel): name = CharField() birthday = DateField() - def __str__(self): - return f'Person(id={self.id} name={self.name!r} birthday={self.birthday} ' \ - f'pets={", ".join(p.name for p in self.pets)!r})' + def __str__(self) -> str: + return ( + f"Person(id={self.id} name={self.name!r} birthday={self.birthday} " + f'pets={", ".join(p.name for p in self.pets)!r})' + ) class Pet(BaseModel): - owner = ForeignKeyField(Person, backref='pets') + owner = ForeignKeyField(Person, backref="pets") name = CharField() animal_type = CharField() - def __str__(self): - return f'Pet(id={self.id} name={self.name!r} owner={self.owner.name!r} self.animal_type={self.animal_type!r})' + def __str__(self) -> str: + return f"Pet(id={self.id} name={self.name!r} owner={self.owner.name!r} self.animal_type={self.animal_type!r})" db.connect() db.create_tables([Person, Pet]) -person = Person.create(name='Ivan', birthday=DT.date.today()) +person = Person.create(name="Ivan", birthday=dt.date.today()) -Pet.create(owner=person, name='Oval', animal_type='Dog') -Pet.create(owner=person, name='Bortik', animal_type='Cat') +Pet.create(owner=person, name="Oval", animal_type="Dog") +Pet.create(owner=person, name="Bortik", animal_type="Cat") print(person) # Person(id=1 name='Ivan' birthday=2020-01-09 pets='Oval, Bortik') @@ -63,7 +65,7 @@ def __str__(self): print() # Create another database and import this -db = SqliteDatabase('persons.sqlite', pragmas={'foreign_keys': 1}) +db = SqliteDatabase("persons.sqlite", pragmas={"foreign_keys": 1}) Person._meta.database = db Pet._meta.database = db db.connect() diff --git a/pens_calcs/common.py b/pens_calcs/common.py index c1c00972b..ea8e09133 100644 --- a/pens_calcs/common.py +++ b/pens_calcs/common.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # http://www.pfrf.ru/thm/common/mod/pensCalc/js/params.js @@ -45,7 +45,7 @@ 7: 1.74, 8: 1.9, 9: 2.09, - 10: 2.32 + 10: 2.32, } VSkoef = 1.8 # Коэффициент за 1 год военной службы diff --git a/pens_calcs/number_of_pension_points_for_year.py b/pens_calcs/number_of_pension_points_for_year.py index 9c12bd721..42d0bc365 100644 --- a/pens_calcs/number_of_pension_points_for_year.py +++ b/pens_calcs/number_of_pension_points_for_year.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import common @@ -21,8 +21,10 @@ def number_of_pension_points_for_year(zp: float) -> float: # Зарплата меньше мрот if zp < common.MROT: - raise Exception('Ошибка! Введите зарплату выше, чем минимальный размер оплаты труда в ' - 'Российской Федерации в 2017 году - 7 500 рублей') + raise Exception( + "Ошибка! Введите зарплату выше, чем минимальный размер оплаты труда в " + "Российской Федерации в 2017 году - 7 500 рублей" + ) kpk_trud = zp / common.ZPM * 10 if kpk_trud > 8.26: @@ -31,7 +33,7 @@ def number_of_pension_points_for_year(zp: float) -> float: return round(kpk_trud * 100) / 100 -if __name__ == '__main__': +if __name__ == "__main__": # Сколько пенсионных баллов может быть начислено Вам за 2017 год? # # Введите размер Вашей ежемесячной заработной платы до вычета НДФЛ: diff --git a/pens_calcs/pens_calc.py b/pens_calcs/pens_calc.py deleted file mode 100644 index 6d51873a1..000000000 --- a/pens_calcs/pens_calc.py +++ /dev/null @@ -1,555 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -import common - - -# TODO: сделать -# http://www.pfrf.ru/thm/common/mod/pensCalc/js/public.js -# TODO: добавить аннотации параметрам и возвращаемому значению - -# # Служба в армии -# yearsInArmy, monthInArmy, daysInArmy - -# # Уход за нетрудоспособными -# careYears, careMonth, careDays - -# # Дети, не более 4х -# TODO: посмотреть к каким полям относится и переименвать соответственно и в формулах их использования -# childrenCount1, childrenVac1 - - -# def pens_calc(gender, birthDate, pensionTarif, -# yearsInArmy, monthInArmy, daysInArmy, -# careYears, careMonth, careDays, -# childrenCount1, childrenVac1, -# retireWorkWithoutPension, -# careerLength, -# revenue, -# SZPeriod -# ): - - - -# var pensionForm = $(".calc-form") -# var persionFormInputs = pensionForm.find("input,select") -# var calcResult = $(".pensionCalcResult") -# var calcResultToday = $(".pensionCalcResultToday") -# var socialPensionWarning = $("#socialPensionWarning") -# var socialPensionWarning2 = $("#socialPensionWarning2") -# var combinationWarning = $("#combinationWarning") -# var enterParamsWarning = $("#enterParamsWarning") -# var enterGenderWarning = $("#enterGenderWarning") -# var enterBYWarning = $("#enterBYWarning") -# var enterBYWarning2 = $("#enterBYWarning2") -# var enterBYWarning3 = $("#enterBYWarning3") -# var alreadyPensioneer = $("#alreadyPensioneer") -# var wrongFee = $("#wrongFee") -# var noTarif = $("#noTarif") -# var ending = $(".ending") -# var newCoefSummSmall = $(".newCoefSummSmall") -# var personOSsmall = $(".personOSsmall") -# -# var armySection = pensionForm.find('.army-section') - -# TODO: возможно будет полезно узнать что скрыто в той секции -# armySection.hide() -# persionFormInputs.filter('[type="radio"][name="noArmy"]').click(function () { -# if($(this).val() == 2) { -# armySection.slideDown(150) -# } -# else { -# armySection.slideUp(150) -# } -# }) - -# var textInputs = persionFormInputs.filter("input[type='text']") -# var prevValues = [] - - -# var careerPlanQuestions = $("div.careerPlanQuestions") -# var careerPlanSwitch = $("input[name='careerPlan']") -# -# function revealCPQuestionsBlocks(switchValue) { -# careerPlanQuestions.hide() -# switch(switchValue) { -# case '1': -# //наемный работник -# careerPlanQuestions.filter(".careerPlan1").show() -# break -# case '2': -# //самозанятый -# careerPlanQuestions.filter(".careerPlan2").show() -# break -# case '3': -# //совмещающий -# careerPlanQuestions.show() -# break -# } -# } -# -# revealCPQuestionsBlocks(careerPlanSwitch.filter(':checked').val()) -# careerPlanSwitch.change(function () { -# revealCPQuestionsBlocks($(this).val()) -# }) - - -# TODO: поле в скрипте не имеет значения, вообще -gender = "1" -# if(gender.length < 1) { -# enterGenderWarning.show() -# $(output_area).slideDown() -# return -# } - -# TODO: поле в скрипте не имеет значения, вообще -birthDate = "1992" -# if(!birthDate.match(/\d{4}/)) { -# enterBYWarning.show() -# $(output_area).slideDown() -# return -# } - -# TODO: узнать что скрывается за полем -# TODO: пусть будет пока 0 -pensionTarif = 0 -# var pensionTarif = persionFormInputs.filter('[name="pensionTarif"]:checked') -# if(pensionTarif.length < 1) { -# noTarif.show() -# $(output_area).slideDown() -# return -# } -# pensionTarif = parseInt(pensionTarif.val()) - -# Общий расчет - -# Служба в армии -yearsInArmy = 1 -monthInArmy = 0 -daysInArmy = 0 - -# Стаж в армии -VS = ((((yearsInArmy * 12) + monthInArmy) / 12 * 365) + daysInArmy) / 365 - -# Коэффициент стажа -VSK = VS * common.VSkoef - -# Уход за нетрудоспособными -careYears = 1 -careMonth = 0 -careDays = 0 - -# Стаж ухода за нетрудоспособными -CR = ((((careYears * 12) + careMonth) / 12 * 365) + careDays) / 365 - -# Коэффициент ухода -CRK = CR * common.CRkoef - -# Дети, не более 4х -childrenCount1 = 1 -if childrenCount1 < 0: - childrenCount1 = 0 - -elif childrenCount1 > 4: - childrenCount1 = 4 - -# TODO: узнать что за childrenVac1 -# TODO: и что за стаж -# Стаж -childrenVac1 = 0 -if childrenVac1 < 0: - childrenVac1 = 0 - -elif childrenVac1 > 1.5: - childrenVac1 = 1.5 - - -# NOTE: только внутри кода объявляется -# Коэффициент -KD = 0.0 - -if childrenCount1 > 0: - if childrenCount1 > 0: - KD += 1.8 - - if childrenCount1 > 1: - KD += 3.6 - - if childrenCount1 > 2: - KD += 5.4 - - if childrenCount1 > 3: - KD += 5.4 - - KD *= childrenVac1 - childrenVac1 *= childrenCount1 - -# Коэффициент за нетрудовые периоды -NK = KD + CRK + VSK - -# Стаж за нетрудовые периоды -NS = childrenVac1 + VS + CR - -# NOTE: сколько после пенсионного возраста собираешься работать неполучая пенсию -retireWorkWithoutPension = 5 -if retireWorkWithoutPension > 10: - retireWorkWithoutPension = 10 - -# Расчеты в зависимости от типа занятости -# TODO: узнать возможные варианты значения -careerPlan = '1' - -# Наемный работник -def calcEmpl(fee: int, careerLength: int, pensionTarif: int): - """ - - :param fee: Зарплата - :param careerLength: Стаж - - # TODO: узнать что за тариф такой - :param pensionTarif: - :return: - """ - - if fee < 0: - fee = 0 - - if fee > common.ZPM: - fee = common.ZPM - - if careerLength < 0: - careerLength = 0 - - # TODO: бросать исключение, узнать какой текст тут скрыт - # if careerLength > 60) { - # enterBYWarning3.show() - # $(output_area).slideDown() - # return false - - # TODO: бросать исключение, узнать какой текст тут скрыт - # # Зарплата меньше мрот - # if careerLength > 0 and fee < common.MROT: - # wrongFee.show() - # $(output_area).slideDown() - # return false - # } - - # Пенсионные коэффициенты за трудовой период - IPKtrud = (fee / common.ZPM) * common.KNPG[pensionTarif] * (careerLength * 10) - - # TODO: Проследить за S и заменить - return { - 'S': careerLength, - 'IPKtrud': IPKtrud, - } - -def calcSZ(SZPeriod: int, revenue: int, pensionTarif: int): - """ - - :param revenue: Годовой доход - :param SZPeriod: Стаж - - # TODO: узнать что за тариф такой - :param pensionTarif: - :return: - """ - - if SZPeriod < 0: - SZPeriod = 0 - - if revenue < 0: - revenue = 0 - - # TODO: разобраться какие значения может иметь pensionTarif - SVGDkoeff = 16 if pensionTarif == 0 else 10 - - # Сумма страховых взносов на страховую пенсию, начисленных исходя из размера годового дохода - if revenue < common.GDmax: - SVGD = (SVGDkoeff * (common.MROT * 0.26 * 12)) / 26 - else: - SVGD = (SVGDkoeff * (common.MROT * 0.26 * 12) + ((revenue - common.GDmax) * 0.01)) / 26 - - # Пенсионные коэффициенты за трудовой период - IPKtrud = (SVGD / common.MSSV) * SZPeriod * 10 - - # TODO: отследить и заменить S - return { - 'S': SZPeriod, - 'IPKtrud': IPKtrud - } - -calcPart = None -calcPartEmpl = None -calcPartSZ = None -IPKtrud = None -S = None - -if careerPlan == '1': - # Наемный работник - calcPart = calcEmpl() - # TODO: добавить проверку, проверить какое значение может возвращаться из функции и как влияет - # возврат значения тут - # - # if(calcPart === false) return false - - # Стаж - S = calcPart.S - - # Пенсионные коэффициенты - IPKtrud = calcPart.IPKtrud - -elif careerPlan == '2': - # Самозанятый - calcPart = calcSZ() - - # Стаж - S = calcPart.S - - # Пенсионные коэффициенты - IPKtrud = calcPart.IPKtrud - -elif careerPlan == '3': - # Совмещающий - calcPartEmpl = calcEmpl() - # TODO: добавить проверку, проверить какое значение может возвращаться из функции и как влияет - # возврат значения тут - # - # if(calcPartEmpl === false) return false - - calcPartSZ = calcSZ() - - # Количество пенсионных коэффициентов свыше максимально установленного значения в год, полученных при совмещённой деятельности - combinePeriod = 0 - # TODO: проверить - # if(combinePeriod.length < 1) { - # combinePeriod = 0 - # persionFormInputs.filter('#combinePeriod').val(combinePeriod) - # } - # TODO: проверить - # if combinePeriod > Math.min(calcPartEmpl.S, calcPartSZ.S)) { - # combinationWarning.show() - # $(output_area).slideDown() - # return false - # } - - # Годовой пенсионный коэффициент, получаемый гражданином в года совмещения деятельности - IPKemp = calcPartEmpl.IPKtrud * combinePeriod / calcPartEmpl.S - IPKsz = calcPartSZ.IPKtrud * combinePeriod / calcPartSZ.S - IPKo = (IPKsz + IPKemp) / combinePeriod - if IPKo > 10: - IPKo = 10 - - IPKis = IPKo * combinePeriod - - # Стаж - S = calcPartEmpl.S + calcPartSZ.S - combinePeriod - - # Пенсионные коэффициенты - IPKtrud = (calcPartSZ.IPKtrud - IPKsz) + (calcPartEmpl.IPKtrud - IPKemp) + IPKis - if combinePeriod == 0: - IPKtrud = calcPartSZ.IPKtrud + calcPartEmpl.IPKtrud - - -# Переходный период для наемных и самозанятых -if careerPlan == '1' or careerPlan == '2': - IPKtrud2015 = 0 - IPKtrud2021 = 0 - - fee = 10000 - revenue = 1 - if revenue < 0: - revenue = 0 - - # Сумма страховых взносов на страховую пенсию, начисленных исходя из размера годового дохода - SVGD = 0 - SVGDkoeff = 16 if pensionTarif == 0 else 10 - - if revenue < common.GDmax: - SVGD = (SVGDkoeff * (common.MROT * 0.26 * 12)) / 26 - else: - SVGD = (SVGDkoeff * (common.MROT * 0.26 * 12) + ((revenue - common.GDmax) * 0.01)) / 26 - - if S > 5: - IPKtrud2021 = IPKtrud * ((S - 5) / S) - - if careerPlan == '1': - IPKtrud2015 = fee / common.ZPM * 10 - - elif careerPlan == '2': - IPKtrud2015 = (SVGD / common.MSSV) * 10 - - elif S < 6: - if careerPlan == '1': - IPKtrud2015 = fee / common.ZPM * 10 - - elif careerPlan == '2': - IPKtrud2015 = (SVGD / common.MSSV) * 10 - - # KNPG = 1 - if pensionTarif == '0': - IPKtrud2015 = (S > 0 ? Math.min(8.26, IPKtrud2015) : 0) + (S > 1 ? Math.min(8.70, IPKtrud2015) : 0) + - (S > 2 ? Math.min(9.13, IPKtrud2015) : 0) + (S > 3 ? Math.min(9.57, IPKtrud2015) : 0) - - # KNPG = 0.625 - else: - IPKtrud2015 = (S > 0 ? Math.min(8.26, IPKtrud2015) : 0) + (S > 1 ? Math.min(5.43, IPKtrud2015) : 0) + - (S > 2 ? Math.min(5.71, IPKtrud2015) : 0) + (S > 3 ? Math.min(5.98, IPKtrud2015) : 0) - - IPKtrud = IPKtrud2015 + IPKtrud2021 - - -# TODO: какое-то обнуление значений в редакторе полей -# if(careerPlan == '1') { -# persionFormInputs.filter('#SZPeriod').val(0) -# persionFormInputs.filter('#revenue').val(0) -# } -# else { -# if(careerPlan == '2') { -# persionFormInputs.filter('#careerLength').val(0) -# persionFormInputs.filter('#fee').val(0) -# } -# } - -# Пенсионные коэффициенты -IPK = (IPKtrud + NK) * common.SPKop[retireWorkWithoutPension] - -# Общий стаж -OS = S + NS - -# # NOTE: Установка значений -# newCoefSummSmall.html((Math.round(IPK * 100) / 100).toString()) -# personOSsmall.html((Math.round(OS * 100) / 100).toString()) - - -var WR = OS.toString().substr(-1) - -ending.html('лет') - -if(WR == 1) { - ending.html('год') -} -else { - if(WR >= 2 && WR <= 4) { - ending.html('года') - } -} - -# пересчёт права выхода на пенсию по стажу (каждому году свой минимальный стаж) -$(output_area).hide() -if(S == 0 && OS < 8) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 1 && OS < 9) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 2 && OS < 10) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 3 && OS < 11) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 4 && OS < 12) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 5 && OS < 13) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 6 && OS < 14) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 7 && OS < 15) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -//пересчёт права выхода на пенсию по ИПК (каждому году свой минимальный ИПК) -if(S == 0 && IPK < 11.4) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 1 && IPK < 13.8) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 2 && IPK < 16.2) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 3 && IPK < 18.6) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 4 && IPK < 21) { - socialPensionWarning2.show() - $(output_area).slideDown() - return false -} - -if(S == 5 && IPK < 23.4) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 6 && IPK < 25.8) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 7 && IPK < 28.2) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -if(S == 8 && IPK < 30) { - socialPensionWarning.show() - $(output_area).slideDown() - return false -} - -# Страховая пенсия -SP = (common.FIKS * common.BPKop[retireWorkWithoutPension]) + (IPK * common.CPK) - -var newCoefSummCont = $("#newCoefSumm") -newCoefSummCont.html((Math.round(IPK * 100) / 100).toString()) - -var pensionIPartCont = $("#pensionIPart") -pensionIPartCont.html((Math.round(SP * 100) / 100).toString()) - -var personOSCont = $("#personOS") -personOSCont.html((Math.round(OS * 100) / 100).toString()) diff --git a/percentage_values_in_list.py b/percentage_values_in_list.py index 6d9aba2c6..b485ac97d 100644 --- a/percentage_values_in_list.py +++ b/percentage_values_in_list.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -26,10 +26,10 @@ def get_percentage_values(items): """Возвращает список с процентным значением элемента списка.""" sum_items = sum(items) - return ['{:.1f}%'.format(i / sum_items * 100) for i in items] + return [f"{i / sum_items * 100:.1f}%" for i in items] -if __name__ == '__main__': +if __name__ == "__main__": print(get_percentage_values([100, 100])) print(get_percentage_values([100, 100, 200])) print(get_percentage_values([100, 100, 200, 400])) diff --git a/permutations.py b/permutations.py index 326c93870..83c5c183d 100644 --- a/permutations.py +++ b/permutations.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from itertools import permutations -items = list(permutations('abc', r=3)) -print(items) # [('a', 'b', 'c'), ('a', 'c', 'b'), ('b', 'a', 'c'), ('b', 'c', 'a'), ('c', 'a', 'b'), ('c', 'b', 'a')] +items = list(permutations("abc", r=3)) +print(items) +# [('a', 'b', 'c'), ('a', 'c', 'b'), ('b', 'a', 'c'), ('b', 'c', 'a'), ('c', 'a', 'b'), ('c', 'b', 'a')] -items = list(permutations('abc', r=2)) +items = list(permutations("abc", r=2)) print(items) # [('a', 'b'), ('a', 'c'), ('b', 'a'), ('b', 'c'), ('c', 'a'), ('c', 'b')] diff --git a/photon__crawler_spider__examples/main.py b/photon__crawler_spider__examples/main.py deleted file mode 100644 index 4fa62e3ec..000000000 --- a/photon__crawler_spider__examples/main.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# SOURCE: https://github.com/s0md3v/Photon/wiki/Photon-Library -# pip install photon -import photon -photon.crawl('https://github.com/s0md3v/Photon/wiki/Photon-Library') -result = photon.result() -print(result) - -with open('result.json', 'w', encoding='utf-8') as f: - import json - json.dump(result, f, indent=4, ensure_ascii=False) diff --git a/pikabu_ru/download_svg_emotions/main.py b/pikabu_ru/download_svg_emotions/main.py new file mode 100644 index 000000000..acd5da042 --- /dev/null +++ b/pikabu_ru/download_svg_emotions/main.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from textwrap import dedent + +# pip install selenium +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.firefox.options import Options + + +DIR = Path(__file__).resolve().parent + +DIR_OUT = DIR / "out" +DIR_OUT.mkdir(parents=True, exist_ok=True) + + +options = Options() +options.add_argument("--headless") + +driver = webdriver.Firefox(options=options) +driver.implicitly_wait(10) + +try: + driver.get("https://pikabu.ru/") + print(f'Title: "{driver.title}"') + + # Содержит иконки в тегах symbol + svg_app_el = driver.find_element(By.CSS_SELECTOR, "svg.app-svg") + + # Содержит предустановленные фигуры. Без него некоторые из иконок-эмоций отображались частично + svg_defs_el = svg_app_el.find_element(By.CSS_SELECTOR, "defs") + defs = svg_defs_el.get_attribute("outerHTML") + + # Перебор и сохранение svg с эмоциями + for svg_symbol_el in svg_app_el.find_elements(By.CSS_SELECTOR, "symbol"): + el_id = svg_symbol_el.get_attribute("id") + if "icon--emotions__" in el_id: + svg_symbol = svg_symbol_el.get_attribute("outerHTML") + svg = dedent( + f"""\ + + {defs} + {svg_symbol} + + + """ + ) + file_name = DIR_OUT / f"{el_id}.svg" + print(f"Saving {file_name} ...") + + file_name.write_text(svg, encoding="utf-8") + +finally: + driver.quit() diff --git a/pikabu_ru/download_svg_emotions/out/icon--emotions__angry.svg b/pikabu_ru/download_svg_emotions/out/icon--emotions__angry.svg new file mode 100644 index 000000000..f1cc3ee26 --- /dev/null +++ b/pikabu_ru/download_svg_emotions/out/icon--emotions__angry.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/pikabu_ru/download_svg_emotions/out/icon--emotions__kind.svg b/pikabu_ru/download_svg_emotions/out/icon--emotions__kind.svg new file mode 100644 index 000000000..25adc02e9 --- /dev/null +++ b/pikabu_ru/download_svg_emotions/out/icon--emotions__kind.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/pikabu_ru/download_svg_emotions/out/icon--emotions__sad.svg b/pikabu_ru/download_svg_emotions/out/icon--emotions__sad.svg new file mode 100644 index 000000000..15f062564 --- /dev/null +++ b/pikabu_ru/download_svg_emotions/out/icon--emotions__sad.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/pikabu_ru/download_svg_emotions/out/icon--emotions__smile.svg b/pikabu_ru/download_svg_emotions/out/icon--emotions__smile.svg new file mode 100644 index 000000000..68c67cc0b --- /dev/null +++ b/pikabu_ru/download_svg_emotions/out/icon--emotions__smile.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/pikabu_ru/download_svg_emotions/out/icon--emotions__surprised.svg b/pikabu_ru/download_svg_emotions/out/icon--emotions__surprised.svg new file mode 100644 index 000000000..7d7ff8b15 --- /dev/null +++ b/pikabu_ru/download_svg_emotions/out/icon--emotions__surprised.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/pikabu_ru/get_profile_rating.py b/pikabu_ru/get_profile_rating.py new file mode 100644 index 000000000..645d5ba44 --- /dev/null +++ b/pikabu_ru/get_profile_rating.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import requests +from bs4 import BeautifulSoup + + +session = requests.Session() +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0" + + +def get_profile_rating(url: str) -> int: + rs = session.get(url) + rs.raise_for_status() + + root = BeautifulSoup(rs.content, "html.parser") + profile_digital_el = root.select_one(".profile__digital") + if not profile_digital_el: + raise Exception('Element ".profile__digital" not found!') + + rating_str = profile_digital_el["aria-label"] + + # В рейтинге могут быть не цифровые символы, типа пробелов + return int("".join(c for c in rating_str if c.isdigit())) + + +if __name__ == "__main__": + url = "https://pikabu.ru/@RytsarSvezhego" + print(get_profile_rating(url)) + # 2016 + + url = "https://pikabu.ru/@tibidohtel" + print(get_profile_rating(url)) + # 104563 diff --git a/pikabu_ru_page_interview_fullstack__question_19/154800137443598227.png b/pikabu_ru/page_interview_fullstack__question_19/154800137443598227.png similarity index 100% rename from pikabu_ru_page_interview_fullstack__question_19/154800137443598227.png rename to pikabu_ru/page_interview_fullstack__question_19/154800137443598227.png diff --git a/pikabu_ru_page_interview_fullstack__question_19/README.md b/pikabu_ru/page_interview_fullstack__question_19/README.md similarity index 100% rename from pikabu_ru_page_interview_fullstack__question_19/README.md rename to pikabu_ru/page_interview_fullstack__question_19/README.md diff --git a/pikabu_ru/page_interview_fullstack__question_19/main.py b/pikabu_ru/page_interview_fullstack__question_19/main.py new file mode 100644 index 000000000..eb9d6407d --- /dev/null +++ b/pikabu_ru/page_interview_fullstack__question_19/main.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://pikabu.ru/page/interview/fullstack/ +# 19. Секретный агент Пикабу передал зашифрованное изображение. Вам необходимо расшифровать изображение и вывести его +# на страницу средствами JS (без сторонних библиотек). Алгоритм дешифрования известен: +# * пиксели перебираются слева направо для каждой строки; +# * для каждого пикселя вычисляется параметр s += x + y * 80 (изначально s = 0 и для каждого следующего пикселя +# значение увеличивается на x + y * 80), где x - колонка пикселя, y - строка; +# * для канала красного и синего цвета необходимо добавить параметр s; +# * для канала зеленого цвета необходимо отнять параметр s следующим образом: green = (green - s) & 0xff; + + +# pip install Pillow +from PIL import Image + + +image = Image.open("154800137443598227.png") +width, height = image.size +pixel = image.load() + +s = 0 + +for y in range(height): + for x in range(width): + s += x + y * 80 + + # RGBa, alpha-канал нас не интересует, только RGB + r, g, b, _ = pixel[x, y] + + r = (r + s) & 0xFF + g = (g - s) & 0xFF + b = (b + s) & 0xFF + + pixel[x, y] = r, g, b + + +image.save("result.png") diff --git a/pikabu_ru_page_interview_fullstack__question_19/result.png b/pikabu_ru/page_interview_fullstack__question_19/result.png similarity index 100% rename from pikabu_ru_page_interview_fullstack__question_19/result.png rename to pikabu_ru/page_interview_fullstack__question_19/result.png diff --git a/pikabu_ru/profile_rating__tracking/db.py b/pikabu_ru/profile_rating__tracking/db.py new file mode 100644 index 000000000..d6f5668d0 --- /dev/null +++ b/pikabu_ru/profile_rating__tracking/db.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt +import shutil +import sys + +from pathlib import Path +from typing import Iterable, Type, TypeVar + +# pip install peewee +from peewee import ( + Model, + SqliteDatabase, + TextField, + DateTimeField, + IntegerField, + CharField, + ForeignKeyField, +) + +DIR = Path(__file__).resolve().parent + +sys.path.append(str(DIR.parent.parent)) +from shorten import shorten + + +# Absolute file name +DB_FILE_NAME = str(DIR / "db.sqlite") +DIR_BACKUP = DIR / "backup" + + +def db_create_backup(backup_dir=DIR_BACKUP) -> None: + backup_dir.mkdir(parents=True, exist_ok=True) + + file_name = str(dt.datetime.today().date()) + ".sqlite" + file_name = backup_dir / file_name + + shutil.copy(DB_FILE_NAME, file_name) + + +# Ensure foreign-key constraints are enforced. +db = SqliteDatabase(DB_FILE_NAME, pragmas={"foreign_keys": 1}) + + +ChildModel = TypeVar("ChildModel", bound="BaseModel") + + +class BaseModel(Model): + """ + Базовая модель классов-таблиц + """ + + class Meta: + database = db + + def get_new(self) -> ChildModel: + return type(self).get(self._pk_expr()) + + @classmethod + def get_first(cls) -> ChildModel: + return cls.select().first() + + @classmethod + def get_last(cls) -> ChildModel: + return cls.select().order_by(cls.id.desc()).first() + + @classmethod + def get_inherited_models(cls) -> list[Type["BaseModel"]]: + return sorted(cls.__subclasses__(), key=lambda x: x.__name__) + + @classmethod + def print_count_of_tables(cls) -> None: + items = [] + for sub_cls in cls.get_inherited_models(): + name = sub_cls.__name__ + count = sub_cls.select().count() + items.append(f"{name}: {count}") + + print(", ".join(items)) + + @classmethod + def count(cls, filters: Iterable = None) -> int: + query = cls.select() + if filters: + query = query.filter(*filters) + return query.count() + + def __str__(self) -> str: + fields = [] + for k, field in self._meta.fields.items(): + v = getattr(self, k) + + if isinstance(field, (TextField, CharField)): + if v: + v = repr(shorten(v)) + + elif isinstance(field, ForeignKeyField): + k = f"{k}_id" + if v: + v = v.id + + fields.append(f"{k}={v}") + + return self.__class__.__name__ + "(" + ", ".join(fields) + ")" + + +class ProfileRating(BaseModel): + url = TextField() + date = DateTimeField(default=dt.datetime.now) + value = IntegerField() + + @classmethod + def append(cls, url: str, value: int) -> "ProfileRating": + return cls.get_or_create(url=url, value=value)[0] + + +db.connect() +db.create_tables(BaseModel.get_inherited_models()) + + +if __name__ == "__main__": + BaseModel.print_count_of_tables() + print() + + print(ProfileRating.get_last()) diff --git a/pikabu_ru/profile_rating__tracking/main.py b/pikabu_ru/profile_rating__tracking/main.py new file mode 100644 index 000000000..91b2d3ffb --- /dev/null +++ b/pikabu_ru/profile_rating__tracking/main.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import datetime as dt +import sys +import traceback + +from pathlib import Path + +# pip install simple-wait +from simple_wait import wait + +DIR = Path(__file__).resolve().parent +sys.path.append(str(DIR.parent)) +from get_profile_rating import get_profile_rating + +from db import ProfileRating, db_create_backup + + +URL = "https://pikabu.ru/@RytsarSvezhego" + + +while True: + print(f"Started at {dt.datetime.now():%d/%m/%Y %H:%M:%S}\n") + + db_create_backup() + + try: + value = get_profile_rating(URL) + print(f"URL: {URL}\n {value}\n") + + ProfileRating.append(URL, value) + + wait(weeks=1) + + except Exception as e: + # Выводим ошибку в консоль + tb = traceback.format_exc() + print(tb) + + print("Wait 15 minutes") + wait(minutes=15) + + print() diff --git a/pikabu_ru/profile_rating__tracking/static/bootstrap-4.3.1/bootstrap.bundle.min.js b/pikabu_ru/profile_rating__tracking/static/bootstrap-4.3.1/bootstrap.bundle.min.js new file mode 100644 index 000000000..43203684c --- /dev/null +++ b/pikabu_ru/profile_rating__tracking/static/bootstrap-4.3.1/bootstrap.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.3.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery")):"function"==typeof define&&define.amd?define(["exports","jquery"],e):e((t=t||self).bootstrap={},t.jQuery)}(this,function(t,p){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)p(this._element).one(q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=n=i.clientWidth&&n>=i.clientHeight}),h=0l[t]&&!i.escapeWithReference&&(n=Math.min(h[e],l[t]-("right"===t?h.width:h.height))),Kt({},e,n)}};return c.forEach(function(t){var e=-1!==["left","top"].indexOf(t)?"primary":"secondary";h=Qt({},h,u[e](t))}),t.offsets.popper=h,t},priority:["left","right","top","bottom"],padding:5,boundariesElement:"scrollParent"},keepTogether:{order:400,enabled:!0,fn:function(t){var e=t.offsets,n=e.popper,i=e.reference,o=t.placement.split("-")[0],r=Math.floor,s=-1!==["top","bottom"].indexOf(o),a=s?"right":"bottom",l=s?"left":"top",c=s?"width":"height";return n[a]r(i[a])&&(t.offsets.popper[l]=r(i[a])),t}},arrow:{order:500,enabled:!0,fn:function(t,e){var n;if(!fe(t.instance.modifiers,"arrow","keepTogether"))return t;var i=e.element;if("string"==typeof i){if(!(i=t.instance.popper.querySelector(i)))return t}else if(!t.instance.popper.contains(i))return console.warn("WARNING: `arrow.element` must be child of its popper element!"),t;var o=t.placement.split("-")[0],r=t.offsets,s=r.popper,a=r.reference,l=-1!==["left","right"].indexOf(o),c=l?"height":"width",h=l?"Top":"Left",u=h.toLowerCase(),f=l?"left":"top",d=l?"bottom":"right",p=Zt(i)[c];a[d]-ps[d]&&(t.offsets.popper[u]+=a[u]+p-s[d]),t.offsets.popper=Vt(t.offsets.popper);var m=a[u]+a[c]/2-p/2,g=Nt(t.instance.popper),_=parseFloat(g["margin"+h],10),v=parseFloat(g["border"+h+"Width"],10),y=m-t.offsets.popper[u]-_-v;return y=Math.max(Math.min(s[c]-p,y),0),t.arrowElement=i,t.offsets.arrow=(Kt(n={},u,Math.round(y)),Kt(n,f,""),n),t},element:"[x-arrow]"},flip:{order:600,enabled:!0,fn:function(p,m){if(oe(p.instance.modifiers,"inner"))return p;if(p.flipped&&p.placement===p.originalPlacement)return p;var g=Gt(p.instance.popper,p.instance.reference,m.padding,m.boundariesElement,p.positionFixed),_=p.placement.split("-")[0],v=te(_),y=p.placement.split("-")[1]||"",E=[];switch(m.behavior){case ge:E=[_,v];break;case _e:E=me(_);break;case ve:E=me(_,!0);break;default:E=m.behavior}return E.forEach(function(t,e){if(_!==t||E.length===e+1)return p;_=p.placement.split("-")[0],v=te(_);var n,i=p.offsets.popper,o=p.offsets.reference,r=Math.floor,s="left"===_&&r(i.right)>r(o.left)||"right"===_&&r(i.left)r(o.top)||"bottom"===_&&r(i.top)r(g.right),c=r(i.top)r(g.bottom),u="left"===_&&a||"right"===_&&l||"top"===_&&c||"bottom"===_&&h,f=-1!==["top","bottom"].indexOf(_),d=!!m.flipVariations&&(f&&"start"===y&&a||f&&"end"===y&&l||!f&&"start"===y&&c||!f&&"end"===y&&h);(s||u||d)&&(p.flipped=!0,(s||u)&&(_=E[e+1]),d&&(y="end"===(n=y)?"start":"start"===n?"end":n),p.placement=_+(y?"-"+y:""),p.offsets.popper=Qt({},p.offsets.popper,ee(p.instance.popper,p.offsets.reference,p.placement)),p=ie(p.instance.modifiers,p,"flip"))}),p},behavior:"flip",padding:5,boundariesElement:"viewport"},inner:{order:700,enabled:!1,fn:function(t){var e=t.placement,n=e.split("-")[0],i=t.offsets,o=i.popper,r=i.reference,s=-1!==["left","right"].indexOf(n),a=-1===["top","left"].indexOf(n);return o[s?"left":"top"]=r[n]-(a?o[s?"width":"height"]:0),t.placement=te(e),t.offsets.popper=Vt(o),t}},hide:{order:800,enabled:!0,fn:function(t){if(!fe(t.instance.modifiers,"hide","preventOverflow"))return t;var e=t.offsets.reference,n=ne(t.instance.modifiers,function(t){return"preventOverflow"===t.name}).boundaries;if(e.bottomn.right||e.top>n.bottom||e.rightdocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
    ',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:vn},Ln="show",xn="out",Pn={HIDE:"hide"+Tn,HIDDEN:"hidden"+Tn,SHOW:"show"+Tn,SHOWN:"shown"+Tn,INSERTED:"inserted"+Tn,CLICK:"click"+Tn,FOCUSIN:"focusin"+Tn,FOCUSOUT:"focusout"+Tn,MOUSEENTER:"mouseenter"+Tn,MOUSELEAVE:"mouseleave"+Tn},Hn="fade",jn="show",Rn=".tooltip-inner",Fn=".arrow",Mn="hover",Wn="focus",Un="click",Bn="manual",qn=function(){function i(t,e){if("undefined"==typeof be)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=p(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),p(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(p(this.getTipElement()).hasClass(jn))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),p.removeData(this.element,this.constructor.DATA_KEY),p(this.element).off(this.constructor.EVENT_KEY),p(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&p(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===p(this.element).css("display"))throw new Error("Please use show on visible elements");var t=p.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){p(this.element).trigger(t);var n=m.findShadowRoot(this.element),i=p.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=m.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&p(o).addClass(Hn);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();p(o).data(this.constructor.DATA_KEY,this),p.contains(this.element.ownerDocument.documentElement,this.tip)||p(o).appendTo(l),p(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new be(this.element,o,{placement:a,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:Fn},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),p(o).addClass(jn),"ontouchstart"in document.documentElement&&p(document.body).children().on("mouseover",null,p.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,p(e.element).trigger(e.constructor.Event.SHOWN),t===xn&&e._leave(null,e)};if(p(this.tip).hasClass(Hn)){var h=m.getTransitionDurationFromElement(this.tip);p(this.tip).one(m.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=p.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==Ln&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),p(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(p(this.element).trigger(i),!i.isDefaultPrevented()){if(p(n).removeClass(jn),"ontouchstart"in document.documentElement&&p(document.body).children().off("mouseover",null,p.noop),this._activeTrigger[Un]=!1,this._activeTrigger[Wn]=!1,this._activeTrigger[Mn]=!1,p(this.tip).hasClass(Hn)){var r=m.getTransitionDurationFromElement(n);p(n).one(m.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){p(this.getTipElement()).addClass(Dn+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||p(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(p(t.querySelectorAll(Rn)),this.getTitle()),p(t).removeClass(Hn+" "+jn)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=bn(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?p(e).parent().is(t)||t.empty().append(e):t.text(p(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:m.isElement(this.config.container)?p(this.config.container):p(document).find(this.config.container)},t._getAttachment=function(t){return Nn[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)p(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Bn){var e=t===Mn?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===Mn?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;p(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),p(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||p(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),p(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Wn:Mn]=!0),p(e.getTipElement()).hasClass(jn)||e._hoverState===Ln?e._hoverState=Ln:(clearTimeout(e._timeout),e._hoverState=Ln,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===Ln&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||p(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),p(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Wn:Mn]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=xn,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===xn&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=p(this.element).data();return Object.keys(e).forEach(function(t){-1!==An.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),m.typeCheckConfig(wn,t,this.constructor.DefaultType),t.sanitize&&(t.template=bn(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=p(this.getTipElement()),e=t.attr("class").match(In);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(p(t).removeClass(Hn),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=p(this).data(Cn),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),p(this).data(Cn,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return kn}},{key:"NAME",get:function(){return wn}},{key:"DATA_KEY",get:function(){return Cn}},{key:"Event",get:function(){return Pn}},{key:"EVENT_KEY",get:function(){return Tn}},{key:"DefaultType",get:function(){return On}}]),i}();p.fn[wn]=qn._jQueryInterface,p.fn[wn].Constructor=qn,p.fn[wn].noConflict=function(){return p.fn[wn]=Sn,qn._jQueryInterface};var Kn="popover",Qn="bs.popover",Vn="."+Qn,Yn=p.fn[Kn],zn="bs-popover",Xn=new RegExp("(^|\\s)"+zn+"\\S+","g"),Gn=l({},qn.Default,{placement:"right",trigger:"click",content:"",template:''}),$n=l({},qn.DefaultType,{content:"(string|element|function)"}),Jn="fade",Zn="show",ti=".popover-header",ei=".popover-body",ni={HIDE:"hide"+Vn,HIDDEN:"hidden"+Vn,SHOW:"show"+Vn,SHOWN:"shown"+Vn,INSERTED:"inserted"+Vn,CLICK:"click"+Vn,FOCUSIN:"focusin"+Vn,FOCUSOUT:"focusout"+Vn,MOUSEENTER:"mouseenter"+Vn,MOUSELEAVE:"mouseleave"+Vn},ii=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){p(this.getTipElement()).addClass(zn+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||p(this.config.template)[0],this.tip},o.setContent=function(){var t=p(this.getTipElement());this.setElementContent(t.find(ti),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(ei),e),t.removeClass(Jn+" "+Zn)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=p(this.getTipElement()),e=t.attr("class").match(Xn);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||tcode{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23dc3545' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23dc3545' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:calc(1rem + .4rem);padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion>.card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion>.card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.accordion>.card .card-header{margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-sm .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-md .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-lg .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-xl .list-group-item:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{margin-right:0;border-top-right-radius:.25rem;border-bottom-right-radius:.25rem;border-bottom-left-radius:0}}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush .list-group-item:last-child{margin-bottom:-1px}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{margin-bottom:0;border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #dee2e6;border-bottom-right-radius:.3rem;border-bottom-left-radius:.3rem}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:0s .6s opacity}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;overflow-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/pikabu_ru/profile_rating__tracking/static/bootstrap-4.3.1/bootstrap.min.js b/pikabu_ru/profile_rating__tracking/static/bootstrap-4.3.1/bootstrap.min.js new file mode 100644 index 000000000..c4c0d1f95 --- /dev/null +++ b/pikabu_ru/profile_rating__tracking/static/bootstrap-4.3.1/bootstrap.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.3.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t=t||self).bootstrap={},t.jQuery,t.Popper)}(this,function(t,g,u){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
    ',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Ee},je="show",He="out",Re={HIDE:"hide"+De,HIDDEN:"hidden"+De,SHOW:"show"+De,SHOWN:"shown"+De,INSERTED:"inserted"+De,CLICK:"click"+De,FOCUSIN:"focusin"+De,FOCUSOUT:"focusout"+De,MOUSEENTER:"mouseenter"+De,MOUSELEAVE:"mouseleave"+De},xe="fade",Fe="show",Ue=".tooltip-inner",We=".arrow",qe="hover",Me="focus",Ke="click",Qe="manual",Be=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Fe))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(xe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,{placement:a,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:We},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),g(o).addClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===He&&e._leave(null,e)};if(g(this.tip).hasClass(xe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=g.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==je&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),g(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(g(this.element).trigger(i),!i.isDefaultPrevented()){if(g(n).removeClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ke]=!1,this._activeTrigger[Me]=!1,this._activeTrigger[qe]=!1,g(this.tip).hasClass(xe)){var r=_.getTransitionDurationFromElement(n);g(n).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Ae+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ue)),this.getTitle()),g(t).removeClass(xe+" "+Fe)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=Se(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Pe[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Qe){var e=t===qe?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===qe?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),g(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Me:qe]=!0),g(e.getTipElement()).hasClass(Fe)||e._hoverState===je?e._hoverState=je:(clearTimeout(e._timeout),e._hoverState=je,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===je&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Me:qe]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=He,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===He&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==Oe.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(be,t,this.constructor.DefaultType),t.sanitize&&(t.template=Se(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ne);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(xe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ie),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ie,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return Le}},{key:"NAME",get:function(){return be}},{key:"DATA_KEY",get:function(){return Ie}},{key:"Event",get:function(){return Re}},{key:"EVENT_KEY",get:function(){return De}},{key:"DefaultType",get:function(){return ke}}]),i}();g.fn[be]=Be._jQueryInterface,g.fn[be].Constructor=Be,g.fn[be].noConflict=function(){return g.fn[be]=we,Be._jQueryInterface};var Ve="popover",Ye="bs.popover",ze="."+Ye,Xe=g.fn[Ve],$e="bs-popover",Ge=new RegExp("(^|\\s)"+$e+"\\S+","g"),Je=l({},Be.Default,{placement:"right",trigger:"click",content:"",template:''}),Ze=l({},Be.DefaultType,{content:"(string|element|function)"}),tn="fade",en="show",nn=".popover-header",on=".popover-body",rn={HIDE:"hide"+ze,HIDDEN:"hidden"+ze,SHOW:"show"+ze,SHOWN:"shown"+ze,INSERTED:"inserted"+ze,CLICK:"click"+ze,FOCUSIN:"focusin"+ze,FOCUSOUT:"focusout"+ze,MOUSEENTER:"mouseenter"+ze,MOUSELEAVE:"mouseleave"+ze},sn=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){g(this.getTipElement()).addClass($e+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},o.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(nn),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(on),e),t.removeClass(tn+" "+en)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ge);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t1&&(a-=1)),[360*a,100*r,100*u]},a.rgb.hwb=function(t){var e=t[0],n=t[1],i=t[2];return[a.rgb.hsl(t)[0],100*(1/255*Math.min(e,Math.min(n,i))),100*(i=1-1/255*Math.max(e,Math.max(n,i)))]},a.rgb.cmyk=function(t){var e,n=t[0]/255,i=t[1]/255,a=t[2]/255;return[100*((1-n-(e=Math.min(1-n,1-i,1-a)))/(1-e)||0),100*((1-i-e)/(1-e)||0),100*((1-a-e)/(1-e)||0),100*e]},a.rgb.keyword=function(t){var i=e[t];if(i)return i;var a,r,o,s=1/0;for(var l in n)if(n.hasOwnProperty(l)){var u=n[l],d=(r=t,o=u,Math.pow(r[0]-o[0],2)+Math.pow(r[1]-o[1],2)+Math.pow(r[2]-o[2],2));d.04045?Math.pow((e+.055)/1.055,2.4):e/12.92)+.3576*(n=n>.04045?Math.pow((n+.055)/1.055,2.4):n/12.92)+.1805*(i=i>.04045?Math.pow((i+.055)/1.055,2.4):i/12.92)),100*(.2126*e+.7152*n+.0722*i),100*(.0193*e+.1192*n+.9505*i)]},a.rgb.lab=function(t){var e=a.rgb.xyz(t),n=e[0],i=e[1],r=e[2];return i/=100,r/=108.883,n=(n/=95.047)>.008856?Math.pow(n,1/3):7.787*n+16/116,[116*(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116)-16,500*(n-i),200*(i-(r=r>.008856?Math.pow(r,1/3):7.787*r+16/116))]},a.hsl.rgb=function(t){var e,n,i,a,r,o=t[0]/360,s=t[1]/100,l=t[2]/100;if(0===s)return[r=255*l,r,r];e=2*l-(n=l<.5?l*(1+s):l+s-l*s),a=[0,0,0];for(var u=0;u<3;u++)(i=o+1/3*-(u-1))<0&&i++,i>1&&i--,r=6*i<1?e+6*(n-e)*i:2*i<1?n:3*i<2?e+(n-e)*(2/3-i)*6:e,a[u]=255*r;return a},a.hsl.hsv=function(t){var e=t[0],n=t[1]/100,i=t[2]/100,a=n,r=Math.max(i,.01);return n*=(i*=2)<=1?i:2-i,a*=r<=1?r:2-r,[e,100*(0===i?2*a/(r+a):2*n/(i+n)),100*((i+n)/2)]},a.hsv.rgb=function(t){var e=t[0]/60,n=t[1]/100,i=t[2]/100,a=Math.floor(e)%6,r=e-Math.floor(e),o=255*i*(1-n),s=255*i*(1-n*r),l=255*i*(1-n*(1-r));switch(i*=255,a){case 0:return[i,l,o];case 1:return[s,i,o];case 2:return[o,i,l];case 3:return[o,s,i];case 4:return[l,o,i];case 5:return[i,o,s]}},a.hsv.hsl=function(t){var e,n,i,a=t[0],r=t[1]/100,o=t[2]/100,s=Math.max(o,.01);return i=(2-r)*o,n=r*s,[a,100*(n=(n/=(e=(2-r)*s)<=1?e:2-e)||0),100*(i/=2)]},a.hwb.rgb=function(t){var e,n,i,a,r,o,s,l=t[0]/360,u=t[1]/100,d=t[2]/100,h=u+d;switch(h>1&&(u/=h,d/=h),i=6*l-(e=Math.floor(6*l)),0!=(1&e)&&(i=1-i),a=u+i*((n=1-d)-u),e){default:case 6:case 0:r=n,o=a,s=u;break;case 1:r=a,o=n,s=u;break;case 2:r=u,o=n,s=a;break;case 3:r=u,o=a,s=n;break;case 4:r=a,o=u,s=n;break;case 5:r=n,o=u,s=a}return[255*r,255*o,255*s]},a.cmyk.rgb=function(t){var e=t[0]/100,n=t[1]/100,i=t[2]/100,a=t[3]/100;return[255*(1-Math.min(1,e*(1-a)+a)),255*(1-Math.min(1,n*(1-a)+a)),255*(1-Math.min(1,i*(1-a)+a))]},a.xyz.rgb=function(t){var e,n,i,a=t[0]/100,r=t[1]/100,o=t[2]/100;return n=-.9689*a+1.8758*r+.0415*o,i=.0557*a+-.204*r+1.057*o,e=(e=3.2406*a+-1.5372*r+-.4986*o)>.0031308?1.055*Math.pow(e,1/2.4)-.055:12.92*e,n=n>.0031308?1.055*Math.pow(n,1/2.4)-.055:12.92*n,i=i>.0031308?1.055*Math.pow(i,1/2.4)-.055:12.92*i,[255*(e=Math.min(Math.max(0,e),1)),255*(n=Math.min(Math.max(0,n),1)),255*(i=Math.min(Math.max(0,i),1))]},a.xyz.lab=function(t){var e=t[0],n=t[1],i=t[2];return n/=100,i/=108.883,e=(e/=95.047)>.008856?Math.pow(e,1/3):7.787*e+16/116,[116*(n=n>.008856?Math.pow(n,1/3):7.787*n+16/116)-16,500*(e-n),200*(n-(i=i>.008856?Math.pow(i,1/3):7.787*i+16/116))]},a.lab.xyz=function(t){var e,n,i,a=t[0];e=t[1]/500+(n=(a+16)/116),i=n-t[2]/200;var r=Math.pow(n,3),o=Math.pow(e,3),s=Math.pow(i,3);return n=r>.008856?r:(n-16/116)/7.787,e=o>.008856?o:(e-16/116)/7.787,i=s>.008856?s:(i-16/116)/7.787,[e*=95.047,n*=100,i*=108.883]},a.lab.lch=function(t){var e,n=t[0],i=t[1],a=t[2];return(e=360*Math.atan2(a,i)/2/Math.PI)<0&&(e+=360),[n,Math.sqrt(i*i+a*a),e]},a.lch.lab=function(t){var e,n=t[0],i=t[1];return e=t[2]/360*2*Math.PI,[n,i*Math.cos(e),i*Math.sin(e)]},a.rgb.ansi16=function(t){var e=t[0],n=t[1],i=t[2],r=1 in arguments?arguments[1]:a.rgb.hsv(t)[2];if(0===(r=Math.round(r/50)))return 30;var o=30+(Math.round(i/255)<<2|Math.round(n/255)<<1|Math.round(e/255));return 2===r&&(o+=60),o},a.hsv.ansi16=function(t){return a.rgb.ansi16(a.hsv.rgb(t),t[2])},a.rgb.ansi256=function(t){var e=t[0],n=t[1],i=t[2];return e===n&&n===i?e<8?16:e>248?231:Math.round((e-8)/247*24)+232:16+36*Math.round(e/255*5)+6*Math.round(n/255*5)+Math.round(i/255*5)},a.ansi16.rgb=function(t){var e=t%10;if(0===e||7===e)return t>50&&(e+=3.5),[e=e/10.5*255,e,e];var n=.5*(1+~~(t>50));return[(1&e)*n*255,(e>>1&1)*n*255,(e>>2&1)*n*255]},a.ansi256.rgb=function(t){if(t>=232){var e=10*(t-232)+8;return[e,e,e]}var n;return t-=16,[Math.floor(t/36)/5*255,Math.floor((n=t%36)/6)/5*255,n%6/5*255]},a.rgb.hex=function(t){var e=(((255&Math.round(t[0]))<<16)+((255&Math.round(t[1]))<<8)+(255&Math.round(t[2]))).toString(16).toUpperCase();return"000000".substring(e.length)+e},a.hex.rgb=function(t){var e=t.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!e)return[0,0,0];var n=e[0];3===e[0].length&&(n=n.split("").map((function(t){return t+t})).join(""));var i=parseInt(n,16);return[i>>16&255,i>>8&255,255&i]},a.rgb.hcg=function(t){var e,n=t[0]/255,i=t[1]/255,a=t[2]/255,r=Math.max(Math.max(n,i),a),o=Math.min(Math.min(n,i),a),s=r-o;return e=s<=0?0:r===n?(i-a)/s%6:r===i?2+(a-n)/s:4+(n-i)/s+4,e/=6,[360*(e%=1),100*s,100*(s<1?o/(1-s):0)]},a.hsl.hcg=function(t){var e=t[1]/100,n=t[2]/100,i=1,a=0;return(i=n<.5?2*e*n:2*e*(1-n))<1&&(a=(n-.5*i)/(1-i)),[t[0],100*i,100*a]},a.hsv.hcg=function(t){var e=t[1]/100,n=t[2]/100,i=e*n,a=0;return i<1&&(a=(n-i)/(1-i)),[t[0],100*i,100*a]},a.hcg.rgb=function(t){var e=t[0]/360,n=t[1]/100,i=t[2]/100;if(0===n)return[255*i,255*i,255*i];var a,r=[0,0,0],o=e%1*6,s=o%1,l=1-s;switch(Math.floor(o)){case 0:r[0]=1,r[1]=s,r[2]=0;break;case 1:r[0]=l,r[1]=1,r[2]=0;break;case 2:r[0]=0,r[1]=1,r[2]=s;break;case 3:r[0]=0,r[1]=l,r[2]=1;break;case 4:r[0]=s,r[1]=0,r[2]=1;break;default:r[0]=1,r[1]=0,r[2]=l}return a=(1-n)*i,[255*(n*r[0]+a),255*(n*r[1]+a),255*(n*r[2]+a)]},a.hcg.hsv=function(t){var e=t[1]/100,n=e+t[2]/100*(1-e),i=0;return n>0&&(i=e/n),[t[0],100*i,100*n]},a.hcg.hsl=function(t){var e=t[1]/100,n=t[2]/100*(1-e)+.5*e,i=0;return n>0&&n<.5?i=e/(2*n):n>=.5&&n<1&&(i=e/(2*(1-n))),[t[0],100*i,100*n]},a.hcg.hwb=function(t){var e=t[1]/100,n=e+t[2]/100*(1-e);return[t[0],100*(n-e),100*(1-n)]},a.hwb.hcg=function(t){var e=t[1]/100,n=1-t[2]/100,i=n-e,a=0;return i<1&&(a=(n-i)/(1-i)),[t[0],100*i,100*a]},a.apple.rgb=function(t){return[t[0]/65535*255,t[1]/65535*255,t[2]/65535*255]},a.rgb.apple=function(t){return[t[0]/255*65535,t[1]/255*65535,t[2]/255*65535]},a.gray.rgb=function(t){return[t[0]/100*255,t[0]/100*255,t[0]/100*255]},a.gray.hsl=a.gray.hsv=function(t){return[0,0,t[0]]},a.gray.hwb=function(t){return[0,100,t[0]]},a.gray.cmyk=function(t){return[0,0,0,t[0]]},a.gray.lab=function(t){return[t[0],0,0]},a.gray.hex=function(t){var e=255&Math.round(t[0]/100*255),n=((e<<16)+(e<<8)+e).toString(16).toUpperCase();return"000000".substring(n.length)+n},a.rgb.gray=function(t){return[(t[0]+t[1]+t[2])/3/255*100]}}));i.rgb,i.hsl,i.hsv,i.hwb,i.cmyk,i.xyz,i.lab,i.lch,i.hex,i.keyword,i.ansi16,i.ansi256,i.hcg,i.apple,i.gray;function a(t){var e=function(){for(var t={},e=Object.keys(i),n=e.length,a=0;a1&&(e=Array.prototype.slice.call(arguments));var n=t(e);if("object"==typeof n)for(var i=n.length,a=0;a1&&(e=Array.prototype.slice.call(arguments)),t(e))};return"conversion"in t&&(e.conversion=t.conversion),e}(i)}))}));var l=s,u={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},d={getRgba:h,getHsla:c,getRgb:function(t){var e=h(t);return e&&e.slice(0,3)},getHsl:function(t){var e=c(t);return e&&e.slice(0,3)},getHwb:f,getAlpha:function(t){var e=h(t);if(e)return e[3];if(e=c(t))return e[3];if(e=f(t))return e[3]},hexString:function(t,e){e=void 0!==e&&3===t.length?e:t[3];return"#"+b(t[0])+b(t[1])+b(t[2])+(e>=0&&e<1?b(Math.round(255*e)):"")},rgbString:function(t,e){if(e<1||t[3]&&t[3]<1)return g(t,e);return"rgb("+t[0]+", "+t[1]+", "+t[2]+")"},rgbaString:g,percentString:function(t,e){if(e<1||t[3]&&t[3]<1)return m(t,e);var n=Math.round(t[0]/255*100),i=Math.round(t[1]/255*100),a=Math.round(t[2]/255*100);return"rgb("+n+"%, "+i+"%, "+a+"%)"},percentaString:m,hslString:function(t,e){if(e<1||t[3]&&t[3]<1)return p(t,e);return"hsl("+t[0]+", "+t[1]+"%, "+t[2]+"%)"},hslaString:p,hwbString:function(t,e){void 0===e&&(e=void 0!==t[3]?t[3]:1);return"hwb("+t[0]+", "+t[1]+"%, "+t[2]+"%"+(void 0!==e&&1!==e?", "+e:"")+")"},keyword:function(t){return y[t.slice(0,3)]}};function h(t){if(t){var e=[0,0,0],n=1,i=t.match(/^#([a-fA-F0-9]{3,4})$/i),a="";if(i){a=(i=i[1])[3];for(var r=0;rn?(e+.05)/(n+.05):(n+.05)/(e+.05)},level:function(t){var e=this.contrast(t);return e>=7.1?"AAA":e>=4.5?"AA":""},dark:function(){var t=this.values.rgb;return(299*t[0]+587*t[1]+114*t[2])/1e3<128},light:function(){return!this.dark()},negate:function(){for(var t=[],e=0;e<3;e++)t[e]=255-this.values.rgb[e];return this.setValues("rgb",t),this},lighten:function(t){var e=this.values.hsl;return e[2]+=e[2]*t,this.setValues("hsl",e),this},darken:function(t){var e=this.values.hsl;return e[2]-=e[2]*t,this.setValues("hsl",e),this},saturate:function(t){var e=this.values.hsl;return e[1]+=e[1]*t,this.setValues("hsl",e),this},desaturate:function(t){var e=this.values.hsl;return e[1]-=e[1]*t,this.setValues("hsl",e),this},whiten:function(t){var e=this.values.hwb;return e[1]+=e[1]*t,this.setValues("hwb",e),this},blacken:function(t){var e=this.values.hwb;return e[2]+=e[2]*t,this.setValues("hwb",e),this},greyscale:function(){var t=this.values.rgb,e=.3*t[0]+.59*t[1]+.11*t[2];return this.setValues("rgb",[e,e,e]),this},clearer:function(t){var e=this.values.alpha;return this.setValues("alpha",e-e*t),this},opaquer:function(t){var e=this.values.alpha;return this.setValues("alpha",e+e*t),this},rotate:function(t){var e=this.values.hsl,n=(e[0]+t)%360;return e[0]=n<0?360+n:n,this.setValues("hsl",e),this},mix:function(t,e){var n=t,i=void 0===e?.5:e,a=2*i-1,r=this.alpha()-n.alpha(),o=((a*r==-1?a:(a+r)/(1+a*r))+1)/2,s=1-o;return this.rgb(o*this.red()+s*n.red(),o*this.green()+s*n.green(),o*this.blue()+s*n.blue()).alpha(this.alpha()*i+n.alpha()*(1-i))},toJSON:function(){return this.rgb()},clone:function(){var t,e,n=new _,i=this.values,a=n.values;for(var r in i)i.hasOwnProperty(r)&&(t=i[r],"[object Array]"===(e={}.toString.call(t))?a[r]=t.slice(0):"[object Number]"===e?a[r]=t:console.error("unexpected color value:",t));return n}},_.prototype.spaces={rgb:["red","green","blue"],hsl:["hue","saturation","lightness"],hsv:["hue","saturation","value"],hwb:["hue","whiteness","blackness"],cmyk:["cyan","magenta","yellow","black"]},_.prototype.maxes={rgb:[255,255,255],hsl:[360,100,100],hsv:[360,100,100],hwb:[360,100,100],cmyk:[100,100,100,100]},_.prototype.getValues=function(t){for(var e=this.values,n={},i=0;i=0;a--)e.call(n,t[a],a);else for(a=0;a=1?t:-(Math.sqrt(1-t*t)-1)},easeOutCirc:function(t){return Math.sqrt(1-(t-=1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:1===t?1:(n||(n=.3),i<1?(i=1,e=n/4):e=n/(2*Math.PI)*Math.asin(1/i),-i*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/n))},easeOutElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:1===t?1:(n||(n=.3),i<1?(i=1,e=n/4):e=n/(2*Math.PI)*Math.asin(1/i),i*Math.pow(2,-10*t)*Math.sin((t-e)*(2*Math.PI)/n)+1)},easeInOutElastic:function(t){var e=1.70158,n=0,i=1;return 0===t?0:2==(t/=.5)?1:(n||(n=.45),i<1?(i=1,e=n/4):e=n/(2*Math.PI)*Math.asin(1/i),t<1?i*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/n)*-.5:i*Math.pow(2,-10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/n)*.5+1)},easeInBack:function(t){var e=1.70158;return t*t*((e+1)*t-e)},easeOutBack:function(t){var e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack:function(t){var e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:function(t){return 1-D.easeOutBounce(1-t)},easeOutBounce:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},easeInOutBounce:function(t){return t<.5?.5*D.easeInBounce(2*t):.5*D.easeOutBounce(2*t-1)+.5}},C={effects:D};S.easingEffects=D;var P=Math.PI,T=P/180,O=2*P,A=P/2,F=P/4,I=2*P/3,L={clear:function(t){t.ctx.clearRect(0,0,t.width,t.height)},roundedRect:function(t,e,n,i,a,r){if(r){var o=Math.min(r,a/2,i/2),s=e+o,l=n+o,u=e+i-o,d=n+a-o;t.moveTo(e,l),se.left-1e-6&&t.xe.top-1e-6&&t.y0&&this.requestAnimationFrame()},advance:function(){for(var t,e,n,i,a=this.animations,r=0;r=n?(H.callback(t.onAnimationComplete,[t],e),e.animating=!1,a.splice(r,1)):++r}},Q=H.options.resolve,tt=["push","pop","shift","splice","unshift"];function et(t,e){var n=t._chartjs;if(n){var i=n.listeners,a=i.indexOf(e);-1!==a&&i.splice(a,1),i.length>0||(tt.forEach((function(e){delete t[e]})),delete t._chartjs)}}var nt=function(t,e){this.initialize(t,e)};H.extend(nt.prototype,{datasetElementType:null,dataElementType:null,_datasetElementOptions:["backgroundColor","borderCapStyle","borderColor","borderDash","borderDashOffset","borderJoinStyle","borderWidth"],_dataElementOptions:["backgroundColor","borderColor","borderWidth","pointStyle"],initialize:function(t,e){var n=this;n.chart=t,n.index=e,n.linkScales(),n.addElements(),n._type=n.getMeta().type},updateIndex:function(t){this.index=t},linkScales:function(){var t=this.getMeta(),e=this.chart,n=e.scales,i=this.getDataset(),a=e.options.scales;null!==t.xAxisID&&t.xAxisID in n&&!i.xAxisID||(t.xAxisID=i.xAxisID||a.xAxes[0].id),null!==t.yAxisID&&t.yAxisID in n&&!i.yAxisID||(t.yAxisID=i.yAxisID||a.yAxes[0].id)},getDataset:function(){return this.chart.data.datasets[this.index]},getMeta:function(){return this.chart.getDatasetMeta(this.index)},getScaleForId:function(t){return this.chart.scales[t]},_getValueScaleId:function(){return this.getMeta().yAxisID},_getIndexScaleId:function(){return this.getMeta().xAxisID},_getValueScale:function(){return this.getScaleForId(this._getValueScaleId())},_getIndexScale:function(){return this.getScaleForId(this._getIndexScaleId())},reset:function(){this._update(!0)},destroy:function(){this._data&&et(this._data,this)},createMetaDataset:function(){var t=this.datasetElementType;return t&&new t({_chart:this.chart,_datasetIndex:this.index})},createMetaData:function(t){var e=this.dataElementType;return e&&new e({_chart:this.chart,_datasetIndex:this.index,_index:t})},addElements:function(){var t,e,n=this.getMeta(),i=this.getDataset().data||[],a=n.data;for(t=0,e=i.length;tn&&this.insertElements(n,i-n)},insertElements:function(t,e){for(var n=0;na?(r=a/e.innerRadius,t.arc(o,s,e.innerRadius-a,i+r,n-r,!0)):t.arc(o,s,a,i+Math.PI/2,n-Math.PI/2),t.closePath(),t.clip()}function ot(t,e,n){var i="inner"===e.borderAlign;i?(t.lineWidth=2*e.borderWidth,t.lineJoin="round"):(t.lineWidth=e.borderWidth,t.lineJoin="bevel"),n.fullCircles&&function(t,e,n,i){var a,r=n.endAngle;for(i&&(n.endAngle=n.startAngle+at,rt(t,n),n.endAngle=r,n.endAngle===n.startAngle&&n.fullCircles&&(n.endAngle+=at,n.fullCircles--)),t.beginPath(),t.arc(n.x,n.y,n.innerRadius,n.startAngle+at,n.startAngle,!0),a=0;as;)a-=at;for(;a=o&&a<=s,u=r>=n.innerRadius&&r<=n.outerRadius;return l&&u}return!1},getCenterPoint:function(){var t=this._view,e=(t.startAngle+t.endAngle)/2,n=(t.innerRadius+t.outerRadius)/2;return{x:t.x+Math.cos(e)*n,y:t.y+Math.sin(e)*n}},getArea:function(){var t=this._view;return Math.PI*((t.endAngle-t.startAngle)/(2*Math.PI))*(Math.pow(t.outerRadius,2)-Math.pow(t.innerRadius,2))},tooltipPosition:function(){var t=this._view,e=t.startAngle+(t.endAngle-t.startAngle)/2,n=(t.outerRadius-t.innerRadius)/2+t.innerRadius;return{x:t.x+Math.cos(e)*n,y:t.y+Math.sin(e)*n}},draw:function(){var t,e=this._chart.ctx,n=this._view,i="inner"===n.borderAlign?.33:0,a={x:n.x,y:n.y,innerRadius:n.innerRadius,outerRadius:Math.max(n.outerRadius-i,0),pixelMargin:i,startAngle:n.startAngle,endAngle:n.endAngle,fullCircles:Math.floor(n.circumference/at)};if(e.save(),e.fillStyle=n.backgroundColor,e.strokeStyle=n.borderColor,a.fullCircles){for(a.endAngle=a.startAngle+at,e.beginPath(),e.arc(a.x,a.y,a.outerRadius,a.startAngle,a.endAngle),e.arc(a.x,a.y,a.innerRadius,a.endAngle,a.startAngle,!0),e.closePath(),t=0;tt.x&&(e=bt(e,"left","right")):t.basen?n:i,r:l.right||a<0?0:a>e?e:a,b:l.bottom||r<0?0:r>n?n:r,l:l.left||o<0?0:o>e?e:o}}function xt(t,e,n){var i=null===e,a=null===n,r=!(!t||i&&a)&&vt(t);return r&&(i||e>=r.left&&e<=r.right)&&(a||n>=r.top&&n<=r.bottom)}W._set("global",{elements:{rectangle:{backgroundColor:mt,borderColor:mt,borderSkipped:"bottom",borderWidth:0}}});var _t=$.extend({_type:"rectangle",draw:function(){var t=this._chart.ctx,e=this._view,n=function(t){var e=vt(t),n=e.right-e.left,i=e.bottom-e.top,a=yt(t,n/2,i/2);return{outer:{x:e.left,y:e.top,w:n,h:i},inner:{x:e.left+a.l,y:e.top+a.t,w:n-a.l-a.r,h:i-a.t-a.b}}}(e),i=n.outer,a=n.inner;t.fillStyle=e.backgroundColor,t.fillRect(i.x,i.y,i.w,i.h),i.w===a.w&&i.h===a.h||(t.save(),t.beginPath(),t.rect(i.x,i.y,i.w,i.h),t.clip(),t.fillStyle=e.borderColor,t.rect(a.x,a.y,a.w,a.h),t.fill("evenodd"),t.restore())},height:function(){var t=this._view;return t.base-t.y},inRange:function(t,e){return xt(this._view,t,e)},inLabelRange:function(t,e){var n=this._view;return pt(n)?xt(n,t,null):xt(n,null,e)},inXRange:function(t){return xt(this._view,t,null)},inYRange:function(t){return xt(this._view,null,t)},getCenterPoint:function(){var t,e,n=this._view;return pt(n)?(t=n.x,e=(n.y+n.base)/2):(t=(n.x+n.base)/2,e=n.y),{x:t,y:e}},getArea:function(){var t=this._view;return pt(t)?t.width*Math.abs(t.y-t.base):t.height*Math.abs(t.x-t.base)},tooltipPosition:function(){var t=this._view;return{x:t.x,y:t.y}}}),wt={},kt=st,Mt=dt,St=gt,Dt=_t;wt.Arc=kt,wt.Line=Mt,wt.Point=St,wt.Rectangle=Dt;var Ct=H._deprecated,Pt=H.valueOrDefault;function Tt(t,e,n){var i,a,r=n.barThickness,o=e.stackCount,s=e.pixels[t],l=H.isNullOrUndef(r)?function(t,e){var n,i,a,r,o=t._length;for(a=1,r=e.length;a0?Math.min(o,Math.abs(i-n)):o,n=i;return o}(e.scale,e.pixels):-1;return H.isNullOrUndef(r)?(i=l*n.categoryPercentage,a=n.barPercentage):(i=r*o,a=1),{chunk:i/o,ratio:a,start:s-i/2}}W._set("bar",{hover:{mode:"label"},scales:{xAxes:[{type:"category",offset:!0,gridLines:{offsetGridLines:!0}}],yAxes:[{type:"linear"}]}}),W._set("global",{datasets:{bar:{categoryPercentage:.8,barPercentage:.9}}});var Ot=it.extend({dataElementType:wt.Rectangle,_dataElementOptions:["backgroundColor","borderColor","borderSkipped","borderWidth","barPercentage","barThickness","categoryPercentage","maxBarThickness","minBarLength"],initialize:function(){var t,e,n=this;it.prototype.initialize.apply(n,arguments),(t=n.getMeta()).stack=n.getDataset().stack,t.bar=!0,e=n._getIndexScale().options,Ct("bar chart",e.barPercentage,"scales.[x/y]Axes.barPercentage","dataset.barPercentage"),Ct("bar chart",e.barThickness,"scales.[x/y]Axes.barThickness","dataset.barThickness"),Ct("bar chart",e.categoryPercentage,"scales.[x/y]Axes.categoryPercentage","dataset.categoryPercentage"),Ct("bar chart",n._getValueScale().options.minBarLength,"scales.[x/y]Axes.minBarLength","dataset.minBarLength"),Ct("bar chart",e.maxBarThickness,"scales.[x/y]Axes.maxBarThickness","dataset.maxBarThickness")},update:function(t){var e,n,i=this.getMeta().data;for(this._ruler=this.getRuler(),e=0,n=i.length;e=0&&m.min>=0?m.min:m.max,x=void 0===m.start?m.end:m.max>=0&&m.min>=0?m.max-m.min:m.min-m.max,_=g.length;if(v||void 0===v&&void 0!==b)for(i=0;i<_&&(a=g[i]).index!==t;++i)a.stack===b&&(r=void 0===(u=h._parseValue(f[a.index].data[e])).start?u.end:u.min>=0&&u.max>=0?u.max:u.min,(m.min<0&&r<0||m.max>=0&&r>0)&&(y+=r));return o=h.getPixelForValue(y),l=(s=h.getPixelForValue(y+x))-o,void 0!==p&&Math.abs(l)=0&&!c||x<0&&c?o-p:o+p),{size:l,base:o,head:s,center:s+l/2}},calculateBarIndexPixels:function(t,e,n,i){var a="flex"===i.barThickness?function(t,e,n){var i,a=e.pixels,r=a[t],o=t>0?a[t-1]:null,s=t=Rt?-Nt:b<-Rt?Nt:0)+p,x=Math.cos(b),_=Math.sin(b),w=Math.cos(y),k=Math.sin(y),M=b<=0&&y>=0||y>=Nt,S=b<=Wt&&y>=Wt||y>=Nt+Wt,D=b<=-Wt&&y>=-Wt||y>=Rt+Wt,C=b===-Rt||y>=Rt?-1:Math.min(x,x*m,w,w*m),P=D?-1:Math.min(_,_*m,k,k*m),T=M?1:Math.max(x,x*m,w,w*m),O=S?1:Math.max(_,_*m,k,k*m);u=(T-C)/2,d=(O-P)/2,h=-(T+C)/2,c=-(O+P)/2}for(i=0,a=g.length;i0&&!isNaN(t)?Nt*(Math.abs(t)/e):0},getMaxBorderWidth:function(t){var e,n,i,a,r,o,s,l,u=0,d=this.chart;if(!t)for(e=0,n=d.data.datasets.length;e(u=s>u?s:u)?l:u);return u},setHoverStyle:function(t){var e=t._model,n=t._options,i=H.getHoverColor;t.$previousStyle={backgroundColor:e.backgroundColor,borderColor:e.borderColor,borderWidth:e.borderWidth},e.backgroundColor=Lt(n.hoverBackgroundColor,i(n.backgroundColor)),e.borderColor=Lt(n.hoverBorderColor,i(n.borderColor)),e.borderWidth=Lt(n.hoverBorderWidth,n.borderWidth)},_getRingWeightOffset:function(t){for(var e=0,n=0;n0&&Ht(l[t-1]._model,s)&&(n.controlPointPreviousX=u(n.controlPointPreviousX,s.left,s.right),n.controlPointPreviousY=u(n.controlPointPreviousY,s.top,s.bottom)),t0&&(r=t.getDatasetMeta(r[0]._datasetIndex).data),r},"x-axis":function(t,e){return ae(t,e,{intersect:!1})},point:function(t,e){return ee(t,Qt(e,t))},nearest:function(t,e,n){var i=Qt(e,t);n.axis=n.axis||"xy";var a=ie(n.axis);return ne(t,i,n.intersect,a)},x:function(t,e,n){var i=Qt(e,t),a=[],r=!1;return te(t,(function(t){t.inXRange(i.x)&&a.push(t),t.inRange(i.x,i.y)&&(r=!0)})),n.intersect&&!r&&(a=[]),a},y:function(t,e,n){var i=Qt(e,t),a=[],r=!1;return te(t,(function(t){t.inYRange(i.y)&&a.push(t),t.inRange(i.x,i.y)&&(r=!0)})),n.intersect&&!r&&(a=[]),a}}},oe=H.extend;function se(t,e){return H.where(t,(function(t){return t.pos===e}))}function le(t,e){return t.sort((function(t,n){var i=e?n:t,a=e?t:n;return i.weight===a.weight?i.index-a.index:i.weight-a.weight}))}function ue(t,e,n,i){return Math.max(t[n],e[n])+Math.max(t[i],e[i])}function de(t,e,n){var i,a,r=n.box,o=t.maxPadding;if(n.size&&(t[n.pos]-=n.size),n.size=n.horizontal?r.height:r.width,t[n.pos]+=n.size,r.getPadding){var s=r.getPadding();o.top=Math.max(o.top,s.top),o.left=Math.max(o.left,s.left),o.bottom=Math.max(o.bottom,s.bottom),o.right=Math.max(o.right,s.right)}if(i=e.outerWidth-ue(o,t,"left","right"),a=e.outerHeight-ue(o,t,"top","bottom"),i!==t.w||a!==t.h)return t.w=i,t.h=a,n.horizontal?i!==t.w:a!==t.h}function he(t,e){var n=e.maxPadding;function i(t){var i={left:0,top:0,right:0,bottom:0};return t.forEach((function(t){i[t]=Math.max(e[t],n[t])})),i}return i(t?["left","right"]:["top","bottom"])}function ce(t,e,n){var i,a,r,o,s,l,u=[];for(i=0,a=t.length;idiv{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}"}))&&ge.default||ge,ve="$chartjs",be="chartjs-size-monitor",ye="chartjs-render-monitor",xe="chartjs-render-animation",_e=["animationstart","webkitAnimationStart"],we={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"};function ke(t,e){var n=H.getStyle(t,e),i=n&&n.match(/^(\d+)(\.\d+)?px$/);return i?Number(i[1]):void 0}var Me=!!function(){var t=!1;try{var e=Object.defineProperty({},"passive",{get:function(){t=!0}});window.addEventListener("e",null,e)}catch(t){}return t}()&&{passive:!0};function Se(t,e,n){t.addEventListener(e,n,Me)}function De(t,e,n){t.removeEventListener(e,n,Me)}function Ce(t,e,n,i,a){return{type:t,chart:e,native:a||null,x:void 0!==n?n:null,y:void 0!==i?i:null}}function Pe(t){var e=document.createElement("div");return e.className=t||"",e}function Te(t,e,n){var i,a,r,o,s=t[ve]||(t[ve]={}),l=s.resizer=function(t){var e=Pe(be),n=Pe(be+"-expand"),i=Pe(be+"-shrink");n.appendChild(Pe()),i.appendChild(Pe()),e.appendChild(n),e.appendChild(i),e._reset=function(){n.scrollLeft=1e6,n.scrollTop=1e6,i.scrollLeft=1e6,i.scrollTop=1e6};var a=function(){e._reset(),t()};return Se(n,"scroll",a.bind(n,"expand")),Se(i,"scroll",a.bind(i,"shrink")),e}((i=function(){if(s.resizer){var i=n.options.maintainAspectRatio&&t.parentNode,a=i?i.clientWidth:0;e(Ce("resize",n)),i&&i.clientWidth0){var r=t[0];r.label?n=r.label:r.xLabel?n=r.xLabel:a>0&&r.index-1?t.split("\n"):t}function Ve(t){var e=W.global;return{xPadding:t.xPadding,yPadding:t.yPadding,xAlign:t.xAlign,yAlign:t.yAlign,rtl:t.rtl,textDirection:t.textDirection,bodyFontColor:t.bodyFontColor,_bodyFontFamily:Ne(t.bodyFontFamily,e.defaultFontFamily),_bodyFontStyle:Ne(t.bodyFontStyle,e.defaultFontStyle),_bodyAlign:t.bodyAlign,bodyFontSize:Ne(t.bodyFontSize,e.defaultFontSize),bodySpacing:t.bodySpacing,titleFontColor:t.titleFontColor,_titleFontFamily:Ne(t.titleFontFamily,e.defaultFontFamily),_titleFontStyle:Ne(t.titleFontStyle,e.defaultFontStyle),titleFontSize:Ne(t.titleFontSize,e.defaultFontSize),_titleAlign:t.titleAlign,titleSpacing:t.titleSpacing,titleMarginBottom:t.titleMarginBottom,footerFontColor:t.footerFontColor,_footerFontFamily:Ne(t.footerFontFamily,e.defaultFontFamily),_footerFontStyle:Ne(t.footerFontStyle,e.defaultFontStyle),footerFontSize:Ne(t.footerFontSize,e.defaultFontSize),_footerAlign:t.footerAlign,footerSpacing:t.footerSpacing,footerMarginTop:t.footerMarginTop,caretSize:t.caretSize,cornerRadius:t.cornerRadius,backgroundColor:t.backgroundColor,opacity:0,legendColorBackground:t.multiKeyBackground,displayColors:t.displayColors,borderColor:t.borderColor,borderWidth:t.borderWidth}}function He(t,e){return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-t.xPadding:t.x+t.xPadding}function Be(t){return ze([],Ee(t))}var je=$.extend({initialize:function(){this._model=Ve(this._options),this._lastActive=[]},getTitle:function(){var t=this,e=t._options,n=e.callbacks,i=n.beforeTitle.apply(t,arguments),a=n.title.apply(t,arguments),r=n.afterTitle.apply(t,arguments),o=[];return o=ze(o,Ee(i)),o=ze(o,Ee(a)),o=ze(o,Ee(r))},getBeforeBody:function(){return Be(this._options.callbacks.beforeBody.apply(this,arguments))},getBody:function(t,e){var n=this,i=n._options.callbacks,a=[];return H.each(t,(function(t){var r={before:[],lines:[],after:[]};ze(r.before,Ee(i.beforeLabel.call(n,t,e))),ze(r.lines,i.label.call(n,t,e)),ze(r.after,Ee(i.afterLabel.call(n,t,e))),a.push(r)})),a},getAfterBody:function(){return Be(this._options.callbacks.afterBody.apply(this,arguments))},getFooter:function(){var t=this,e=t._options.callbacks,n=e.beforeFooter.apply(t,arguments),i=e.footer.apply(t,arguments),a=e.afterFooter.apply(t,arguments),r=[];return r=ze(r,Ee(n)),r=ze(r,Ee(i)),r=ze(r,Ee(a))},update:function(t){var e,n,i,a,r,o,s,l,u,d,h=this,c=h._options,f=h._model,g=h._model=Ve(c),m=h._active,p=h._data,v={xAlign:f.xAlign,yAlign:f.yAlign},b={x:f.x,y:f.y},y={width:f.width,height:f.height},x={x:f.caretX,y:f.caretY};if(m.length){g.opacity=1;var _=[],w=[];x=Ye[c.position].call(h,m,h._eventPosition);var k=[];for(e=0,n=m.length;ei.width&&(a=i.width-e.width),a<0&&(a=0)),"top"===d?r+=h:r-="bottom"===d?e.height+h:e.height/2,"center"===d?"left"===u?a+=h:"right"===u&&(a-=h):"left"===u?a-=c:"right"===u&&(a+=c),{x:a,y:r}}(g,y,v=function(t,e){var n,i,a,r,o,s=t._model,l=t._chart,u=t._chart.chartArea,d="center",h="center";s.yl.height-e.height&&(h="bottom");var c=(u.left+u.right)/2,f=(u.top+u.bottom)/2;"center"===h?(n=function(t){return t<=c},i=function(t){return t>c}):(n=function(t){return t<=e.width/2},i=function(t){return t>=l.width-e.width/2}),a=function(t){return t+e.width+s.caretSize+s.caretPadding>l.width},r=function(t){return t-e.width-s.caretSize-s.caretPadding<0},o=function(t){return t<=f?"top":"bottom"},n(s.x)?(d="left",a(s.x)&&(d="center",h=o(s.y))):i(s.x)&&(d="right",r(s.x)&&(d="center",h=o(s.y)));var g=t._options;return{xAlign:g.xAlign?g.xAlign:d,yAlign:g.yAlign?g.yAlign:h}}(this,y),h._chart)}else g.opacity=0;return g.xAlign=v.xAlign,g.yAlign=v.yAlign,g.x=b.x,g.y=b.y,g.width=y.width,g.height=y.height,g.caretX=x.x,g.caretY=x.y,h._model=g,t&&c.custom&&c.custom.call(h,g),h},drawCaret:function(t,e){var n=this._chart.ctx,i=this._view,a=this.getCaretPosition(t,e,i);n.lineTo(a.x1,a.y1),n.lineTo(a.x2,a.y2),n.lineTo(a.x3,a.y3)},getCaretPosition:function(t,e,n){var i,a,r,o,s,l,u=n.caretSize,d=n.cornerRadius,h=n.xAlign,c=n.yAlign,f=t.x,g=t.y,m=e.width,p=e.height;if("center"===c)s=g+p/2,"left"===h?(a=(i=f)-u,r=i,o=s+u,l=s-u):(a=(i=f+m)+u,r=i,o=s-u,l=s+u);else if("left"===h?(i=(a=f+d+u)-u,r=a+u):"right"===h?(i=(a=f+m-d-u)-u,r=a+u):(i=(a=n.caretX)-u,r=a+u),"top"===c)s=(o=g)-u,l=o;else{s=(o=g+p)+u,l=o;var v=r;r=i,i=v}return{x1:i,x2:a,x3:r,y1:o,y2:s,y3:l}},drawTitle:function(t,e,n){var i,a,r,o=e.title,s=o.length;if(s){var l=We(e.rtl,e.x,e.width);for(t.x=He(e,e._titleAlign),n.textAlign=l.textAlign(e._titleAlign),n.textBaseline="middle",i=e.titleFontSize,a=e.titleSpacing,n.fillStyle=e.titleFontColor,n.font=H.fontString(i,e._titleFontStyle,e._titleFontFamily),r=0;r0&&n.stroke()},draw:function(){var t=this._chart.ctx,e=this._view;if(0!==e.opacity){var n={width:e.width,height:e.height},i={x:e.x,y:e.y},a=Math.abs(e.opacity<.001)?0:e.opacity,r=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;this._options.enabled&&r&&(t.save(),t.globalAlpha=a,this.drawBackground(i,e,t,n),i.y+=e.yPadding,H.rtl.overrideTextDirection(t,e.textDirection),this.drawTitle(i,e,t),this.drawBody(i,e,t),this.drawFooter(i,e,t),H.rtl.restoreTextDirection(t,e.textDirection),t.restore())}},handleEvent:function(t){var e,n=this,i=n._options;return n._lastActive=n._lastActive||[],"mouseout"===t.type?n._active=[]:(n._active=n._chart.getElementsAtEventForMode(t,i.mode,i),i.reverse&&n._active.reverse()),(e=!H.arrayEquals(n._active,n._lastActive))&&(n._lastActive=n._active,(i.enabled||i.custom)&&(n._eventPosition={x:t.x,y:t.y},n.update(!0),n.pivot())),e}}),Ue=Ye,Ge=je;Ge.positioners=Ue;var qe=H.valueOrDefault;function Ze(){return H.merge({},[].slice.call(arguments),{merger:function(t,e,n,i){if("xAxes"===t||"yAxes"===t){var a,r,o,s=n[t].length;for(e[t]||(e[t]=[]),a=0;a=e[t].length&&e[t].push({}),!e[t][a].type||o.type&&o.type!==e[t][a].type?H.merge(e[t][a],[Re.getScaleDefaults(r),o]):H.merge(e[t][a],o)}else H._merger(t,e,n,i)}})}function $e(){return H.merge({},[].slice.call(arguments),{merger:function(t,e,n,i){var a=e[t]||{},r=n[t];"scales"===t?e[t]=Ze(a,r):"scale"===t?e[t]=H.merge(a,[Re.getScaleDefaults(r.type),r]):H._merger(t,e,n,i)}})}function Xe(t){var e=t.options;H.each(t.scales,(function(e){me.removeBox(t,e)})),e=$e(W.global,W[t.config.type],e),t.options=t.config.options=e,t.ensureScalesHaveIDs(),t.buildOrUpdateScales(),t.tooltip._options=e.tooltips,t.tooltip.initialize()}function Ke(t,e,n){var i,a=function(t){return t.id===i};do{i=e+n++}while(H.findIndex(t,a)>=0);return i}function Je(t){return"top"===t||"bottom"===t}function Qe(t,e){return function(n,i){return n[t]===i[t]?n[e]-i[e]:n[t]-i[t]}}W._set("global",{elements:{},events:["mousemove","mouseout","click","touchstart","touchmove"],hover:{onHover:null,mode:"nearest",intersect:!0,animationDuration:400},onClick:null,maintainAspectRatio:!0,responsive:!0,responsiveAnimationDuration:0});var tn=function(t,e){return this.construct(t,e),this};H.extend(tn.prototype,{construct:function(t,e){var n=this;e=function(t){var e=(t=t||{}).data=t.data||{};return e.datasets=e.datasets||[],e.labels=e.labels||[],t.options=$e(W.global,W[t.type],t.options||{}),t}(e);var i=Ie.acquireContext(t,e),a=i&&i.canvas,r=a&&a.height,o=a&&a.width;n.id=H.uid(),n.ctx=i,n.canvas=a,n.config=e,n.width=o,n.height=r,n.aspectRatio=r?o/r:null,n.options=e.options,n._bufferedRender=!1,n._layers=[],n.chart=n,n.controller=n,tn.instances[n.id]=n,Object.defineProperty(n,"data",{get:function(){return n.config.data},set:function(t){n.config.data=t}}),i&&a?(n.initialize(),n.update()):console.error("Failed to create chart: can't acquire context from the given item")},initialize:function(){var t=this;return Le.notify(t,"beforeInit"),H.retinaScale(t,t.options.devicePixelRatio),t.bindEvents(),t.options.responsive&&t.resize(!0),t.initToolTip(),Le.notify(t,"afterInit"),t},clear:function(){return H.canvas.clear(this),this},stop:function(){return J.cancelAnimation(this),this},resize:function(t){var e=this,n=e.options,i=e.canvas,a=n.maintainAspectRatio&&e.aspectRatio||null,r=Math.max(0,Math.floor(H.getMaximumWidth(i))),o=Math.max(0,Math.floor(a?r/a:H.getMaximumHeight(i)));if((e.width!==r||e.height!==o)&&(i.width=e.width=r,i.height=e.height=o,i.style.width=r+"px",i.style.height=o+"px",H.retinaScale(e,n.devicePixelRatio),!t)){var s={width:r,height:o};Le.notify(e,"resize",[s]),n.onResize&&n.onResize(e,s),e.stop(),e.update({duration:n.responsiveAnimationDuration})}},ensureScalesHaveIDs:function(){var t=this.options,e=t.scales||{},n=t.scale;H.each(e.xAxes,(function(t,n){t.id||(t.id=Ke(e.xAxes,"x-axis-",n))})),H.each(e.yAxes,(function(t,n){t.id||(t.id=Ke(e.yAxes,"y-axis-",n))})),n&&(n.id=n.id||"scale")},buildOrUpdateScales:function(){var t=this,e=t.options,n=t.scales||{},i=[],a=Object.keys(n).reduce((function(t,e){return t[e]=!1,t}),{});e.scales&&(i=i.concat((e.scales.xAxes||[]).map((function(t){return{options:t,dtype:"category",dposition:"bottom"}})),(e.scales.yAxes||[]).map((function(t){return{options:t,dtype:"linear",dposition:"left"}})))),e.scale&&i.push({options:e.scale,dtype:"radialLinear",isDefault:!0,dposition:"chartArea"}),H.each(i,(function(e){var i=e.options,r=i.id,o=qe(i.type,e.dtype);Je(i.position)!==Je(e.dposition)&&(i.position=e.dposition),a[r]=!0;var s=null;if(r in n&&n[r].type===o)(s=n[r]).options=i,s.ctx=t.ctx,s.chart=t;else{var l=Re.getScaleConstructor(o);if(!l)return;s=new l({id:r,type:o,options:i,ctx:t.ctx,chart:t}),n[s.id]=s}s.mergeTicksOptions(),e.isDefault&&(t.scale=s)})),H.each(a,(function(t,e){t||delete n[e]})),t.scales=n,Re.addScalesToLayout(this)},buildOrUpdateControllers:function(){var t,e,n=this,i=[],a=n.data.datasets;for(t=0,e=a.length;t=0;--n)this.drawDataset(e[n],t);Le.notify(this,"afterDatasetsDraw",[t])}},drawDataset:function(t,e){var n={meta:t,index:t.index,easingValue:e};!1!==Le.notify(this,"beforeDatasetDraw",[n])&&(t.controller.draw(e),Le.notify(this,"afterDatasetDraw",[n]))},_drawTooltip:function(t){var e=this.tooltip,n={tooltip:e,easingValue:t};!1!==Le.notify(this,"beforeTooltipDraw",[n])&&(e.draw(),Le.notify(this,"afterTooltipDraw",[n]))},getElementAtEvent:function(t){return re.modes.single(this,t)},getElementsAtEvent:function(t){return re.modes.label(this,t,{intersect:!0})},getElementsAtXAxis:function(t){return re.modes["x-axis"](this,t,{intersect:!0})},getElementsAtEventForMode:function(t,e,n){var i=re.modes[e];return"function"==typeof i?i(this,t,n):[]},getDatasetAtEvent:function(t){return re.modes.dataset(this,t,{intersect:!0})},getDatasetMeta:function(t){var e=this.data.datasets[t];e._meta||(e._meta={});var n=e._meta[this.id];return n||(n=e._meta[this.id]={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e.order||0,index:t}),n},getVisibleDatasetCount:function(){for(var t=0,e=0,n=this.data.datasets.length;e3?n[2]-n[1]:n[1]-n[0];Math.abs(i)>1&&t!==Math.floor(t)&&(i=t-Math.floor(t));var a=H.log10(Math.abs(i)),r="";if(0!==t)if(Math.max(Math.abs(n[0]),Math.abs(n[n.length-1]))<1e-4){var o=H.log10(Math.abs(t)),s=Math.floor(o)-Math.floor(a);s=Math.max(Math.min(s,20),0),r=t.toExponential(s)}else{var l=-1*Math.floor(a);l=Math.max(Math.min(l,20),0),r=t.toFixed(l)}else r="0";return r},logarithmic:function(t,e,n){var i=t/Math.pow(10,Math.floor(H.log10(t)));return 0===t?"0":1===i||2===i||5===i||0===e||e===n.length-1?t.toExponential():""}}},sn=H.isArray,ln=H.isNullOrUndef,un=H.valueOrDefault,dn=H.valueAtIndexOrDefault;function hn(t,e,n){var i,a=t.getTicks().length,r=Math.min(e,a-1),o=t.getPixelForTick(r),s=t._startPixel,l=t._endPixel;if(!(n&&(i=1===a?Math.max(o-s,l-o):0===e?(t.getPixelForTick(1)-o)/2:(o-t.getPixelForTick(r-1))/2,(o+=rl+1e-6)))return o}function cn(t,e,n,i){var a,r,o,s,l,u,d,h,c,f,g,m,p,v=n.length,b=[],y=[],x=[];for(a=0;ae){for(n=0;n=c||d<=1||!s.isHorizontal()?s.labelRotation=h:(e=(t=s._getLabelSizes()).widest.width,n=t.highest.height-t.highest.offset,i=Math.min(s.maxWidth,s.chart.width-e),e+6>(a=l.offset?s.maxWidth/d:i/(d-1))&&(a=i/(d-(l.offset?.5:1)),r=s.maxHeight-fn(l.gridLines)-u.padding-gn(l.scaleLabel),o=Math.sqrt(e*e+n*n),f=H.toDegrees(Math.min(Math.asin(Math.min((t.highest.height+6)/a,1)),Math.asin(Math.min(r/o,1))-Math.asin(n/o))),f=Math.max(h,Math.min(c,f))),s.labelRotation=f)},afterCalculateTickRotation:function(){H.callback(this.options.afterCalculateTickRotation,[this])},beforeFit:function(){H.callback(this.options.beforeFit,[this])},fit:function(){var t=this,e=t.minSize={width:0,height:0},n=t.chart,i=t.options,a=i.ticks,r=i.scaleLabel,o=i.gridLines,s=t._isVisible(),l="bottom"===i.position,u=t.isHorizontal();if(u?e.width=t.maxWidth:s&&(e.width=fn(o)+gn(r)),u?s&&(e.height=fn(o)+gn(r)):e.height=t.maxHeight,a.display&&s){var d=pn(a),h=t._getLabelSizes(),c=h.first,f=h.last,g=h.widest,m=h.highest,p=.4*d.minor.lineHeight,v=a.padding;if(u){var b=0!==t.labelRotation,y=H.toRadians(t.labelRotation),x=Math.cos(y),_=Math.sin(y),w=_*g.width+x*(m.height-(b?m.offset:0))+(b?0:p);e.height=Math.min(t.maxHeight,e.height+w+v);var k,M,S=t.getPixelForTick(0)-t.left,D=t.right-t.getPixelForTick(t.getTicks().length-1);b?(k=l?x*c.width+_*c.offset:_*(c.height-c.offset),M=l?_*(f.height-f.offset):x*f.width+_*f.offset):(k=c.width/2,M=f.width/2),t.paddingLeft=Math.max((k-S)*t.width/(t.width-S),0)+3,t.paddingRight=Math.max((M-D)*t.width/(t.width-D),0)+3}else{var C=a.mirror?0:g.width+v+p;e.width=Math.min(t.maxWidth,e.width+C),t.paddingTop=c.height/2,t.paddingBottom=f.height/2}}t.handleMargins(),u?(t.width=t._length=n.width-t.margins.left-t.margins.right,t.height=e.height):(t.width=e.width,t.height=t._length=n.height-t.margins.top-t.margins.bottom)},handleMargins:function(){var t=this;t.margins&&(t.margins.left=Math.max(t.paddingLeft,t.margins.left),t.margins.top=Math.max(t.paddingTop,t.margins.top),t.margins.right=Math.max(t.paddingRight,t.margins.right),t.margins.bottom=Math.max(t.paddingBottom,t.margins.bottom))},afterFit:function(){H.callback(this.options.afterFit,[this])},isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},isFullWidth:function(){return this.options.fullWidth},getRightValue:function(t){if(ln(t))return NaN;if(("number"==typeof t||t instanceof Number)&&!isFinite(t))return NaN;if(t)if(this.isHorizontal()){if(void 0!==t.x)return this.getRightValue(t.x)}else if(void 0!==t.y)return this.getRightValue(t.y);return t},_convertTicksToLabels:function(t){var e,n,i,a=this;for(a.ticks=t.map((function(t){return t.value})),a.beforeTickToLabelConversion(),e=a.convertTicksToLabels(t)||a.ticks,a.afterTickToLabelConversion(),n=0,i=t.length;nn-1?null:this.getPixelForDecimal(t*i+(e?i/2:0))},getPixelForDecimal:function(t){return this._reversePixels&&(t=1-t),this._startPixel+t*this._length},getDecimalForPixel:function(t){var e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e},getBasePixel:function(){return this.getPixelForValue(this.getBaseValue())},getBaseValue:function(){var t=this.min,e=this.max;return this.beginAtZero?0:t<0&&e<0?e:t>0&&e>0?t:0},_autoSkip:function(t){var e,n,i,a,r=this.options.ticks,o=this._length,s=r.maxTicksLimit||o/this._tickSize()+1,l=r.major.enabled?function(t){var e,n,i=[];for(e=0,n=t.length;es)return function(t,e,n){var i,a,r=0,o=e[0];for(n=Math.ceil(n),i=0;iu)return r;return Math.max(u,1)}(l,t,0,s),u>0){for(e=0,n=u-1;e1?(h-d)/(u-1):null,bn(t,i,H.isNullOrUndef(a)?0:d-a,d),bn(t,i,h,H.isNullOrUndef(a)?t.length:h+a),vn(t)}return bn(t,i),vn(t)},_tickSize:function(){var t=this.options.ticks,e=H.toRadians(this.labelRotation),n=Math.abs(Math.cos(e)),i=Math.abs(Math.sin(e)),a=this._getLabelSizes(),r=t.autoSkipPadding||0,o=a?a.widest.width+r:0,s=a?a.highest.height+r:0;return this.isHorizontal()?s*n>o*i?o/n:s/i:s*i=0&&(o=t),void 0!==r&&(t=n.indexOf(r))>=0&&(s=t),e.minIndex=o,e.maxIndex=s,e.min=n[o],e.max=n[s]},buildTicks:function(){var t=this._getLabels(),e=this.minIndex,n=this.maxIndex;this.ticks=0===e&&n===t.length-1?t:t.slice(e,n+1)},getLabelForIndex:function(t,e){var n=this.chart;return n.getDatasetMeta(e).controller._getValueScaleId()===this.id?this.getRightValue(n.data.datasets[e].data[t]):this._getLabels()[t]},_configure:function(){var t=this,e=t.options.offset,n=t.ticks;xn.prototype._configure.call(t),t.isHorizontal()||(t._reversePixels=!t._reversePixels),n&&(t._startValue=t.minIndex-(e?.5:0),t._valueRange=Math.max(n.length-(e?0:1),1))},getPixelForValue:function(t,e,n){var i,a,r,o=this;return _n(e)||_n(n)||(t=o.chart.data.datasets[n].data[e]),_n(t)||(i=o.isHorizontal()?t.x:t.y),(void 0!==i||void 0!==t&&isNaN(e))&&(a=o._getLabels(),t=H.valueOrDefault(i,t),e=-1!==(r=a.indexOf(t))?r:e,isNaN(e)&&(e=t)),o.getPixelForDecimal((e-o._startValue)/o._valueRange)},getPixelForTick:function(t){var e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t],t+this.minIndex)},getValueForPixel:function(t){var e=Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange);return Math.min(Math.max(e,0),this.ticks.length-1)},getBasePixel:function(){return this.bottom}}),kn={position:"bottom"};wn._defaults=kn;var Mn=H.noop,Sn=H.isNullOrUndef;var Dn=xn.extend({getRightValue:function(t){return"string"==typeof t?+t:xn.prototype.getRightValue.call(this,t)},handleTickRangeOptions:function(){var t=this,e=t.options.ticks;if(e.beginAtZero){var n=H.sign(t.min),i=H.sign(t.max);n<0&&i<0?t.max=0:n>0&&i>0&&(t.min=0)}var a=void 0!==e.min||void 0!==e.suggestedMin,r=void 0!==e.max||void 0!==e.suggestedMax;void 0!==e.min?t.min=e.min:void 0!==e.suggestedMin&&(null===t.min?t.min=e.suggestedMin:t.min=Math.min(t.min,e.suggestedMin)),void 0!==e.max?t.max=e.max:void 0!==e.suggestedMax&&(null===t.max?t.max=e.suggestedMax:t.max=Math.max(t.max,e.suggestedMax)),a!==r&&t.min>=t.max&&(a?t.max=t.min+1:t.min=t.max-1),t.min===t.max&&(t.max++,e.beginAtZero||t.min--)},getTickLimit:function(){var t,e=this.options.ticks,n=e.stepSize,i=e.maxTicksLimit;return n?t=Math.ceil(this.max/n)-Math.floor(this.min/n)+1:(t=this._computeTickLimit(),i=i||11),i&&(t=Math.min(i,t)),t},_computeTickLimit:function(){return Number.POSITIVE_INFINITY},handleDirectionalChanges:Mn,buildTicks:function(){var t=this,e=t.options.ticks,n=t.getTickLimit(),i={maxTicks:n=Math.max(2,n),min:e.min,max:e.max,precision:e.precision,stepSize:H.valueOrDefault(e.fixedStepSize,e.stepSize)},a=t.ticks=function(t,e){var n,i,a,r,o=[],s=t.stepSize,l=s||1,u=t.maxTicks-1,d=t.min,h=t.max,c=t.precision,f=e.min,g=e.max,m=H.niceNum((g-f)/u/l)*l;if(m<1e-14&&Sn(d)&&Sn(h))return[f,g];(r=Math.ceil(g/m)-Math.floor(f/m))>u&&(m=H.niceNum(r*m/u/l)*l),s||Sn(c)?n=Math.pow(10,H._decimalPlaces(m)):(n=Math.pow(10,c),m=Math.ceil(m*n)/n),i=Math.floor(f/m)*m,a=Math.ceil(g/m)*m,s&&(!Sn(d)&&H.almostWhole(d/m,m/1e3)&&(i=d),!Sn(h)&&H.almostWhole(h/m,m/1e3)&&(a=h)),r=(a-i)/m,r=H.almostEquals(r,Math.round(r),m/1e3)?Math.round(r):Math.ceil(r),i=Math.round(i*n)/n,a=Math.round(a*n)/n,o.push(Sn(d)?i:d);for(var p=1;pe.length-1?null:this.getPixelForValue(e[t])}}),An=Cn;On._defaults=An;var Fn=H.valueOrDefault,In=H.math.log10;var Ln={position:"left",ticks:{callback:on.formatters.logarithmic}};function Rn(t,e){return H.isFinite(t)&&t>=0?t:e}var Nn=xn.extend({determineDataLimits:function(){var t,e,n,i,a,r,o=this,s=o.options,l=o.chart,u=l.data.datasets,d=o.isHorizontal();function h(t){return d?t.xAxisID===o.id:t.yAxisID===o.id}o.min=Number.POSITIVE_INFINITY,o.max=Number.NEGATIVE_INFINITY,o.minNotZero=Number.POSITIVE_INFINITY;var c=s.stacked;if(void 0===c)for(t=0;t0){var e=H.min(t),n=H.max(t);o.min=Math.min(o.min,e),o.max=Math.max(o.max,n)}}))}else for(t=0;t0?t.minNotZero=t.min:t.max<1?t.minNotZero=Math.pow(10,Math.floor(In(t.max))):t.minNotZero=1)},buildTicks:function(){var t=this,e=t.options.ticks,n=!t.isHorizontal(),i={min:Rn(e.min),max:Rn(e.max)},a=t.ticks=function(t,e){var n,i,a=[],r=Fn(t.min,Math.pow(10,Math.floor(In(e.min)))),o=Math.floor(In(e.max)),s=Math.ceil(e.max/Math.pow(10,o));0===r?(n=Math.floor(In(e.minNotZero)),i=Math.floor(e.minNotZero/Math.pow(10,n)),a.push(r),r=i*Math.pow(10,n)):(n=Math.floor(In(r)),i=Math.floor(r/Math.pow(10,n)));var l=n<0?Math.pow(10,Math.abs(n)):1;do{a.push(r),10===++i&&(i=1,l=++n>=0?1:l),r=Math.round(i*Math.pow(10,n)*l)/l}while(ne.length-1?null:this.getPixelForValue(e[t])},_getFirstTickValue:function(t){var e=Math.floor(In(t));return Math.floor(t/Math.pow(10,e))*Math.pow(10,e)},_configure:function(){var t=this,e=t.min,n=0;xn.prototype._configure.call(t),0===e&&(e=t._getFirstTickValue(t.minNotZero),n=Fn(t.options.ticks.fontSize,W.global.defaultFontSize)/t._length),t._startValue=In(e),t._valueOffset=n,t._valueRange=(In(t.max)-In(e))/(1-n)},getPixelForValue:function(t){var e=this,n=0;return(t=+e.getRightValue(t))>e.min&&t>0&&(n=(In(t)-e._startValue)/e._valueRange+e._valueOffset),e.getPixelForDecimal(n)},getValueForPixel:function(t){var e=this,n=e.getDecimalForPixel(t);return 0===n&&0===e.min?0:Math.pow(10,e._startValue+(n-e._valueOffset)*e._valueRange)}}),Wn=Ln;Nn._defaults=Wn;var Yn=H.valueOrDefault,zn=H.valueAtIndexOrDefault,En=H.options.resolve,Vn={display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,color:"rgba(0,0,0,0.1)",lineWidth:1,borderDash:[],borderDashOffset:0},gridLines:{circular:!1},ticks:{showLabelBackdrop:!0,backdropColor:"rgba(255,255,255,0.75)",backdropPaddingY:2,backdropPaddingX:2,callback:on.formatters.linear},pointLabels:{display:!0,fontSize:10,callback:function(t){return t}}};function Hn(t){var e=t.ticks;return e.display&&t.display?Yn(e.fontSize,W.global.defaultFontSize)+2*e.backdropPaddingY:0}function Bn(t,e,n,i,a){return t===i||t===a?{start:e-n/2,end:e+n/2}:ta?{start:e-n,end:e}:{start:e,end:e+n}}function jn(t){return 0===t||180===t?"center":t<180?"left":"right"}function Un(t,e,n,i){var a,r,o=n.y+i/2;if(H.isArray(e))for(a=0,r=e.length;a270||t<90)&&(n.y-=e.h)}function qn(t){return H.isNumber(t)?t:0}var Zn=Dn.extend({setDimensions:function(){var t=this;t.width=t.maxWidth,t.height=t.maxHeight,t.paddingTop=Hn(t.options)/2,t.xCenter=Math.floor(t.width/2),t.yCenter=Math.floor((t.height-t.paddingTop)/2),t.drawingArea=Math.min(t.height-t.paddingTop,t.width)/2},determineDataLimits:function(){var t=this,e=t.chart,n=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;H.each(e.data.datasets,(function(a,r){if(e.isDatasetVisible(r)){var o=e.getDatasetMeta(r);H.each(a.data,(function(e,a){var r=+t.getRightValue(e);isNaN(r)||o.data[a].hidden||(n=Math.min(r,n),i=Math.max(r,i))}))}})),t.min=n===Number.POSITIVE_INFINITY?0:n,t.max=i===Number.NEGATIVE_INFINITY?0:i,t.handleTickRangeOptions()},_computeTickLimit:function(){return Math.ceil(this.drawingArea/Hn(this.options))},convertTicksToLabels:function(){var t=this;Dn.prototype.convertTicksToLabels.call(t),t.pointLabels=t.chart.data.labels.map((function(){var e=H.callback(t.options.pointLabels.callback,arguments,t);return e||0===e?e:""}))},getLabelForIndex:function(t,e){return+this.getRightValue(this.chart.data.datasets[e].data[t])},fit:function(){var t=this.options;t.display&&t.pointLabels.display?function(t){var e,n,i,a=H.options._parseFont(t.options.pointLabels),r={l:0,r:t.width,t:0,b:t.height-t.paddingTop},o={};t.ctx.font=a.string,t._pointLabelSizes=[];var s,l,u,d=t.chart.data.labels.length;for(e=0;er.r&&(r.r=f.end,o.r=h),g.startr.b&&(r.b=g.end,o.b=h)}t.setReductions(t.drawingArea,r,o)}(this):this.setCenterPoint(0,0,0,0)},setReductions:function(t,e,n){var i=this,a=e.l/Math.sin(n.l),r=Math.max(e.r-i.width,0)/Math.sin(n.r),o=-e.t/Math.cos(n.t),s=-Math.max(e.b-(i.height-i.paddingTop),0)/Math.cos(n.b);a=qn(a),r=qn(r),o=qn(o),s=qn(s),i.drawingArea=Math.min(Math.floor(t-(a+r)/2),Math.floor(t-(o+s)/2)),i.setCenterPoint(a,r,o,s)},setCenterPoint:function(t,e,n,i){var a=this,r=a.width-e-a.drawingArea,o=t+a.drawingArea,s=n+a.drawingArea,l=a.height-a.paddingTop-i-a.drawingArea;a.xCenter=Math.floor((o+r)/2+a.left),a.yCenter=Math.floor((s+l)/2+a.top+a.paddingTop)},getIndexAngle:function(t){var e=this.chart,n=(t*(360/e.data.labels.length)+((e.options||{}).startAngle||0))%360;return(n<0?n+360:n)*Math.PI*2/360},getDistanceFromCenterForValue:function(t){var e=this;if(H.isNullOrUndef(t))return NaN;var n=e.drawingArea/(e.max-e.min);return e.options.ticks.reverse?(e.max-t)*n:(t-e.min)*n},getPointPosition:function(t,e){var n=this.getIndexAngle(t)-Math.PI/2;return{x:Math.cos(n)*e+this.xCenter,y:Math.sin(n)*e+this.yCenter}},getPointPositionForValue:function(t,e){return this.getPointPosition(t,this.getDistanceFromCenterForValue(e))},getBasePosition:function(t){var e=this.min,n=this.max;return this.getPointPositionForValue(t||0,this.beginAtZero?0:e<0&&n<0?n:e>0&&n>0?e:0)},_drawGrid:function(){var t,e,n,i=this,a=i.ctx,r=i.options,o=r.gridLines,s=r.angleLines,l=Yn(s.lineWidth,o.lineWidth),u=Yn(s.color,o.color);if(r.pointLabels.display&&function(t){var e=t.ctx,n=t.options,i=n.pointLabels,a=Hn(n),r=t.getDistanceFromCenterForValue(n.ticks.reverse?t.min:t.max),o=H.options._parseFont(i);e.save(),e.font=o.string,e.textBaseline="middle";for(var s=t.chart.data.labels.length-1;s>=0;s--){var l=0===s?a/2:0,u=t.getPointPosition(s,r+l+5),d=zn(i.fontColor,s,W.global.defaultFontColor);e.fillStyle=d;var h=t.getIndexAngle(s),c=H.toDegrees(h);e.textAlign=jn(c),Gn(c,t._pointLabelSizes[s],u),Un(e,t.pointLabels[s],u,o.lineHeight)}e.restore()}(i),o.display&&H.each(i.ticks,(function(t,n){0!==n&&(e=i.getDistanceFromCenterForValue(i.ticksAsNumbers[n]),function(t,e,n,i){var a,r=t.ctx,o=e.circular,s=t.chart.data.labels.length,l=zn(e.color,i-1),u=zn(e.lineWidth,i-1);if((o||s)&&l&&u){if(r.save(),r.strokeStyle=l,r.lineWidth=u,r.setLineDash&&(r.setLineDash(e.borderDash||[]),r.lineDashOffset=e.borderDashOffset||0),r.beginPath(),o)r.arc(t.xCenter,t.yCenter,n,0,2*Math.PI);else{a=t.getPointPosition(0,n),r.moveTo(a.x,a.y);for(var d=1;d=0;t--)e=i.getDistanceFromCenterForValue(r.ticks.reverse?i.min:i.max),n=i.getPointPosition(t,e),a.beginPath(),a.moveTo(i.xCenter,i.yCenter),a.lineTo(n.x,n.y),a.stroke();a.restore()}},_drawLabels:function(){var t=this,e=t.ctx,n=t.options.ticks;if(n.display){var i,a,r=t.getIndexAngle(0),o=H.options._parseFont(n),s=Yn(n.fontColor,W.global.defaultFontColor);e.save(),e.font=o.string,e.translate(t.xCenter,t.yCenter),e.rotate(r),e.textAlign="center",e.textBaseline="middle",H.each(t.ticks,(function(r,l){(0!==l||n.reverse)&&(i=t.getDistanceFromCenterForValue(t.ticksAsNumbers[l]),n.showLabelBackdrop&&(a=e.measureText(r).width,e.fillStyle=n.backdropColor,e.fillRect(-a/2-n.backdropPaddingX,-i-o.size/2-n.backdropPaddingY,a+2*n.backdropPaddingX,o.size+2*n.backdropPaddingY)),e.fillStyle=s,e.fillText(r,0,-i))})),e.restore()}},_drawTitle:H.noop}),$n=Vn;Zn._defaults=$n;var Xn=H._deprecated,Kn=H.options.resolve,Jn=H.valueOrDefault,Qn=Number.MIN_SAFE_INTEGER||-9007199254740991,ti=Number.MAX_SAFE_INTEGER||9007199254740991,ei={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},ni=Object.keys(ei);function ii(t,e){return t-e}function ai(t){return H.valueOrDefault(t.time.min,t.ticks.min)}function ri(t){return H.valueOrDefault(t.time.max,t.ticks.max)}function oi(t,e,n,i){var a=function(t,e,n){for(var i,a,r,o=0,s=t.length-1;o>=0&&o<=s;){if(a=t[(i=o+s>>1)-1]||null,r=t[i],!a)return{lo:null,hi:r};if(r[e]n))return{lo:a,hi:r};s=i-1}}return{lo:r,hi:null}}(t,e,n),r=a.lo?a.hi?a.lo:t[t.length-2]:t[0],o=a.lo?a.hi?a.hi:t[t.length-1]:t[1],s=o[e]-r[e],l=s?(n-r[e])/s:0,u=(o[i]-r[i])*l;return r[i]+u}function si(t,e){var n=t._adapter,i=t.options.time,a=i.parser,r=a||i.format,o=e;return"function"==typeof a&&(o=a(o)),H.isFinite(o)||(o="string"==typeof r?n.parse(o,r):n.parse(o)),null!==o?+o:(a||"function"!=typeof r||(o=r(e),H.isFinite(o)||(o=n.parse(o))),o)}function li(t,e){if(H.isNullOrUndef(e))return null;var n=t.options.time,i=si(t,t.getRightValue(e));return null===i?i:(n.round&&(i=+t._adapter.startOf(i,n.round)),i)}function ui(t,e,n,i){var a,r,o,s=ni.length;for(a=ni.indexOf(t);a=0&&(e[r].major=!0);return e}(t,r,o,n):r}var hi=xn.extend({initialize:function(){this.mergeTicksOptions(),xn.prototype.initialize.call(this)},update:function(){var t=this,e=t.options,n=e.time||(e.time={}),i=t._adapter=new rn._date(e.adapters.date);return Xn("time scale",n.format,"time.format","time.parser"),Xn("time scale",n.min,"time.min","ticks.min"),Xn("time scale",n.max,"time.max","ticks.max"),H.mergeIf(n.displayFormats,i.formats()),xn.prototype.update.apply(t,arguments)},getRightValue:function(t){return t&&void 0!==t.t&&(t=t.t),xn.prototype.getRightValue.call(this,t)},determineDataLimits:function(){var t,e,n,i,a,r,o,s=this,l=s.chart,u=s._adapter,d=s.options,h=d.time.unit||"day",c=ti,f=Qn,g=[],m=[],p=[],v=s._getLabels();for(t=0,n=v.length;t1?function(t){var e,n,i,a={},r=[];for(e=0,n=t.length;e1e5*u)throw e+" and "+n+" are too far apart with stepSize of "+u+" "+l;for(a=h;a=a&&n<=r&&d.push(n);return i.min=a,i.max=r,i._unit=l.unit||(s.autoSkip?ui(l.minUnit,i.min,i.max,h):function(t,e,n,i,a){var r,o;for(r=ni.length-1;r>=ni.indexOf(n);r--)if(o=ni[r],ei[o].common&&t._adapter.diff(a,i,o)>=e-1)return o;return ni[n?ni.indexOf(n):0]}(i,d.length,l.minUnit,i.min,i.max)),i._majorUnit=s.major.enabled&&"year"!==i._unit?function(t){for(var e=ni.indexOf(t)+1,n=ni.length;ee&&s=0&&t0?s:1}}),ci={position:"bottom",distribution:"linear",bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,displayFormat:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{autoSkip:!1,source:"auto",major:{enabled:!1}}};hi._defaults=ci;var fi={category:wn,linear:On,logarithmic:Nn,radialLinear:Zn,time:hi},gi=e((function(e,n){e.exports=function(){var n,i;function a(){return n.apply(null,arguments)}function r(t){return t instanceof Array||"[object Array]"===Object.prototype.toString.call(t)}function o(t){return null!=t&&"[object Object]"===Object.prototype.toString.call(t)}function s(t){return void 0===t}function l(t){return"number"==typeof t||"[object Number]"===Object.prototype.toString.call(t)}function u(t){return t instanceof Date||"[object Date]"===Object.prototype.toString.call(t)}function d(t,e){var n,i=[];for(n=0;n>>0,i=0;i0)for(n=0;n=0?n?"+":"":"-")+Math.pow(10,Math.max(0,a)).toString().substr(1)+i}var E=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,V=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,H={},B={};function j(t,e,n,i){var a=i;"string"==typeof i&&(a=function(){return this[i]()}),t&&(B[t]=a),e&&(B[e[0]]=function(){return z(a.apply(this,arguments),e[1],e[2])}),n&&(B[n]=function(){return this.localeData().ordinal(a.apply(this,arguments),t)})}function U(t,e){return t.isValid()?(e=G(e,t.localeData()),H[e]=H[e]||function(t){var e,n,i,a=t.match(E);for(e=0,n=a.length;e=0&&V.test(t);)t=t.replace(V,i),V.lastIndex=0,n-=1;return t}var q=/\d/,Z=/\d\d/,$=/\d{3}/,X=/\d{4}/,K=/[+-]?\d{6}/,J=/\d\d?/,Q=/\d\d\d\d?/,tt=/\d\d\d\d\d\d?/,et=/\d{1,3}/,nt=/\d{1,4}/,it=/[+-]?\d{1,6}/,at=/\d+/,rt=/[+-]?\d+/,ot=/Z|[+-]\d\d:?\d\d/gi,st=/Z|[+-]\d\d(?::?\d\d)?/gi,lt=/[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i,ut={};function dt(t,e,n){ut[t]=O(e)?e:function(t,i){return t&&n?n:e}}function ht(t,e){return h(ut,t)?ut[t](e._strict,e._locale):new RegExp(ct(t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,(function(t,e,n,i,a){return e||n||i||a}))))}function ct(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var ft={};function gt(t,e){var n,i=e;for("string"==typeof t&&(t=[t]),l(e)&&(i=function(t,n){n[e]=k(t)}),n=0;n68?1900:2e3)};var Pt,Tt=Ot("FullYear",!0);function Ot(t,e){return function(n){return null!=n?(Ft(this,t,n),a.updateOffset(this,e),this):At(this,t)}}function At(t,e){return t.isValid()?t._d["get"+(t._isUTC?"UTC":"")+e]():NaN}function Ft(t,e,n){t.isValid()&&!isNaN(n)&&("FullYear"===e&&Ct(t.year())&&1===t.month()&&29===t.date()?t._d["set"+(t._isUTC?"UTC":"")+e](n,t.month(),It(n,t.month())):t._d["set"+(t._isUTC?"UTC":"")+e](n))}function It(t,e){if(isNaN(t)||isNaN(e))return NaN;var n=function(t,e){return(t%e+e)%e}(e,12);return t+=(e-n)/12,1===n?Ct(t)?29:28:31-n%7%2}Pt=Array.prototype.indexOf?Array.prototype.indexOf:function(t){var e;for(e=0;e=0?(s=new Date(t+400,e,n,i,a,r,o),isFinite(s.getFullYear())&&s.setFullYear(t)):s=new Date(t,e,n,i,a,r,o),s}function jt(t){var e;if(t<100&&t>=0){var n=Array.prototype.slice.call(arguments);n[0]=t+400,e=new Date(Date.UTC.apply(null,n)),isFinite(e.getUTCFullYear())&&e.setUTCFullYear(t)}else e=new Date(Date.UTC.apply(null,arguments));return e}function Ut(t,e,n){var i=7+e-n;return-(7+jt(t,0,i).getUTCDay()-e)%7+i-1}function Gt(t,e,n,i,a){var r,o,s=1+7*(e-1)+(7+n-i)%7+Ut(t,i,a);return s<=0?o=Dt(r=t-1)+s:s>Dt(t)?(r=t+1,o=s-Dt(t)):(r=t,o=s),{year:r,dayOfYear:o}}function qt(t,e,n){var i,a,r=Ut(t.year(),e,n),o=Math.floor((t.dayOfYear()-r-1)/7)+1;return o<1?i=o+Zt(a=t.year()-1,e,n):o>Zt(t.year(),e,n)?(i=o-Zt(t.year(),e,n),a=t.year()+1):(a=t.year(),i=o),{week:i,year:a}}function Zt(t,e,n){var i=Ut(t,e,n),a=Ut(t+1,e,n);return(Dt(t)-i+a)/7}function $t(t,e){return t.slice(e,7).concat(t.slice(0,e))}j("w",["ww",2],"wo","week"),j("W",["WW",2],"Wo","isoWeek"),L("week","w"),L("isoWeek","W"),Y("week",5),Y("isoWeek",5),dt("w",J),dt("ww",J,Z),dt("W",J),dt("WW",J,Z),mt(["w","ww","W","WW"],(function(t,e,n,i){e[i.substr(0,1)]=k(t)})),j("d",0,"do","day"),j("dd",0,0,(function(t){return this.localeData().weekdaysMin(this,t)})),j("ddd",0,0,(function(t){return this.localeData().weekdaysShort(this,t)})),j("dddd",0,0,(function(t){return this.localeData().weekdays(this,t)})),j("e",0,0,"weekday"),j("E",0,0,"isoWeekday"),L("day","d"),L("weekday","e"),L("isoWeekday","E"),Y("day",11),Y("weekday",11),Y("isoWeekday",11),dt("d",J),dt("e",J),dt("E",J),dt("dd",(function(t,e){return e.weekdaysMinRegex(t)})),dt("ddd",(function(t,e){return e.weekdaysShortRegex(t)})),dt("dddd",(function(t,e){return e.weekdaysRegex(t)})),mt(["dd","ddd","dddd"],(function(t,e,n,i){var a=n._locale.weekdaysParse(t,i,n._strict);null!=a?e.d=a:g(n).invalidWeekday=t})),mt(["d","e","E"],(function(t,e,n,i){e[i]=k(t)}));var Xt="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Kt="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Jt="Su_Mo_Tu_We_Th_Fr_Sa".split("_");function Qt(t,e,n){var i,a,r,o=t.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],i=0;i<7;++i)r=f([2e3,1]).day(i),this._minWeekdaysParse[i]=this.weekdaysMin(r,"").toLocaleLowerCase(),this._shortWeekdaysParse[i]=this.weekdaysShort(r,"").toLocaleLowerCase(),this._weekdaysParse[i]=this.weekdays(r,"").toLocaleLowerCase();return n?"dddd"===e?-1!==(a=Pt.call(this._weekdaysParse,o))?a:null:"ddd"===e?-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:null:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:null:"dddd"===e?-1!==(a=Pt.call(this._weekdaysParse,o))?a:-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:null:"ddd"===e?-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:-1!==(a=Pt.call(this._weekdaysParse,o))?a:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:null:-1!==(a=Pt.call(this._minWeekdaysParse,o))?a:-1!==(a=Pt.call(this._weekdaysParse,o))?a:-1!==(a=Pt.call(this._shortWeekdaysParse,o))?a:null}var te=lt,ee=lt,ne=lt;function ie(){function t(t,e){return e.length-t.length}var e,n,i,a,r,o=[],s=[],l=[],u=[];for(e=0;e<7;e++)n=f([2e3,1]).day(e),i=this.weekdaysMin(n,""),a=this.weekdaysShort(n,""),r=this.weekdays(n,""),o.push(i),s.push(a),l.push(r),u.push(i),u.push(a),u.push(r);for(o.sort(t),s.sort(t),l.sort(t),u.sort(t),e=0;e<7;e++)s[e]=ct(s[e]),l[e]=ct(l[e]),u[e]=ct(u[e]);this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+o.join("|")+")","i")}function ae(){return this.hours()%12||12}function re(t,e){j(t,0,0,(function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)}))}function oe(t,e){return e._meridiemParse}j("H",["HH",2],0,"hour"),j("h",["hh",2],0,ae),j("k",["kk",2],0,(function(){return this.hours()||24})),j("hmm",0,0,(function(){return""+ae.apply(this)+z(this.minutes(),2)})),j("hmmss",0,0,(function(){return""+ae.apply(this)+z(this.minutes(),2)+z(this.seconds(),2)})),j("Hmm",0,0,(function(){return""+this.hours()+z(this.minutes(),2)})),j("Hmmss",0,0,(function(){return""+this.hours()+z(this.minutes(),2)+z(this.seconds(),2)})),re("a",!0),re("A",!1),L("hour","h"),Y("hour",13),dt("a",oe),dt("A",oe),dt("H",J),dt("h",J),dt("k",J),dt("HH",J,Z),dt("hh",J,Z),dt("kk",J,Z),dt("hmm",Q),dt("hmmss",tt),dt("Hmm",Q),dt("Hmmss",tt),gt(["H","HH"],xt),gt(["k","kk"],(function(t,e,n){var i=k(t);e[xt]=24===i?0:i})),gt(["a","A"],(function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t})),gt(["h","hh"],(function(t,e,n){e[xt]=k(t),g(n).bigHour=!0})),gt("hmm",(function(t,e,n){var i=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i)),g(n).bigHour=!0})),gt("hmmss",(function(t,e,n){var i=t.length-4,a=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i,2)),e[wt]=k(t.substr(a)),g(n).bigHour=!0})),gt("Hmm",(function(t,e,n){var i=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i))})),gt("Hmmss",(function(t,e,n){var i=t.length-4,a=t.length-2;e[xt]=k(t.substr(0,i)),e[_t]=k(t.substr(i,2)),e[wt]=k(t.substr(a))}));var se,le=Ot("Hours",!0),ue={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Rt,monthsShort:Nt,week:{dow:0,doy:6},weekdays:Xt,weekdaysMin:Jt,weekdaysShort:Kt,meridiemParse:/[ap]\.?m?\.?/i},de={},he={};function ce(t){return t?t.toLowerCase().replace("_","-"):t}function fe(n){var i=null;if(!de[n]&&e&&e.exports)try{i=se._abbr,t(),ge(i)}catch(t){}return de[n]}function ge(t,e){var n;return t&&((n=s(e)?pe(t):me(t,e))?se=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+t+" not found. Did you forget to load it?")),se._abbr}function me(t,e){if(null!==e){var n,i=ue;if(e.abbr=t,null!=de[t])T("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),i=de[t]._config;else if(null!=e.parentLocale)if(null!=de[e.parentLocale])i=de[e.parentLocale]._config;else{if(null==(n=fe(e.parentLocale)))return he[e.parentLocale]||(he[e.parentLocale]=[]),he[e.parentLocale].push({name:t,config:e}),null;i=n._config}return de[t]=new F(A(i,e)),he[t]&&he[t].forEach((function(t){me(t.name,t.config)})),ge(t),de[t]}return delete de[t],null}function pe(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return se;if(!r(t)){if(e=fe(t))return e;t=[t]}return function(t){for(var e,n,i,a,r=0;r0;){if(i=fe(a.slice(0,e).join("-")))return i;if(n&&n.length>=e&&M(a,n,!0)>=e-1)break;e--}r++}return se}(t)}function ve(t){var e,n=t._a;return n&&-2===g(t).overflow&&(e=n[bt]<0||n[bt]>11?bt:n[yt]<1||n[yt]>It(n[vt],n[bt])?yt:n[xt]<0||n[xt]>24||24===n[xt]&&(0!==n[_t]||0!==n[wt]||0!==n[kt])?xt:n[_t]<0||n[_t]>59?_t:n[wt]<0||n[wt]>59?wt:n[kt]<0||n[kt]>999?kt:-1,g(t)._overflowDayOfYear&&(eyt)&&(e=yt),g(t)._overflowWeeks&&-1===e&&(e=Mt),g(t)._overflowWeekday&&-1===e&&(e=St),g(t).overflow=e),t}function be(t,e,n){return null!=t?t:null!=e?e:n}function ye(t){var e,n,i,r,o,s=[];if(!t._d){for(i=function(t){var e=new Date(a.now());return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}(t),t._w&&null==t._a[yt]&&null==t._a[bt]&&function(t){var e,n,i,a,r,o,s,l;if(null!=(e=t._w).GG||null!=e.W||null!=e.E)r=1,o=4,n=be(e.GG,t._a[vt],qt(Le(),1,4).year),i=be(e.W,1),((a=be(e.E,1))<1||a>7)&&(l=!0);else{r=t._locale._week.dow,o=t._locale._week.doy;var u=qt(Le(),r,o);n=be(e.gg,t._a[vt],u.year),i=be(e.w,u.week),null!=e.d?((a=e.d)<0||a>6)&&(l=!0):null!=e.e?(a=e.e+r,(e.e<0||e.e>6)&&(l=!0)):a=r}i<1||i>Zt(n,r,o)?g(t)._overflowWeeks=!0:null!=l?g(t)._overflowWeekday=!0:(s=Gt(n,i,a,r,o),t._a[vt]=s.year,t._dayOfYear=s.dayOfYear)}(t),null!=t._dayOfYear&&(o=be(t._a[vt],i[vt]),(t._dayOfYear>Dt(o)||0===t._dayOfYear)&&(g(t)._overflowDayOfYear=!0),n=jt(o,0,t._dayOfYear),t._a[bt]=n.getUTCMonth(),t._a[yt]=n.getUTCDate()),e=0;e<3&&null==t._a[e];++e)t._a[e]=s[e]=i[e];for(;e<7;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[xt]&&0===t._a[_t]&&0===t._a[wt]&&0===t._a[kt]&&(t._nextDay=!0,t._a[xt]=0),t._d=(t._useUTC?jt:Bt).apply(null,s),r=t._useUTC?t._d.getUTCDay():t._d.getDay(),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[xt]=24),t._w&&void 0!==t._w.d&&t._w.d!==r&&(g(t).weekdayMismatch=!0)}}var xe=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,_e=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,we=/Z|[+-]\d\d(?::?\d\d)?/,ke=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],Me=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Se=/^\/?Date\((\-?\d+)/i;function De(t){var e,n,i,a,r,o,s=t._i,l=xe.exec(s)||_e.exec(s);if(l){for(g(t).iso=!0,e=0,n=ke.length;e0&&g(t).unusedInput.push(o),s=s.slice(s.indexOf(n)+n.length),u+=n.length),B[r]?(n?g(t).empty=!1:g(t).unusedTokens.push(r),pt(r,n,t)):t._strict&&!n&&g(t).unusedTokens.push(r);g(t).charsLeftOver=l-u,s.length>0&&g(t).unusedInput.push(s),t._a[xt]<=12&&!0===g(t).bigHour&&t._a[xt]>0&&(g(t).bigHour=void 0),g(t).parsedDateParts=t._a.slice(0),g(t).meridiem=t._meridiem,t._a[xt]=function(t,e,n){var i;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?((i=t.isPM(n))&&e<12&&(e+=12),i||12!==e||(e=0),e):e}(t._locale,t._a[xt],t._meridiem),ye(t),ve(t)}else Oe(t);else De(t)}function Fe(t){var e=t._i,n=t._f;return t._locale=t._locale||pe(t._l),null===e||void 0===n&&""===e?p({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),_(e)?new x(ve(e)):(u(e)?t._d=e:r(n)?function(t){var e,n,i,a,r;if(0===t._f.length)return g(t).invalidFormat=!0,void(t._d=new Date(NaN));for(a=0;athis?this:t:p()}));function We(t,e){var n,i;if(1===e.length&&r(e[0])&&(e=e[0]),!e.length)return Le();for(n=e[0],i=1;i=0?new Date(t+400,e,n)-hn:new Date(t,e,n).valueOf()}function gn(t,e,n){return t<100&&t>=0?Date.UTC(t+400,e,n)-hn:Date.UTC(t,e,n)}function mn(t,e){j(0,[t,t.length],0,e)}function pn(t,e,n,i,a){var r;return null==t?qt(this,i,a).year:(e>(r=Zt(t,i,a))&&(e=r),vn.call(this,t,e,n,i,a))}function vn(t,e,n,i,a){var r=Gt(t,e,n,i,a),o=jt(r.year,0,r.dayOfYear);return this.year(o.getUTCFullYear()),this.month(o.getUTCMonth()),this.date(o.getUTCDate()),this}j(0,["gg",2],0,(function(){return this.weekYear()%100})),j(0,["GG",2],0,(function(){return this.isoWeekYear()%100})),mn("gggg","weekYear"),mn("ggggg","weekYear"),mn("GGGG","isoWeekYear"),mn("GGGGG","isoWeekYear"),L("weekYear","gg"),L("isoWeekYear","GG"),Y("weekYear",1),Y("isoWeekYear",1),dt("G",rt),dt("g",rt),dt("GG",J,Z),dt("gg",J,Z),dt("GGGG",nt,X),dt("gggg",nt,X),dt("GGGGG",it,K),dt("ggggg",it,K),mt(["gggg","ggggg","GGGG","GGGGG"],(function(t,e,n,i){e[i.substr(0,2)]=k(t)})),mt(["gg","GG"],(function(t,e,n,i){e[i]=a.parseTwoDigitYear(t)})),j("Q",0,"Qo","quarter"),L("quarter","Q"),Y("quarter",7),dt("Q",q),gt("Q",(function(t,e){e[bt]=3*(k(t)-1)})),j("D",["DD",2],"Do","date"),L("date","D"),Y("date",9),dt("D",J),dt("DD",J,Z),dt("Do",(function(t,e){return t?e._dayOfMonthOrdinalParse||e._ordinalParse:e._dayOfMonthOrdinalParseLenient})),gt(["D","DD"],yt),gt("Do",(function(t,e){e[yt]=k(t.match(J)[0])}));var bn=Ot("Date",!0);j("DDD",["DDDD",3],"DDDo","dayOfYear"),L("dayOfYear","DDD"),Y("dayOfYear",4),dt("DDD",et),dt("DDDD",$),gt(["DDD","DDDD"],(function(t,e,n){n._dayOfYear=k(t)})),j("m",["mm",2],0,"minute"),L("minute","m"),Y("minute",14),dt("m",J),dt("mm",J,Z),gt(["m","mm"],_t);var yn=Ot("Minutes",!1);j("s",["ss",2],0,"second"),L("second","s"),Y("second",15),dt("s",J),dt("ss",J,Z),gt(["s","ss"],wt);var xn,_n=Ot("Seconds",!1);for(j("S",0,0,(function(){return~~(this.millisecond()/100)})),j(0,["SS",2],0,(function(){return~~(this.millisecond()/10)})),j(0,["SSS",3],0,"millisecond"),j(0,["SSSS",4],0,(function(){return 10*this.millisecond()})),j(0,["SSSSS",5],0,(function(){return 100*this.millisecond()})),j(0,["SSSSSS",6],0,(function(){return 1e3*this.millisecond()})),j(0,["SSSSSSS",7],0,(function(){return 1e4*this.millisecond()})),j(0,["SSSSSSSS",8],0,(function(){return 1e5*this.millisecond()})),j(0,["SSSSSSSSS",9],0,(function(){return 1e6*this.millisecond()})),L("millisecond","ms"),Y("millisecond",16),dt("S",et,q),dt("SS",et,Z),dt("SSS",et,$),xn="SSSS";xn.length<=9;xn+="S")dt(xn,at);function wn(t,e){e[kt]=k(1e3*("0."+t))}for(xn="S";xn.length<=9;xn+="S")gt(xn,wn);var kn=Ot("Milliseconds",!1);j("z",0,0,"zoneAbbr"),j("zz",0,0,"zoneName");var Mn=x.prototype;function Sn(t){return t}Mn.add=en,Mn.calendar=function(t,e){var n=t||Le(),i=Ue(n,this).startOf("day"),r=a.calendarFormat(this,i)||"sameElse",o=e&&(O(e[r])?e[r].call(this,n):e[r]);return this.format(o||this.localeData().calendar(r,this,Le(n)))},Mn.clone=function(){return new x(this)},Mn.diff=function(t,e,n){var i,a,r;if(!this.isValid())return NaN;if(!(i=Ue(t,this)).isValid())return NaN;switch(a=6e4*(i.utcOffset()-this.utcOffset()),e=R(e)){case"year":r=an(this,i)/12;break;case"month":r=an(this,i);break;case"quarter":r=an(this,i)/3;break;case"second":r=(this-i)/1e3;break;case"minute":r=(this-i)/6e4;break;case"hour":r=(this-i)/36e5;break;case"day":r=(this-i-a)/864e5;break;case"week":r=(this-i-a)/6048e5;break;default:r=this-i}return n?r:w(r)},Mn.endOf=function(t){var e;if(void 0===(t=R(t))||"millisecond"===t||!this.isValid())return this;var n=this._isUTC?gn:fn;switch(t){case"year":e=n(this.year()+1,0,1)-1;break;case"quarter":e=n(this.year(),this.month()-this.month()%3+3,1)-1;break;case"month":e=n(this.year(),this.month()+1,1)-1;break;case"week":e=n(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case"isoWeek":e=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case"day":case"date":e=n(this.year(),this.month(),this.date()+1)-1;break;case"hour":e=this._d.valueOf(),e+=dn-cn(e+(this._isUTC?0:this.utcOffset()*un),dn)-1;break;case"minute":e=this._d.valueOf(),e+=un-cn(e,un)-1;break;case"second":e=this._d.valueOf(),e+=ln-cn(e,ln)-1}return this._d.setTime(e),a.updateOffset(this,!0),this},Mn.format=function(t){t||(t=this.isUtc()?a.defaultFormatUtc:a.defaultFormat);var e=U(this,t);return this.localeData().postformat(e)},Mn.from=function(t,e){return this.isValid()&&(_(t)&&t.isValid()||Le(t).isValid())?Xe({to:this,from:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},Mn.fromNow=function(t){return this.from(Le(),t)},Mn.to=function(t,e){return this.isValid()&&(_(t)&&t.isValid()||Le(t).isValid())?Xe({from:this,to:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},Mn.toNow=function(t){return this.to(Le(),t)},Mn.get=function(t){return O(this[t=R(t)])?this[t]():this},Mn.invalidAt=function(){return g(this).overflow},Mn.isAfter=function(t,e){var n=_(t)?t:Le(t);return!(!this.isValid()||!n.isValid())&&("millisecond"===(e=R(e)||"millisecond")?this.valueOf()>n.valueOf():n.valueOf()9999?U(n,e?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):O(Date.prototype.toISOString)?e?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace("Z",U(n,"Z")):U(n,e?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")},Mn.inspect=function(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var t="moment",e="";this.isLocal()||(t=0===this.utcOffset()?"moment.utc":"moment.parseZone",e="Z");var n="["+t+'("]',i=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",a=e+'[")]';return this.format(n+i+"-MM-DD[T]HH:mm:ss.SSS"+a)},Mn.toJSON=function(){return this.isValid()?this.toISOString():null},Mn.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},Mn.unix=function(){return Math.floor(this.valueOf()/1e3)},Mn.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},Mn.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},Mn.year=Tt,Mn.isLeapYear=function(){return Ct(this.year())},Mn.weekYear=function(t){return pn.call(this,t,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)},Mn.isoWeekYear=function(t){return pn.call(this,t,this.isoWeek(),this.isoWeekday(),1,4)},Mn.quarter=Mn.quarters=function(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)},Mn.month=zt,Mn.daysInMonth=function(){return It(this.year(),this.month())},Mn.week=Mn.weeks=function(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")},Mn.isoWeek=Mn.isoWeeks=function(t){var e=qt(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")},Mn.weeksInYear=function(){var t=this.localeData()._week;return Zt(this.year(),t.dow,t.doy)},Mn.isoWeeksInYear=function(){return Zt(this.year(),1,4)},Mn.date=bn,Mn.day=Mn.days=function(t){if(!this.isValid())return null!=t?this:NaN;var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=function(t,e){return"string"!=typeof t?t:isNaN(t)?"number"==typeof(t=e.weekdaysParse(t))?t:null:parseInt(t,10)}(t,this.localeData()),this.add(t-e,"d")):e},Mn.weekday=function(t){if(!this.isValid())return null!=t?this:NaN;var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")},Mn.isoWeekday=function(t){if(!this.isValid())return null!=t?this:NaN;if(null!=t){var e=function(t,e){return"string"==typeof t?e.weekdaysParse(t)%7||7:isNaN(t)?null:t}(t,this.localeData());return this.day(this.day()%7?e:e-7)}return this.day()||7},Mn.dayOfYear=function(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")},Mn.hour=Mn.hours=le,Mn.minute=Mn.minutes=yn,Mn.second=Mn.seconds=_n,Mn.millisecond=Mn.milliseconds=kn,Mn.utcOffset=function(t,e,n){var i,r=this._offset||0;if(!this.isValid())return null!=t?this:NaN;if(null!=t){if("string"==typeof t){if(null===(t=je(st,t)))return this}else Math.abs(t)<16&&!n&&(t*=60);return!this._isUTC&&e&&(i=Ge(this)),this._offset=t,this._isUTC=!0,null!=i&&this.add(i,"m"),r!==t&&(!e||this._changeInProgress?tn(this,Xe(t-r,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?r:Ge(this)},Mn.utc=function(t){return this.utcOffset(0,t)},Mn.local=function(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(Ge(this),"m")),this},Mn.parseZone=function(){if(null!=this._tzm)this.utcOffset(this._tzm,!1,!0);else if("string"==typeof this._i){var t=je(ot,this._i);null!=t?this.utcOffset(t):this.utcOffset(0,!0)}return this},Mn.hasAlignedHourOffset=function(t){return!!this.isValid()&&(t=t?Le(t).utcOffset():0,(this.utcOffset()-t)%60==0)},Mn.isDST=function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},Mn.isLocal=function(){return!!this.isValid()&&!this._isUTC},Mn.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},Mn.isUtc=qe,Mn.isUTC=qe,Mn.zoneAbbr=function(){return this._isUTC?"UTC":""},Mn.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},Mn.dates=D("dates accessor is deprecated. Use date instead.",bn),Mn.months=D("months accessor is deprecated. Use month instead",zt),Mn.years=D("years accessor is deprecated. Use year instead",Tt),Mn.zone=D("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",(function(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()})),Mn.isDSTShifted=D("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",(function(){if(!s(this._isDSTShifted))return this._isDSTShifted;var t={};if(b(t,this),(t=Fe(t))._a){var e=t._isUTC?f(t._a):Le(t._a);this._isDSTShifted=this.isValid()&&M(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}));var Dn=F.prototype;function Cn(t,e,n,i){var a=pe(),r=f().set(i,e);return a[n](r,t)}function Pn(t,e,n){if(l(t)&&(e=t,t=void 0),t=t||"",null!=e)return Cn(t,e,n,"month");var i,a=[];for(i=0;i<12;i++)a[i]=Cn(t,i,n,"month");return a}function Tn(t,e,n,i){"boolean"==typeof t?(l(e)&&(n=e,e=void 0),e=e||""):(n=e=t,t=!1,l(e)&&(n=e,e=void 0),e=e||"");var a,r=pe(),o=t?r._week.dow:0;if(null!=n)return Cn(e,(n+o)%7,i,"day");var s=[];for(a=0;a<7;a++)s[a]=Cn(e,(a+o)%7,i,"day");return s}Dn.calendar=function(t,e,n){var i=this._calendar[t]||this._calendar.sameElse;return O(i)?i.call(e,n):i},Dn.longDateFormat=function(t){var e=this._longDateFormat[t],n=this._longDateFormat[t.toUpperCase()];return e||!n?e:(this._longDateFormat[t]=n.replace(/MMMM|MM|DD|dddd/g,(function(t){return t.slice(1)})),this._longDateFormat[t])},Dn.invalidDate=function(){return this._invalidDate},Dn.ordinal=function(t){return this._ordinal.replace("%d",t)},Dn.preparse=Sn,Dn.postformat=Sn,Dn.relativeTime=function(t,e,n,i){var a=this._relativeTime[n];return O(a)?a(t,e,n,i):a.replace(/%d/i,t)},Dn.pastFuture=function(t,e){var n=this._relativeTime[t>0?"future":"past"];return O(n)?n(e):n.replace(/%s/i,e)},Dn.set=function(t){var e,n;for(n in t)O(e=t[n])?this[n]=e:this["_"+n]=e;this._config=t,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},Dn.months=function(t,e){return t?r(this._months)?this._months[t.month()]:this._months[(this._months.isFormat||Lt).test(e)?"format":"standalone"][t.month()]:r(this._months)?this._months:this._months.standalone},Dn.monthsShort=function(t,e){return t?r(this._monthsShort)?this._monthsShort[t.month()]:this._monthsShort[Lt.test(e)?"format":"standalone"][t.month()]:r(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},Dn.monthsParse=function(t,e,n){var i,a,r;if(this._monthsParseExact)return Wt.call(this,t,e,n);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),i=0;i<12;i++){if(a=f([2e3,i]),n&&!this._longMonthsParse[i]&&(this._longMonthsParse[i]=new RegExp("^"+this.months(a,"").replace(".","")+"$","i"),this._shortMonthsParse[i]=new RegExp("^"+this.monthsShort(a,"").replace(".","")+"$","i")),n||this._monthsParse[i]||(r="^"+this.months(a,"")+"|^"+this.monthsShort(a,""),this._monthsParse[i]=new RegExp(r.replace(".",""),"i")),n&&"MMMM"===e&&this._longMonthsParse[i].test(t))return i;if(n&&"MMM"===e&&this._shortMonthsParse[i].test(t))return i;if(!n&&this._monthsParse[i].test(t))return i}},Dn.monthsRegex=function(t){return this._monthsParseExact?(h(this,"_monthsRegex")||Ht.call(this),t?this._monthsStrictRegex:this._monthsRegex):(h(this,"_monthsRegex")||(this._monthsRegex=Vt),this._monthsStrictRegex&&t?this._monthsStrictRegex:this._monthsRegex)},Dn.monthsShortRegex=function(t){return this._monthsParseExact?(h(this,"_monthsRegex")||Ht.call(this),t?this._monthsShortStrictRegex:this._monthsShortRegex):(h(this,"_monthsShortRegex")||(this._monthsShortRegex=Et),this._monthsShortStrictRegex&&t?this._monthsShortStrictRegex:this._monthsShortRegex)},Dn.week=function(t){return qt(t,this._week.dow,this._week.doy).week},Dn.firstDayOfYear=function(){return this._week.doy},Dn.firstDayOfWeek=function(){return this._week.dow},Dn.weekdays=function(t,e){var n=r(this._weekdays)?this._weekdays:this._weekdays[t&&!0!==t&&this._weekdays.isFormat.test(e)?"format":"standalone"];return!0===t?$t(n,this._week.dow):t?n[t.day()]:n},Dn.weekdaysMin=function(t){return!0===t?$t(this._weekdaysMin,this._week.dow):t?this._weekdaysMin[t.day()]:this._weekdaysMin},Dn.weekdaysShort=function(t){return!0===t?$t(this._weekdaysShort,this._week.dow):t?this._weekdaysShort[t.day()]:this._weekdaysShort},Dn.weekdaysParse=function(t,e,n){var i,a,r;if(this._weekdaysParseExact)return Qt.call(this,t,e,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),i=0;i<7;i++){if(a=f([2e3,1]).day(i),n&&!this._fullWeekdaysParse[i]&&(this._fullWeekdaysParse[i]=new RegExp("^"+this.weekdays(a,"").replace(".","\\.?")+"$","i"),this._shortWeekdaysParse[i]=new RegExp("^"+this.weekdaysShort(a,"").replace(".","\\.?")+"$","i"),this._minWeekdaysParse[i]=new RegExp("^"+this.weekdaysMin(a,"").replace(".","\\.?")+"$","i")),this._weekdaysParse[i]||(r="^"+this.weekdays(a,"")+"|^"+this.weekdaysShort(a,"")+"|^"+this.weekdaysMin(a,""),this._weekdaysParse[i]=new RegExp(r.replace(".",""),"i")),n&&"dddd"===e&&this._fullWeekdaysParse[i].test(t))return i;if(n&&"ddd"===e&&this._shortWeekdaysParse[i].test(t))return i;if(n&&"dd"===e&&this._minWeekdaysParse[i].test(t))return i;if(!n&&this._weekdaysParse[i].test(t))return i}},Dn.weekdaysRegex=function(t){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||ie.call(this),t?this._weekdaysStrictRegex:this._weekdaysRegex):(h(this,"_weekdaysRegex")||(this._weekdaysRegex=te),this._weekdaysStrictRegex&&t?this._weekdaysStrictRegex:this._weekdaysRegex)},Dn.weekdaysShortRegex=function(t){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||ie.call(this),t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(h(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=ee),this._weekdaysShortStrictRegex&&t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},Dn.weekdaysMinRegex=function(t){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||ie.call(this),t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(h(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=ne),this._weekdaysMinStrictRegex&&t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},Dn.isPM=function(t){return"p"===(t+"").toLowerCase().charAt(0)},Dn.meridiem=function(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"},ge("en",{dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10;return t+(1===k(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th")}}),a.lang=D("moment.lang is deprecated. Use moment.locale instead.",ge),a.langData=D("moment.langData is deprecated. Use moment.localeData instead.",pe);var On=Math.abs;function An(t,e,n,i){var a=Xe(e,n);return t._milliseconds+=i*a._milliseconds,t._days+=i*a._days,t._months+=i*a._months,t._bubble()}function Fn(t){return t<0?Math.floor(t):Math.ceil(t)}function In(t){return 4800*t/146097}function Ln(t){return 146097*t/4800}function Rn(t){return function(){return this.as(t)}}var Nn=Rn("ms"),Wn=Rn("s"),Yn=Rn("m"),zn=Rn("h"),En=Rn("d"),Vn=Rn("w"),Hn=Rn("M"),Bn=Rn("Q"),jn=Rn("y");function Un(t){return function(){return this.isValid()?this._data[t]:NaN}}var Gn=Un("milliseconds"),qn=Un("seconds"),Zn=Un("minutes"),$n=Un("hours"),Xn=Un("days"),Kn=Un("months"),Jn=Un("years"),Qn=Math.round,ti={ss:44,s:45,m:45,h:22,d:26,M:11};function ei(t,e,n,i,a){return a.relativeTime(e||1,!!n,t,i)}var ni=Math.abs;function ii(t){return(t>0)-(t<0)||+t}function ai(){if(!this.isValid())return this.localeData().invalidDate();var t,e,n=ni(this._milliseconds)/1e3,i=ni(this._days),a=ni(this._months);t=w(n/60),e=w(t/60),n%=60,t%=60;var r=w(a/12),o=a%=12,s=i,l=e,u=t,d=n?n.toFixed(3).replace(/\.?0+$/,""):"",h=this.asSeconds();if(!h)return"P0D";var c=h<0?"-":"",f=ii(this._months)!==ii(h)?"-":"",g=ii(this._days)!==ii(h)?"-":"",m=ii(this._milliseconds)!==ii(h)?"-":"";return c+"P"+(r?f+r+"Y":"")+(o?f+o+"M":"")+(s?g+s+"D":"")+(l||u||d?"T":"")+(l?m+l+"H":"")+(u?m+u+"M":"")+(d?m+d+"S":"")}var ri=ze.prototype;return ri.isValid=function(){return this._isValid},ri.abs=function(){var t=this._data;return this._milliseconds=On(this._milliseconds),this._days=On(this._days),this._months=On(this._months),t.milliseconds=On(t.milliseconds),t.seconds=On(t.seconds),t.minutes=On(t.minutes),t.hours=On(t.hours),t.months=On(t.months),t.years=On(t.years),this},ri.add=function(t,e){return An(this,t,e,1)},ri.subtract=function(t,e){return An(this,t,e,-1)},ri.as=function(t){if(!this.isValid())return NaN;var e,n,i=this._milliseconds;if("month"===(t=R(t))||"quarter"===t||"year"===t)switch(e=this._days+i/864e5,n=this._months+In(e),t){case"month":return n;case"quarter":return n/3;case"year":return n/12}else switch(e=this._days+Math.round(Ln(this._months)),t){case"week":return e/7+i/6048e5;case"day":return e+i/864e5;case"hour":return 24*e+i/36e5;case"minute":return 1440*e+i/6e4;case"second":return 86400*e+i/1e3;case"millisecond":return Math.floor(864e5*e)+i;default:throw new Error("Unknown unit "+t)}},ri.asMilliseconds=Nn,ri.asSeconds=Wn,ri.asMinutes=Yn,ri.asHours=zn,ri.asDays=En,ri.asWeeks=Vn,ri.asMonths=Hn,ri.asQuarters=Bn,ri.asYears=jn,ri.valueOf=function(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*k(this._months/12):NaN},ri._bubble=function(){var t,e,n,i,a,r=this._milliseconds,o=this._days,s=this._months,l=this._data;return r>=0&&o>=0&&s>=0||r<=0&&o<=0&&s<=0||(r+=864e5*Fn(Ln(s)+o),o=0,s=0),l.milliseconds=r%1e3,t=w(r/1e3),l.seconds=t%60,e=w(t/60),l.minutes=e%60,n=w(e/60),l.hours=n%24,o+=w(n/24),a=w(In(o)),s+=a,o-=Fn(Ln(a)),i=w(s/12),s%=12,l.days=o,l.months=s,l.years=i,this},ri.clone=function(){return Xe(this)},ri.get=function(t){return t=R(t),this.isValid()?this[t+"s"]():NaN},ri.milliseconds=Gn,ri.seconds=qn,ri.minutes=Zn,ri.hours=$n,ri.days=Xn,ri.weeks=function(){return w(this.days()/7)},ri.months=Kn,ri.years=Jn,ri.humanize=function(t){if(!this.isValid())return this.localeData().invalidDate();var e=this.localeData(),n=function(t,e,n){var i=Xe(t).abs(),a=Qn(i.as("s")),r=Qn(i.as("m")),o=Qn(i.as("h")),s=Qn(i.as("d")),l=Qn(i.as("M")),u=Qn(i.as("y")),d=a<=ti.ss&&["s",a]||a0,d[4]=n,ei.apply(null,d)}(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)},ri.toISOString=ai,ri.toString=ai,ri.toJSON=ai,ri.locale=rn,ri.localeData=sn,ri.toIsoString=D("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",ai),ri.lang=on,j("X",0,0,"unix"),j("x",0,0,"valueOf"),dt("x",rt),dt("X",/[+-]?\d+(\.\d{1,3})?/),gt("X",(function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))})),gt("x",(function(t,e,n){n._d=new Date(k(t))})),a.version="2.24.0",n=Le,a.fn=Mn,a.min=function(){return We("isBefore",[].slice.call(arguments,0))},a.max=function(){return We("isAfter",[].slice.call(arguments,0))},a.now=function(){return Date.now?Date.now():+new Date},a.utc=f,a.unix=function(t){return Le(1e3*t)},a.months=function(t,e){return Pn(t,e,"months")},a.isDate=u,a.locale=ge,a.invalid=p,a.duration=Xe,a.isMoment=_,a.weekdays=function(t,e,n){return Tn(t,e,n,"weekdays")},a.parseZone=function(){return Le.apply(null,arguments).parseZone()},a.localeData=pe,a.isDuration=Ee,a.monthsShort=function(t,e){return Pn(t,e,"monthsShort")},a.weekdaysMin=function(t,e,n){return Tn(t,e,n,"weekdaysMin")},a.defineLocale=me,a.updateLocale=function(t,e){if(null!=e){var n,i,a=ue;null!=(i=fe(t))&&(a=i._config),e=A(a,e),(n=new F(e)).parentLocale=de[t],de[t]=n,ge(t)}else null!=de[t]&&(null!=de[t].parentLocale?de[t]=de[t].parentLocale:null!=de[t]&&delete de[t]);return de[t]},a.locales=function(){return C(de)},a.weekdaysShort=function(t,e,n){return Tn(t,e,n,"weekdaysShort")},a.normalizeUnits=R,a.relativeTimeRounding=function(t){return void 0===t?Qn:"function"==typeof t&&(Qn=t,!0)},a.relativeTimeThreshold=function(t,e){return void 0!==ti[t]&&(void 0===e?ti[t]:(ti[t]=e,"s"===t&&(ti.ss=e-1),!0))},a.calendarFormat=function(t,e){var n=t.diff(e,"days",!0);return n<-6?"sameElse":n<-1?"lastWeek":n<0?"lastDay":n<1?"sameDay":n<2?"nextDay":n<7?"nextWeek":"sameElse"},a.prototype=Mn,a.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},a}()})),mi={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};rn._date.override("function"==typeof gi?{_id:"moment",formats:function(){return mi},parse:function(t,e){return"string"==typeof t&&"string"==typeof e?t=gi(t,e):t instanceof gi||(t=gi(t)),t.isValid()?t.valueOf():null},format:function(t,e){return gi(t).format(e)},add:function(t,e,n){return gi(t).add(e,n).valueOf()},diff:function(t,e,n){return gi(t).diff(gi(e),n)},startOf:function(t,e,n){return t=gi(t),"isoWeek"===e?t.isoWeekday(n).valueOf():t.startOf(e).valueOf()},endOf:function(t,e){return gi(t).endOf(e).valueOf()},_create:function(t){return gi(t)}}:{}),W._set("global",{plugins:{filler:{propagate:!0}}});var pi={dataset:function(t){var e=t.fill,n=t.chart,i=n.getDatasetMeta(e),a=i&&n.isDatasetVisible(e)&&i.dataset._children||[],r=a.length||0;return r?function(t,e){return e=n)&&i;switch(r){case"bottom":return"start";case"top":return"end";case"zero":return"origin";case"origin":case"start":case"end":return r;default:return!1}}function bi(t){return(t.el._scale||{}).getPointPositionForValue?function(t){var e,n,i,a,r,o=t.el._scale,s=o.options,l=o.chart.data.labels.length,u=t.fill,d=[];if(!l)return null;for(e=s.ticks.reverse?o.max:o.min,n=s.ticks.reverse?o.min:o.max,i=o.getPointPositionForValue(0,e),a=0;a0;--r)H.canvas.lineTo(t,n[r],n[r-1],!0);else for(o=n[0].cx,s=n[0].cy,l=Math.sqrt(Math.pow(n[0].x-o,2)+Math.pow(n[0].y-s,2)),r=a-1;r>0;--r)t.arc(o,s,l,n[r].angle,n[r-1].angle,!0)}}function ki(t,e,n,i,a,r){var o,s,l,u,d,h,c,f,g=e.length,m=i.spanGaps,p=[],v=[],b=0,y=0;for(t.beginPath(),o=0,s=g;o=0;--n)(e=l[n].$filler)&&e.visible&&(a=(i=e.el)._view,r=i._children||[],o=e.mapper,s=a.backgroundColor||W.global.defaultColor,o&&s&&r.length&&(H.canvas.clipArea(u,t.chartArea),ki(u,r,o,a,s,i._loop),H.canvas.unclipArea(u)))}},Si=H.rtl.getRtlAdapter,Di=H.noop,Ci=H.valueOrDefault;function Pi(t,e){return t.usePointStyle&&t.boxWidth>e?e:t.boxWidth}W._set("global",{legend:{display:!0,position:"top",align:"center",fullWidth:!0,reverse:!1,weight:1e3,onClick:function(t,e){var n=e.datasetIndex,i=this.chart,a=i.getDatasetMeta(n);a.hidden=null===a.hidden?!i.data.datasets[n].hidden:null,i.update()},onHover:null,onLeave:null,labels:{boxWidth:40,padding:10,generateLabels:function(t){var e=t.data.datasets,n=t.options.legend||{},i=n.labels&&n.labels.usePointStyle;return t._getSortedDatasetMetas().map((function(n){var a=n.controller.getStyle(i?0:void 0);return{text:e[n.index].label,fillStyle:a.backgroundColor,hidden:!t.isDatasetVisible(n.index),lineCap:a.borderCapStyle,lineDash:a.borderDash,lineDashOffset:a.borderDashOffset,lineJoin:a.borderJoinStyle,lineWidth:a.borderWidth,strokeStyle:a.borderColor,pointStyle:a.pointStyle,rotation:a.rotation,datasetIndex:n.index}}),this)}}},legendCallback:function(t){var e,n,i,a=document.createElement("ul"),r=t.data.datasets;for(a.setAttribute("class",t.id+"-legend"),e=0,n=r.length;el.width)&&(h+=o+n.padding,d[d.length-(e>0?0:1)]=0),s[e]={left:0,top:0,width:i,height:o},d[d.length-1]+=i+n.padding})),l.height+=h}else{var c=n.padding,f=t.columnWidths=[],g=t.columnHeights=[],m=n.padding,p=0,v=0;H.each(t.legendItems,(function(t,e){var i=Pi(n,o)+o/2+a.measureText(t.text).width;e>0&&v+o+2*c>l.height&&(m+=p+n.padding,f.push(p),g.push(v),p=0,v=0),p=Math.max(p,i),v+=o+c,s[e]={left:0,top:0,width:i,height:o}})),m+=p,f.push(p),g.push(v),l.width+=m}t.width=l.width,t.height=l.height}else t.width=l.width=t.height=l.height=0},afterFit:Di,isHorizontal:function(){return"top"===this.options.position||"bottom"===this.options.position},draw:function(){var t=this,e=t.options,n=e.labels,i=W.global,a=i.defaultColor,r=i.elements.line,o=t.height,s=t.columnHeights,l=t.width,u=t.lineWidths;if(e.display){var d,h=Si(e.rtl,t.left,t.minSize.width),c=t.ctx,f=Ci(n.fontColor,i.defaultFontColor),g=H.options._parseFont(n),m=g.size;c.textAlign=h.textAlign("left"),c.textBaseline="middle",c.lineWidth=.5,c.strokeStyle=f,c.fillStyle=f,c.font=g.string;var p=Pi(n,m),v=t.legendHitBoxes,b=function(t,i){switch(e.align){case"start":return n.padding;case"end":return t-i;default:return(t-i+n.padding)/2}},y=t.isHorizontal();d=y?{x:t.left+b(l,u[0]),y:t.top+n.padding,line:0}:{x:t.left+n.padding,y:t.top+b(o,s[0]),line:0},H.rtl.overrideTextDirection(t.ctx,e.textDirection);var x=m+n.padding;H.each(t.legendItems,(function(e,i){var f=c.measureText(e.text).width,g=p+m/2+f,_=d.x,w=d.y;h.setWidth(t.minSize.width),y?i>0&&_+g+n.padding>t.left+t.minSize.width&&(w=d.y+=x,d.line++,_=d.x=t.left+b(l,u[d.line])):i>0&&w+x>t.top+t.minSize.height&&(_=d.x=_+t.columnWidths[d.line]+n.padding,d.line++,w=d.y=t.top+b(o,s[d.line]));var k=h.x(_);!function(t,e,i){if(!(isNaN(p)||p<=0)){c.save();var o=Ci(i.lineWidth,r.borderWidth);if(c.fillStyle=Ci(i.fillStyle,a),c.lineCap=Ci(i.lineCap,r.borderCapStyle),c.lineDashOffset=Ci(i.lineDashOffset,r.borderDashOffset),c.lineJoin=Ci(i.lineJoin,r.borderJoinStyle),c.lineWidth=o,c.strokeStyle=Ci(i.strokeStyle,a),c.setLineDash&&c.setLineDash(Ci(i.lineDash,r.borderDash)),n&&n.usePointStyle){var s=p*Math.SQRT2/2,l=h.xPlus(t,p/2),u=e+m/2;H.canvas.drawPoint(c,i.pointStyle,s,l,u,i.rotation)}else c.fillRect(h.leftForLtr(t,p),e,p,m),0!==o&&c.strokeRect(h.leftForLtr(t,p),e,p,m);c.restore()}}(k,w,e),v[i].left=h.leftForLtr(k,v[i].width),v[i].top=w,function(t,e,n,i){var a=m/2,r=h.xPlus(t,p+a),o=e+a;c.fillText(n.text,r,o),n.hidden&&(c.beginPath(),c.lineWidth=2,c.moveTo(r,o),c.lineTo(h.xPlus(r,i),o),c.stroke())}(k,w,e,f),y?d.x+=g+n.padding:d.y+=x})),H.rtl.restoreTextDirection(t.ctx,e.textDirection)}},_getLegendItemAt:function(t,e){var n,i,a,r=this;if(t>=r.left&&t<=r.right&&e>=r.top&&e<=r.bottom)for(a=r.legendHitBoxes,n=0;n=(i=a[n]).left&&t<=i.left+i.width&&e>=i.top&&e<=i.top+i.height)return r.legendItems[n];return null},handleEvent:function(t){var e,n=this,i=n.options,a="mouseup"===t.type?"click":t.type;if("mousemove"===a){if(!i.onHover&&!i.onLeave)return}else{if("click"!==a)return;if(!i.onClick)return}e=n._getLegendItemAt(t.x,t.y),"click"===a?e&&i.onClick&&i.onClick.call(n,t.native,e):(i.onLeave&&e!==n._hoveredItem&&(n._hoveredItem&&i.onLeave.call(n,t.native,n._hoveredItem),n._hoveredItem=e),i.onHover&&e&&i.onHover.call(n,t.native,e))}});function Oi(t,e){var n=new Ti({ctx:t.ctx,options:e,chart:t});me.configure(t,n,e),me.addBox(t,n),t.legend=n}var Ai={id:"legend",_element:Ti,beforeInit:function(t){var e=t.options.legend;e&&Oi(t,e)},beforeUpdate:function(t){var e=t.options.legend,n=t.legend;e?(H.mergeIf(e,W.global.legend),n?(me.configure(t,n,e),n.options=e):Oi(t,e)):n&&(me.removeBox(t,n),delete t.legend)},afterEvent:function(t,e){var n=t.legend;n&&n.handleEvent(e)}},Fi=H.noop;W._set("global",{title:{display:!1,fontStyle:"bold",fullWidth:!0,padding:10,position:"top",text:"",weight:2e3}});var Ii=$.extend({initialize:function(t){H.extend(this,t),this.legendHitBoxes=[]},beforeUpdate:Fi,update:function(t,e,n){var i=this;return i.beforeUpdate(),i.maxWidth=t,i.maxHeight=e,i.margins=n,i.beforeSetDimensions(),i.setDimensions(),i.afterSetDimensions(),i.beforeBuildLabels(),i.buildLabels(),i.afterBuildLabels(),i.beforeFit(),i.fit(),i.afterFit(),i.afterUpdate(),i.minSize},afterUpdate:Fi,beforeSetDimensions:Fi,setDimensions:function(){var t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height),t.paddingLeft=0,t.paddingTop=0,t.paddingRight=0,t.paddingBottom=0,t.minSize={width:0,height:0}},afterSetDimensions:Fi,beforeBuildLabels:Fi,buildLabels:Fi,afterBuildLabels:Fi,beforeFit:Fi,fit:function(){var t,e=this,n=e.options,i=e.minSize={},a=e.isHorizontal();n.display?(t=(H.isArray(n.text)?n.text.length:1)*H.options._parseFont(n).lineHeight+2*n.padding,e.width=i.width=a?e.maxWidth:t,e.height=i.height=a?t:e.maxHeight):e.width=i.width=e.height=i.height=0},afterFit:Fi,isHorizontal:function(){var t=this.options.position;return"top"===t||"bottom"===t},draw:function(){var t=this,e=t.ctx,n=t.options;if(n.display){var i,a,r,o=H.options._parseFont(n),s=o.lineHeight,l=s/2+n.padding,u=0,d=t.top,h=t.left,c=t.bottom,f=t.right;e.fillStyle=H.valueOrDefault(n.fontColor,W.global.defaultFontColor),e.font=o.string,t.isHorizontal()?(a=h+(f-h)/2,r=d+l,i=f-h):(a="left"===n.position?h+l:f-l,r=d+(c-d)/2,i=c-d,u=Math.PI*("left"===n.position?-.5:.5)),e.save(),e.translate(a,r),e.rotate(u),e.textAlign="center",e.textBaseline="middle";var g=n.text;if(H.isArray(g))for(var m=0,p=0;p=0;i--){var a=t[i];if(e(a))return a}},H.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},H.almostEquals=function(t,e,n){return Math.abs(t-e)=t},H.max=function(t){return t.reduce((function(t,e){return isNaN(e)?t:Math.max(t,e)}),Number.NEGATIVE_INFINITY)},H.min=function(t){return t.reduce((function(t,e){return isNaN(e)?t:Math.min(t,e)}),Number.POSITIVE_INFINITY)},H.sign=Math.sign?function(t){return Math.sign(t)}:function(t){return 0===(t=+t)||isNaN(t)?t:t>0?1:-1},H.toRadians=function(t){return t*(Math.PI/180)},H.toDegrees=function(t){return t*(180/Math.PI)},H._decimalPlaces=function(t){if(H.isFinite(t)){for(var e=1,n=0;Math.round(t*e)/e!==t;)e*=10,n++;return n}},H.getAngleFromPoint=function(t,e){var n=e.x-t.x,i=e.y-t.y,a=Math.sqrt(n*n+i*i),r=Math.atan2(i,n);return r<-.5*Math.PI&&(r+=2*Math.PI),{angle:r,distance:a}},H.distanceBetweenPoints=function(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))},H.aliasPixel=function(t){return t%2==0?0:.5},H._alignPixel=function(t,e,n){var i=t.currentDevicePixelRatio,a=n/2;return Math.round((e-a)*i)/i+a},H.splineCurve=function(t,e,n,i){var a=t.skip?e:t,r=e,o=n.skip?e:n,s=Math.sqrt(Math.pow(r.x-a.x,2)+Math.pow(r.y-a.y,2)),l=Math.sqrt(Math.pow(o.x-r.x,2)+Math.pow(o.y-r.y,2)),u=s/(s+l),d=l/(s+l),h=i*(u=isNaN(u)?0:u),c=i*(d=isNaN(d)?0:d);return{previous:{x:r.x-h*(o.x-a.x),y:r.y-h*(o.y-a.y)},next:{x:r.x+c*(o.x-a.x),y:r.y+c*(o.y-a.y)}}},H.EPSILON=Number.EPSILON||1e-14,H.splineCurveMonotone=function(t){var e,n,i,a,r,o,s,l,u,d=(t||[]).map((function(t){return{model:t._model,deltaK:0,mK:0}})),h=d.length;for(e=0;e0?d[e-1]:null,(a=e0?d[e-1]:null,a=e=t.length-1?t[0]:t[e+1]:e>=t.length-1?t[t.length-1]:t[e+1]},H.previousItem=function(t,e,n){return n?e<=0?t[t.length-1]:t[e-1]:e<=0?t[0]:t[e-1]},H.niceNum=function(t,e){var n=Math.floor(H.log10(t)),i=t/Math.pow(10,n);return(e?i<1.5?1:i<3?2:i<7?5:10:i<=1?1:i<=2?2:i<=5?5:10)*Math.pow(10,n)},H.requestAnimFrame="undefined"==typeof window?function(t){t()}:window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)},H.getRelativePosition=function(t,e){var n,i,a=t.originalEvent||t,r=t.target||t.srcElement,o=r.getBoundingClientRect(),s=a.touches;s&&s.length>0?(n=s[0].clientX,i=s[0].clientY):(n=a.clientX,i=a.clientY);var l=parseFloat(H.getStyle(r,"padding-left")),u=parseFloat(H.getStyle(r,"padding-top")),d=parseFloat(H.getStyle(r,"padding-right")),h=parseFloat(H.getStyle(r,"padding-bottom")),c=o.right-o.left-l-d,f=o.bottom-o.top-u-h;return{x:n=Math.round((n-o.left-l)/c*r.width/e.currentDevicePixelRatio),y:i=Math.round((i-o.top-u)/f*r.height/e.currentDevicePixelRatio)}},H.getConstraintWidth=function(t){return n(t,"max-width","clientWidth")},H.getConstraintHeight=function(t){return n(t,"max-height","clientHeight")},H._calculatePadding=function(t,e,n){return(e=H.getStyle(t,e)).indexOf("%")>-1?n*parseInt(e,10)/100:parseInt(e,10)},H._getParentNode=function(t){var e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e},H.getMaximumWidth=function(t){var e=H._getParentNode(t);if(!e)return t.clientWidth;var n=e.clientWidth,i=n-H._calculatePadding(e,"padding-left",n)-H._calculatePadding(e,"padding-right",n),a=H.getConstraintWidth(t);return isNaN(a)?i:Math.min(i,a)},H.getMaximumHeight=function(t){var e=H._getParentNode(t);if(!e)return t.clientHeight;var n=e.clientHeight,i=n-H._calculatePadding(e,"padding-top",n)-H._calculatePadding(e,"padding-bottom",n),a=H.getConstraintHeight(t);return isNaN(a)?i:Math.min(i,a)},H.getStyle=function(t,e){return t.currentStyle?t.currentStyle[e]:document.defaultView.getComputedStyle(t,null).getPropertyValue(e)},H.retinaScale=function(t,e){var n=t.currentDevicePixelRatio=e||"undefined"!=typeof window&&window.devicePixelRatio||1;if(1!==n){var i=t.canvas,a=t.height,r=t.width;i.height=a*n,i.width=r*n,t.ctx.scale(n,n),i.style.height||i.style.width||(i.style.height=a+"px",i.style.width=r+"px")}},H.fontString=function(t,e,n){return e+" "+t+"px "+n},H.longestText=function(t,e,n,i){var a=(i=i||{}).data=i.data||{},r=i.garbageCollect=i.garbageCollect||[];i.font!==e&&(a=i.data={},r=i.garbageCollect=[],i.font=e),t.font=e;var o,s,l,u,d,h=0,c=n.length;for(o=0;on.length){for(o=0;oi&&(i=r),i},H.numberOfLabelLines=function(t){var e=1;return H.each(t,(function(t){H.isArray(t)&&t.length>e&&(e=t.length)})),e},H.color=k?function(t){return t instanceof CanvasGradient&&(t=W.global.defaultColor),k(t)}:function(t){return console.error("Color.js not found!"),t},H.getHoverColor=function(t){return t instanceof CanvasPattern||t instanceof CanvasGradient?t:H.color(t).saturate(.5).darken(.1).rgbString()}}(),en._adapters=rn,en.Animation=K,en.animationService=J,en.controllers=Jt,en.DatasetController=it,en.defaults=W,en.Element=$,en.elements=wt,en.Interaction=re,en.layouts=me,en.platform=Ie,en.plugins=Le,en.Scale=xn,en.scaleService=Re,en.Ticks=on,en.Tooltip=Ge,en.helpers.each(fi,(function(t,e){en.scaleService.registerScaleType(e,t,t._defaults)})),Ri)Ri.hasOwnProperty(zi)&&en.plugins.register(Ri[zi]);en.platform.initialize();var Ei=en;return"undefined"!=typeof window&&(window.Chart=en),en.Chart=en,en.Legend=Ri.legend._element,en.Title=Ri.title._element,en.pluginService=en.plugins,en.PluginBase=en.Element.extend({}),en.canvasHelpers=en.helpers.canvas,en.layoutService=en.layouts,en.LinearScaleBase=Dn,en.helpers.each(["Bar","Bubble","Doughnut","Line","PolarArea","Radar","Scatter"],(function(t){en[t]=function(e,n){return new en(e,en.helpers.merge(n||{},{type:t.charAt(0).toLowerCase()+t.slice(1)}))}})),Ei})); diff --git a/pikabu_ru/profile_rating__tracking/static/js/jquery-3.1.1.min.js b/pikabu_ru/profile_rating__tracking/static/js/jquery-3.1.1.min.js new file mode 100644 index 000000000..4c5be4c0f --- /dev/null +++ b/pikabu_ru/profile_rating__tracking/static/js/jquery-3.1.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R), +a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)), +void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" + + + + + + + +
    +
    +
    + + + + + {% for item in items %} + + {% endfor %} + + + + {% for item in items %} + + {% endfor %} + + +
    DATE:{{ item.date.strftime('%d/%m/%Y') }}
    VALUE:{{ item.value }}
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/pikabu_ru/profile_rating__tracking/web.py b/pikabu_ru/profile_rating__tracking/web.py new file mode 100644 index 000000000..ad41cb934 --- /dev/null +++ b/pikabu_ru/profile_rating__tracking/web.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from flask import Flask, render_template +from db import ProfileRating + + +app = Flask(__name__) + + +@app.route("/") +def index(): + items = ProfileRating.select().order_by(ProfileRating.id.desc()) + return render_template("index.html", items=items) + + +if __name__ == "__main__": + app.debug = True + app.run(port=10017) diff --git a/pikabu_ru_page_interview_fullstack__question_19/main.py b/pikabu_ru_page_interview_fullstack__question_19/main.py deleted file mode 100644 index 650524aad..000000000 --- a/pikabu_ru_page_interview_fullstack__question_19/main.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# SOURCE: https://pikabu.ru/page/interview/fullstack/ -# 19. Секретный агент Пикабу передал зашифрованное изображение. Вам необходимо расшифровать изображение и вывести его -# на страницу средствами JS (без сторонних библиотек). Алгоритм дешифрования известен: -# * пиксели перебираются слева направо для каждой строки; -# * для каждого пикселя вычисляется параметр s += x + y * 80 (изначально s = 0 и для каждого следующего пикселя -# значение увеличивается на x + y * 80), где x - колонка пикселя, y - строка; -# * для канала красного и синего цвета необходимо добавить параметр s; -# * для канала зеленого цвета необходимо отнять параметр s следующим образом: green = (green - s) & 0xff; - - -# pip install Pillow -from PIL import Image - -image = Image.open('154800137443598227.png') -width, height = image.size -pixel = image.load() - -s = 0 - -for y in range(height): - for x in range(width): - s += x + y * 80 - - # RGBa, alpha-канал нас не интересует, только RGB - r, g, b, _ = pixel[x, y] - - r = (r + s) & 0xff - g = (g - s) & 0xff - b = (b + s) & 0xff - - pixel[x, y] = r, g, b - - -image.save('result.png') diff --git a/pil_pillow__examples/blur/main.py b/pil_pillow__examples/blur/main.py index ed8b73d7c..7ccb6bbd1 100644 --- a/pil_pillow__examples/blur/main.py +++ b/pil_pillow__examples/blur/main.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image, ImageFilter + image_file = "input.jpg" img = Image.open(image_file) img = img.filter(ImageFilter.GaussianBlur(2)) -img.save('output.png') +img.save("output.png") img.show() diff --git a/pil_pillow__examples/bytes_to_image/main.py b/pil_pillow__examples/bytes_to_image/main.py index 945ac776f..7b3192433 100644 --- a/pil_pillow__examples/bytes_to_image/main.py +++ b/pil_pillow__examples/bytes_to_image/main.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -with open('input.jpg', 'rb') as f: - data = f.read() - import io -data_io = io.BytesIO(data) # pip install Pillow from PIL import Image + + +with open("input.jpg", "rb") as f: + data = f.read() + +data_io = io.BytesIO(data) img = Image.open(data_io) print(img) diff --git a/pil_pillow__examples/crop.py b/pil_pillow__examples/crop.py index d1fdb421b..224192eff 100644 --- a/pil_pillow__examples/crop.py +++ b/pil_pillow__examples/crop.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow diff --git a/pil_pillow__examples/crop_groups.py b/pil_pillow__examples/crop_groups.py index f4dcd32af..d48bf3ece 100644 --- a/pil_pillow__examples/crop_groups.py +++ b/pil_pillow__examples/crop_groups.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pathlib import Path @@ -12,7 +12,7 @@ path = Path(...) -for file_name in path.glob('*.jpg'): +for file_name in path.glob("*.jpg"): img = Image.open(file_name) width, height = img.size diff --git a/pil_pillow__examples/darker_picture/darker_picture.py b/pil_pillow__examples/darker_picture/darker_picture.py index 10fce76e4..4292d4f6a 100644 --- a/pil_pillow__examples/darker_picture/darker_picture.py +++ b/pil_pillow__examples/darker_picture/darker_picture.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image + image_file = "Collapse all.png" -img = Image.open(image_file).convert('RGBA') +img = Image.open(image_file).convert("RGBA") pixdata = img.load() @@ -16,6 +17,8 @@ for y in range(img.size[1]): for x in range(img.size[0]): if pixdata[x, y][:3] != (255, 255, 255): - pixdata[x, y] = tuple(map(lambda x: x - 50, pixdata[x, y][:3])) + pixdata[x, y][3:] + pixdata[x, y] = ( + tuple(map(lambda x: x - 50, pixdata[x, y][:3])) + pixdata[x, y][3:] + ) img.save("Collapse all black.png") diff --git a/pil_pillow__examples/draw watermark/main.py b/pil_pillow__examples/draw watermark/main.py index 70e747df3..30237ba53 100644 --- a/pil_pillow__examples/draw watermark/main.py +++ b/pil_pillow__examples/draw watermark/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -25,6 +25,6 @@ for i in range(0, width, width_text * 2): for j in range(0, height, height_text * 2): - drawer.text((i, j), text, font=font, fill=(0x00, 0xff, 0x00)) + drawer.text((i, j), text, font=font, fill=(0x00, 0xFF, 0x00)) image.show() diff --git a/pil_pillow__examples/draw_progress_on_image/main.py b/pil_pillow__examples/draw_progress_on_image/main.py index ed99db174..d6aea02da 100644 --- a/pil_pillow__examples/draw_progress_on_image/main.py +++ b/pil_pillow__examples/draw_progress_on_image/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow @@ -33,11 +33,11 @@ def draw_progress(image: Image, percent: int) -> Image: return image -if __name__ == '__main__': +if __name__ == "__main__": image_file = "input.jpg" image = Image.open(image_file) - + for percent in (5, 15, 75, 100): img = draw_progress(image, percent) - img.save(f'output/image_{percent}%.png') + img.save(f"output/image_{percent}%.png") img.show() diff --git a/pil_pillow__examples/draw_times_cyrillic/main.py b/pil_pillow__examples/draw_times_cyrillic/main.py index 944747c5f..c9d42ccdf 100644 --- a/pil_pillow__examples/draw_times_cyrillic/main.py +++ b/pil_pillow__examples/draw_times_cyrillic/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/q/1236348/201445 @@ -17,32 +17,36 @@ def set_text(template, texts): - font_upper = ImageFont.truetype(f"{FONTS}/times.ttf", 64, encoding='utf-8') - font_lower = ImageFont.truetype(f"{FONTS}/times.ttf", 46, encoding='utf-8') + font_upper = ImageFont.truetype(f"{FONTS}/times.ttf", 64, encoding="utf-8") + font_lower = ImageFont.truetype(f"{FONTS}/times.ttf", 46, encoding="utf-8") draw_upper_text = ImageDraw.Draw(template) upper_text_width, _ = draw_upper_text.textsize(texts[0], font_upper) draw_upper_text.text( ( - (template.width-upper_text_width)/2, - template.height-BOTTOM_PADDING+10 + (template.width - upper_text_width) / 2, + template.height - BOTTOM_PADDING + 10, ), - texts[0], fill="white", font=font_upper + texts[0], + fill="white", + font=font_upper, ) draw_lower_text = ImageDraw.Draw(template) lower_text_width, _ = draw_lower_text.textsize(texts[1], font_lower) draw_lower_text.text( ( - (template.width-lower_text_width)/2, - template.height-BOTTOM_PADDING+(PADDING*4)-10 + (template.width - lower_text_width) / 2, + template.height - BOTTOM_PADDING + (PADDING * 4) - 10, ), - texts[1], fill="white", font=font_lower + texts[1], + fill="white", + font=font_lower, ) return template -if __name__ == '__main__': +if __name__ == "__main__": img = Image.new("RGB", (400, 300), "black") set_text(img, ["ПРИВЕТ", "HELLO WORLD!"]) diff --git a/pil_pillow__examples/fast_average_color/main.py b/pil_pillow__examples/fast_average_color/main.py index 27332a422..0fbeb875f 100644 --- a/pil_pillow__examples/fast_average_color/main.py +++ b/pil_pillow__examples/fast_average_color/main.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import math -from typing import Tuple from pathlib import Path from PIL import Image @@ -15,16 +14,16 @@ def get_mode_correction(image: Image) -> str: - if image.mode not in ['RGB', 'RGBA']: - return 'RGB' + if image.mode not in ["RGB", "RGBA"]: + return "RGB" return image.mode # SOURCE: https://github.com/fast-average-color/fast-average-color/blob/15561aa55a36702f0b1af3d0ef611a205f8d56b5/src/algorithm/sqrt.js#L3 -def sqrt_algorithm(image: Image) -> Tuple[int, int, int, int]: +def sqrt_algorithm(image: Image) -> tuple[int, int, int, int]: mode = get_mode_correction(image) if mode != image.mode: - image = image.convert('RGB') + image = image.convert("RGB") red_total = 0 green_total = 0 @@ -53,7 +52,7 @@ def sqrt_algorithm(image: Image) -> Tuple[int, int, int, int]: round(math.sqrt(red_total / alpha_total)), round(math.sqrt(green_total / alpha_total)), round(math.sqrt(blue_total / alpha_total)), - round(alpha_total / count) + round(alpha_total / count), ) @@ -71,9 +70,9 @@ def draw_example(image: Image, margin=70) -> Image: return image_output -if __name__ == '__main__': - for file_name in DIR.glob('input/*.*'): +if __name__ == "__main__": + for file_name in DIR.glob("input/*.*"): image = Image.open(file_name) image_output = draw_example(image) # image_output.show() - image_output.save(DIR / 'output' / file_name.name) + image_output.save(DIR / "output" / file_name.name) diff --git a/pil_pillow__examples/fill_0_and_1/main.py b/pil_pillow__examples/fill_0_and_1/main.py index 7a431250c..c72f716dc 100644 --- a/pil_pillow__examples/fill_0_and_1/main.py +++ b/pil_pillow__examples/fill_0_and_1/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import itertools @@ -10,7 +10,7 @@ from PIL import Image, ImageDraw, ImageFont -img = Image.new('RGB', (500, 300), (255, 0, 0)) +img = Image.new("RGB", (500, 300), (255, 0, 0)) width, height = img.size drawer = ImageDraw.Draw(img) @@ -19,7 +19,8 @@ text_0, text_1 = "0", "1" color_0, color_1 = (0, 0, 0), (255, 255, 255) it = itertools.cycle([ - (text_0, color_0), (text_1, color_1) + (text_0, color_0), + (text_1, color_1), ]) width_text_0, height_text_0 = font.getsize(text_0) diff --git a/pil_pillow__examples/flip/flip.py b/pil_pillow__examples/flip/flip.py index 9c38ef45d..7aba1a3a5 100644 --- a/pil_pillow__examples/flip/flip.py +++ b/pil_pillow__examples/flip/flip.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image, ImageOps -img = Image.open('input.png') + + +img = Image.open("input.png") img = ImageOps.flip(img) img.show() diff --git a/pil_pillow__examples/for_cycle__pixels__array/main.py b/pil_pillow__examples/for_cycle__pixels__array/main.py index ed7f5d2a9..70a89eb4b 100644 --- a/pil_pillow__examples/for_cycle__pixels__array/main.py +++ b/pil_pillow__examples/for_cycle__pixels__array/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow @@ -21,7 +21,7 @@ def get_pixel_array(img, rgb_hex=False): r, g, b = img.getpixel((x, y)) if rgb_hex: - value = '{:02X}{:02X}{:02X}'.format(r, g, b) + value = f"{r:02X}{g:02X}{b:02X}" row.append(value) else: row.append((r, g, b)) @@ -29,16 +29,16 @@ def get_pixel_array(img, rgb_hex=False): return pixels -if __name__ == '__main__': +if __name__ == "__main__": img = Image.open("input.jpg") print(img) print() pixels = get_pixel_array(img) - print('Rows: {}, cols: {}'.format(len(pixels), len(pixels[0]))) + print(f"Rows: {len(pixels)}, cols: {len(pixels[0])}") print([pixels[0][i] for i in range(2)]) # [(7, 7, 7), (23, 23, 23)] print() pixels = get_pixel_array(img, rgb_hex=True) - print('Rows: {}, cols: {}'.format(len(pixels), len(pixels[0]))) + print(f"Rows: {len(pixels)}, cols: {len(pixels[0])}") print([pixels[0][i] for i in range(2)]) # ['070707', '171717'] diff --git a/pil_pillow__examples/get_image_size/main.py b/pil_pillow__examples/get_image_size/main.py index 96e07cda7..9851cc596 100644 --- a/pil_pillow__examples/get_image_size/main.py +++ b/pil_pillow__examples/get_image_size/main.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image + img = Image.open("input.jpg") print(img) print(img.size) diff --git a/pil_pillow__examples/get_viewers.py b/pil_pillow__examples/get_viewers.py index 5ed94a7fa..7646fd6b8 100644 --- a/pil_pillow__examples/get_viewers.py +++ b/pil_pillow__examples/get_viewers.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PIL.ImageShow import _viewers -print(f'Viewers: {len(_viewers)}') +print(f"Viewers: {len(_viewers)}") for viewer in _viewers: - print(viewer.get_command('123.png')) + print(viewer.get_command("123.png")) """ Viewers: 1 diff --git a/pil_pillow__examples/gray/main.py b/pil_pillow__examples/gray/main.py index a954414bb..c29a90b06 100644 --- a/pil_pillow__examples/gray/main.py +++ b/pil_pillow__examples/gray/main.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image, ImageOps + image_file = "input.jpg" image = Image.open(image_file) image_gray = ImageOps.grayscale(image) -image_gray.save('image_gray.png') +image_gray.save("image_gray.png") image_gray.show() diff --git a/pil_pillow__examples/image_to_bytes/main.py b/pil_pillow__examples/image_to_bytes/main.py index c3504430f..b1f70bfcf 100644 --- a/pil_pillow__examples/image_to_bytes/main.py +++ b/pil_pillow__examples/image_to_bytes/main.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import io + # pip install Pillow from PIL import Image -import io img = Image.open("input.jpg") diff --git a/pil_pillow__examples/img_to_grid/main.py b/pil_pillow__examples/img_to_grid/main.py index c5e2fcf09..9a0d01864 100644 --- a/pil_pillow__examples/img_to_grid/main.py +++ b/pil_pillow__examples/img_to_grid/main.py @@ -1,18 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image + + image = Image.open("input.jpg") width, height = image.size ROW_COUNT = 3 COLUMN_COUNT = 5 -grid_image = Image.new("RGB", (width * COLUMN_COUNT, height * ROW_COUNT), (255, 255, 255)) +grid_image = Image.new( + "RGB", + (width * COLUMN_COUNT, height * ROW_COUNT), + (255, 255, 255), +) for row in range(ROW_COUNT): y = row * width @@ -22,4 +28,4 @@ grid_image.paste(image, (x, y)) grid_image.show() -grid_image.save('output.jpg') +grid_image.save("output.jpg") diff --git a/pil_pillow__examples/invert/main.py b/pil_pillow__examples/invert/main.py index fc9481503..c3c409f00 100644 --- a/pil_pillow__examples/invert/main.py +++ b/pil_pillow__examples/invert/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Инвертирование цвета картинки""" @@ -12,21 +12,21 @@ def invert(image): - if image.mode == 'RGBA': + if image.mode == "RGBA": r, g, b, a = image.split() - rgb_image = Image.merge('RGB', (r, g, b)) + rgb_image = Image.merge("RGB", (r, g, b)) inverted_image = ImageOps.invert(rgb_image) r2, g2, b2 = inverted_image.split() - return Image.merge('RGBA', (r2, g2, b2, a)) + return Image.merge("RGBA", (r2, g2, b2, a)) else: return ImageOps.invert(image) -if __name__ == '__main__': +if __name__ == "__main__": image_file = "input.jpg" image = Image.open(image_file) image_invert = invert(image) - image_invert.save('image_invert.png') + image_invert.save("image_invert.png") image_invert.show() diff --git a/pil_pillow__examples/invert_gray/main.py b/pil_pillow__examples/invert_gray/main.py index 3ee1191b2..6ddb9c4f1 100644 --- a/pil_pillow__examples/invert_gray/main.py +++ b/pil_pillow__examples/invert_gray/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow @@ -9,22 +9,22 @@ def invert(image): - if image.mode == 'RGBA': + if image.mode == "RGBA": r, g, b, a = image.split() - rgb_image = Image.merge('RGB', (r, g, b)) + rgb_image = Image.merge("RGB", (r, g, b)) inverted_image = ImageOps.invert(rgb_image) r2, g2, b2 = inverted_image.split() - return Image.merge('RGBA', (r2, g2, b2, a)) + return Image.merge("RGBA", (r2, g2, b2, a)) else: return ImageOps.invert(image) -if __name__ == '__main__': +if __name__ == "__main__": image_file = "input.jpg" image = Image.open(image_file) image_invert = invert(image) image_invert_gray = ImageOps.grayscale(image_invert) - image_invert_gray.save('image_invert_gray.png') + image_invert_gray.save("image_invert_gray.png") image_invert_gray.show() diff --git "a/pil_pillow__examples/jpeg_quality=1__\321\210\320\260\320\272\320\260\320\273\320\270\321\201\321\202\321\213\320\265_\321\204\320\276\321\202\320\272\320\270__jackal_jpg/main.py" "b/pil_pillow__examples/jpeg_quality=1__\321\210\320\260\320\272\320\260\320\273\320\270\321\201\321\202\321\213\320\265_\321\204\320\276\321\202\320\272\320\270__jackal_jpg/main.py" index b9d9ff20b..6abde7363 100644 --- "a/pil_pillow__examples/jpeg_quality=1__\321\210\320\260\320\272\320\260\320\273\320\270\321\201\321\202\321\213\320\265_\321\204\320\276\321\202\320\272\320\270__jackal_jpg/main.py" +++ "b/pil_pillow__examples/jpeg_quality=1__\321\210\320\260\320\272\320\260\320\273\320\270\321\201\321\202\321\213\320\265_\321\204\320\276\321\202\320\272\320\270__jackal_jpg/main.py" @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PIL import Image -img = Image.open('input.jpg') -img.save('output.jpg', format='JPEG', quality=1) + + +img = Image.open("input.jpg") +img.save("output.jpg", format="JPEG", quality=1) diff --git a/pil_pillow__examples/pil_image__to__qimage_qpixmap__qt/main.py b/pil_pillow__examples/pil_image__to__qimage_qpixmap__qt/main.py index a52ba908c..1c1e97c64 100644 --- a/pil_pillow__examples/pil_image__to__qimage_qpixmap__qt/main.py +++ b/pil_pillow__examples/pil_image__to__qimage_qpixmap__qt/main.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image + from PyQt5 import Qt + image_file = "input.jpg" image = Image.open(image_file) diff --git a/pil_pillow__examples/pixelate_image/main.py b/pil_pillow__examples/pixelate_image/main.py index 14d7bdf2d..ea9fa95e6 100644 --- a/pil_pillow__examples/pixelate_image/main.py +++ b/pil_pillow__examples/pixelate_image/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://gist.github.com/danyshaanan/6754465 @@ -17,8 +17,12 @@ def pixelate(image, pixel_size=9, draw_margin=True): margin_color = (0, 0, 0) - image = image.resize((image.size[0] // pixel_size, image.size[1] // pixel_size), Image.NEAREST) - image = image.resize((image.size[0] * pixel_size, image.size[1] * pixel_size), Image.NEAREST) + image = image.resize( + (image.size[0] // pixel_size, image.size[1] // pixel_size), Image.NEAREST + ) + image = image.resize( + (image.size[0] * pixel_size, image.size[1] * pixel_size), Image.NEAREST + ) pixel = image.load() # Draw black margin between pixels @@ -26,25 +30,25 @@ def pixelate(image, pixel_size=9, draw_margin=True): for i in range(0, image.size[0], pixel_size): for j in range(0, image.size[1], pixel_size): for r in range(pixel_size): - pixel[i+r, j] = margin_color - pixel[i, j+r] = margin_color + pixel[i + r, j] = margin_color + pixel[i, j + r] = margin_color return image -if __name__ == '__main__': - image = Image.open('image/input.jpg').convert('RGB') +if __name__ == "__main__": + image = Image.open("image/input.jpg").convert("RGB") # image.show() image_pixelate = pixelate(image, draw_margin=False) - image_pixelate.save('image/output_no_margin.jpg') + image_pixelate.save("image/output_no_margin.jpg") # image_pixelate.show() image_pixelate = pixelate(image) - image_pixelate.save('image/output.jpg') + image_pixelate.save("image/output.jpg") # image_pixelate.show() for size in (16, 32, 48): image_pixelate = pixelate(image, pixel_size=size) - image_pixelate.save('image/output_{}.jpg'.format(size)) + image_pixelate.save(f"image/output_{size}.jpg") # image_pixelate.show() diff --git a/pil_pillow__examples/png2jpg/main.py b/pil_pillow__examples/png2jpg/main.py index 9cfc0673a..d0acd602d 100644 --- a/pil_pillow__examples/png2jpg/main.py +++ b/pil_pillow__examples/png2jpg/main.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +from PIL import Image file_name = "input.png" -new_file_name = file_name[:-3] + 'jpg' +new_file_name = file_name[:-3] + "jpg" -from PIL import Image img = Image.open(file_name) - img.save(new_file_name, optimize=True, quality=100) diff --git a/pil_pillow__examples/qimage_qpixmap__to__pil_image/main.py b/pil_pillow__examples/qimage_qpixmap__to__pil_image/main.py index a3a1fe53e..48e8cd089 100644 --- a/pil_pillow__examples/qimage_qpixmap__to__pil_image/main.py +++ b/pil_pillow__examples/qimage_qpixmap__to__pil_image/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow @@ -10,6 +10,7 @@ # pip install pyqt5 from PyQt5.Qt import QImage, QPixmap, QApplication + image_file = "input.jpg" app = QApplication([]) diff --git a/pil_pillow__examples/replace_rgb_by_alpha/main.py b/pil_pillow__examples/replace_rgb_by_alpha/main.py index 3e346db51..a9ff418d5 100644 --- a/pil_pillow__examples/replace_rgb_by_alpha/main.py +++ b/pil_pillow__examples/replace_rgb_by_alpha/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow @@ -9,7 +9,7 @@ image_file = "input.png" -img = Image.open(image_file).convert('RGBA') +img = Image.open(image_file).convert("RGBA") pixdata = img.load() diff --git a/pil_pillow__examples/resize_width_or_height_with_keep_ratio/input.jpg b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/input.jpg new file mode 100644 index 000000000..2b7999a0f Binary files /dev/null and b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/input.jpg differ diff --git a/pil_pillow__examples/resize_width_or_height_with_keep_ratio/main.py b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/main.py new file mode 100644 index 000000000..52e049336 --- /dev/null +++ b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/main.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install Pillow +from PIL import Image +from PIL.Image import Resampling + + +def _resize_img( + img: Image, + width: int, + height: int, + resample: Resampling | None = None, +) -> Image: + img_resized = img.resize(size=(width, height), resample=resample) + img_resized.format = img.format # NOTE: resize reset format + return img_resized + + +def resize_height_img( + img: Image, + height: int, + resample: Resampling | None = None, +) -> Image: + """Resize by height, keep ratio.""" + return _resize_img(img, img.width * height // img.height, height, resample) + + +def resize_width_img( + img: Image, + width: int, + resample: Resampling | None = None, +) -> Image: + """Resize by width, keep ratio.""" + return _resize_img(img, width, img.height * width // img.width, resample) + + +if __name__ == "__main__": + file_name = "input.jpg" + img = Image.open(file_name) + print(img.size) + # (660, 438) + + resample = Resampling.LANCZOS + + img_height_300 = resize_height_img(img, height=300, resample=resample) + img_height_300.save("output_height=300.jpg") + print(img_height_300.size) + # (452, 300) + + img_width_300 = resize_width_img(img, width=300, resample=resample) + img_width_300.save("output_width=300.jpg") + print(img_width_300.size) + # (300, 199) diff --git a/pil_pillow__examples/resize_width_or_height_with_keep_ratio/output_height=300.jpg b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/output_height=300.jpg new file mode 100644 index 000000000..fb4a062a2 Binary files /dev/null and b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/output_height=300.jpg differ diff --git a/pil_pillow__examples/resize_width_or_height_with_keep_ratio/output_width=300.jpg b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/output_width=300.jpg new file mode 100644 index 000000000..3381d2a90 Binary files /dev/null and b/pil_pillow__examples/resize_width_or_height_with_keep_ratio/output_width=300.jpg differ diff --git a/pil_pillow__examples/rounded_rectangle.py b/pil_pillow__examples/rounded_rectangle.py index ad836f2c7..57784bfa4 100644 --- a/pil_pillow__examples/rounded_rectangle.py +++ b/pil_pillow__examples/rounded_rectangle.py @@ -1,38 +1,46 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/szabgab/slides/blob/914d15bb55cb0e335afcb72b6af0fa2aa9ed356f/python/examples/pil/draw_rectangle_with_rounded_corners.py -# TODO: append alpha +from PIL import Image, ImageDraw -from PIL import Image, ImageDraw +Color = float | tuple[int, ...] | str -def round_corner(radius, fill): +def round_corner(radius: int, fill: Color) -> Image: """Draw a round corner""" - corner = Image.new('RGB', (radius, radius), (0, 0, 0, 0)) + corner = Image.new("RGBA", (radius, radius), (0, 0, 0, 0)) + draw = ImageDraw.Draw(corner) - draw.pieslice((0, 0, radius * 2, radius * 2), 180, 270, fill=fill) + draw.pieslice((0, 0, radius * 2, radius * 2), start=180, end=270, fill=fill) + return corner -def round_rectangle(size, radius, fill): +def round_rectangle(width: int, height: int, radius: int, fill: Color) -> Image: """Draw a rounded rectangle""" - width, height = size - rectangle = Image.new('RGBA', size, fill) + + rectangle = Image.new("RGBA", (width, height), fill) corner = round_corner(radius, fill) + rectangle.paste(corner, (0, 0)) - rectangle.paste(corner.rotate(90), (0, height - radius)) # Rotate the corner and paste it + + # Rotate the corner and paste it + rectangle.paste(corner.rotate(90), (0, height - radius)) + rectangle.paste(corner.rotate(180), (width - radius, height - radius)) rectangle.paste(corner.rotate(270), (width - radius, 0)) + return rectangle -img = round_rectangle((500, 500), 50, "yellow") -img.show() -img.save('img.png') +if __name__ == "__main__": + img = round_rectangle(width=500, height=500, radius=50, fill="yellow") + img.show() + img.save("img.png") diff --git a/pil_pillow__examples/set_transparent_pixels/main.py b/pil_pillow__examples/set_transparent_pixels/main.py new file mode 100644 index 000000000..491a8f6a2 --- /dev/null +++ b/pil_pillow__examples/set_transparent_pixels/main.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PIL import Image + + +file_name = "../for_cycle__pixels__array/input.jpg" + +img = Image.open(file_name).convert("RGBA") +pixels = img.load() +width, height = img.size + +for i in range(width): + for j in range(height): + if (j + i) % 2 == 0: + pixels[i, j] = 0, 0, 0, 0 + +img.save("output.png") diff --git a/pil_pillow__examples/simple_draw__hello_world/main.py b/pil_pillow__examples/simple_draw__hello_world/main.py index 26a9a4d30..ba888f3d7 100644 --- a/pil_pillow__examples/simple_draw__hello_world/main.py +++ b/pil_pillow__examples/simple_draw__hello_world/main.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image, ImageDraw, ImageFont + image = Image.open("images.jpg") font = ImageFont.truetype("arial.ttf", 25) drawer = ImageDraw.Draw(image) -drawer.text((50, 100), "Hello World!\nПривет мир!", font=font, fill='black') +drawer.text((50, 100), "Hello World!\nПривет мир!", font=font, fill="black") -image.save('new_img.jpg') +image.save("new_img.jpg") image.show() diff --git a/pil_pillow__examples/square_to_circle__avatar/main.py b/pil_pillow__examples/square_to_circle__avatar/main.py index 796e98f89..7ad249805 100644 --- a/pil_pillow__examples/square_to_circle__avatar/main.py +++ b/pil_pillow__examples/square_to_circle__avatar/main.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PIL import Image, ImageDraw -img = Image.open('input.jpg') + +img = Image.open("input.jpg") bigsize = img.size[0] * 3, img.size[1] * 3 -mask = Image.new('L', bigsize, 0) +mask = Image.new("L", bigsize, 0) draw = ImageDraw.Draw(mask) draw.ellipse((0, 0) + bigsize, fill=255) mask = mask.resize(img.size, Image.ANTIALIAS) img.putalpha(mask) -img.save('output.png') +img.save("output.png") diff --git a/pil_pillow__examples/text_to_pixels/main.py b/pil_pillow__examples/text_to_pixels/main.py index 6f3309b45..959090869 100644 --- a/pil_pillow__examples/text_to_pixels/main.py +++ b/pil_pillow__examples/text_to_pixels/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PIL import Image, ImageDraw @@ -46,7 +46,7 @@ img_size = s break -assert img_size != -1, "Неудалось подобрать размер изображения" +assert img_size != -1, "Не удалось подобрать размер изображения" image = Image.new("RGBA", (img_size, img_size), (0, 0, 0, 0)) draw = ImageDraw.Draw(image) @@ -61,4 +61,4 @@ color = colors[pos] draw.point((x, y), fill=(color, 255, 255)) -image.save('result.png') +image.save("result.png") diff --git a/pil_pillow__examples/thumbnails/main.py b/pil_pillow__examples/thumbnails/main.py index ba5910aeb..668ba6756 100644 --- a/pil_pillow__examples/thumbnails/main.py +++ b/pil_pillow__examples/thumbnails/main.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install Pillow from PIL import Image + image_file = "input.jpg" img = Image.open(image_file) size = 128, 128 img.thumbnail(size) -img.save('output_thumbnail.png') +img.save("output_thumbnail.png") img.show() diff --git a/pipette/main.py b/pipette/main.py index addd5d231..ad58972e9 100644 --- a/pipette/main.py +++ b/pipette/main.py @@ -1,23 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://habrahabr.ru/post/346146/ +import time + # pip install pyautogui import pyautogui + from graphics import GraphWin, Circle, Point, Entry, color_rgb -import time -win = GraphWin("pipetka", 200, 200, autoflush=True) # создаем графическую форму размером 200х200 и элементы на ней + +# Создаем графическую форму размером 200х200 и элементы на ней +win = GraphWin( + "pipetka", 200, 200, autoflush=True +) x, y = pyautogui.position() # получаем в x, y координаты мыши r, g, b = pyautogui.pixel(x, y) # получаем в r, g, b цвет ColorDot = Circle(Point(100, 100), 25) # создаем точку, отображающую цвет -ColorDot.setFill(color_rgb(r, g, b)) # устанавливает ей заливку из ранее полученных цветов +# Устанавливает ей заливку из ранее полученных цветов +ColorDot.setFill( + color_rgb(r, g, b) +) ColorDot.draw(win) # рисуем на форме win RGBtext = Entry(Point(win.getWidth() / 2, 25), 10) # создаем RGB вывод @@ -37,7 +46,7 @@ ColorDot.setFill(color_rgb(r, g, b)) # Обновляем цвет RGBtext.setText(pyautogui.pixel(x, y)) # Обновляем RGB RGBstring.setText(color_rgb(r, g, b)) # Обновляем web цвет - Coordstring.setText('{} x {}'.format(x, y)) # Обновляем координаты + Coordstring.setText(f"{x} x {y}") # Обновляем координаты if win.isClosed(): break diff --git a/pixelate_image_gui/main.py b/pixelate_image_gui/main.py index 718acb70f..cd121acd3 100644 --- a/pixelate_image_gui/main.py +++ b/pixelate_image_gui/main.py @@ -1,20 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os -from PyQt5 import Qt +import sys +import traceback + from PIL import Image +from PyQt5 import Qt + # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/1c1ad83de7c0f0bd02d9926fe141da4ba5b92720/pil_example/pixelate_image/main.py def pixelate(image, pixel_size=9, draw_margin=True): margin_color = (0, 0, 0) - image = image.resize((image.size[0] // pixel_size, image.size[1] // pixel_size), Image.NEAREST) - image = image.resize((image.size[0] * pixel_size, image.size[1] * pixel_size), Image.NEAREST) + image = image.resize( + (image.size[0] // pixel_size, image.size[1] // pixel_size), Image.NEAREST + ) + image = image.resize( + (image.size[0] * pixel_size, image.size[1] * pixel_size), Image.NEAREST + ) pixel = image.load() # Draw black margin between pixels @@ -22,36 +30,34 @@ def pixelate(image, pixel_size=9, draw_margin=True): for i in range(0, image.size[0], pixel_size): for j in range(0, image.size[1], pixel_size): for r in range(pixel_size): - pixel[i+r, j] = margin_color - pixel[i, j+r] = margin_color + pixel[i + r, j] = margin_color + pixel[i, j + r] = margin_color return image -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - Qt.QMessageBox.critical(None, 'Error', text) + Qt.QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions -WINDOW_TITLE = 'Pixelate' +WINDOW_TITLE = "Pixelate" class MainWindow(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle(WINDOW_TITLE) - self.last_load_path = '.' + self.last_load_path = "." self.file_name = None self.image_source = None self.image_result = None @@ -59,20 +65,20 @@ def __init__(self): self.lb_image_view = Qt.QLabel() self.lb_image_view.setAlignment(Qt.Qt.AlignCenter) - self.pb_load_image = Qt.QPushButton('Load') + self.pb_load_image = Qt.QPushButton("Load") self.pb_load_image.clicked.connect(self.load) - self.pb_save_as = Qt.QPushButton('Save as...') + self.pb_save_as = Qt.QPushButton("Save as...") self.pb_save_as.clicked.connect(self.save_as) - self.rb_original = Qt.QRadioButton('Original') + self.rb_original = Qt.QRadioButton("Original") self.rb_original.clicked.connect(self._do_pixelate) - self.rb_result = Qt.QRadioButton('Result') + self.rb_result = Qt.QRadioButton("Result") self.rb_result.setChecked(True) self.rb_result.clicked.connect(self._do_pixelate) - self.cb_draw_margin = Qt.QCheckBox('Draw margin') + self.cb_draw_margin = Qt.QCheckBox("Draw margin") self.cb_draw_margin.setChecked(True) self.cb_draw_margin.clicked.connect(self._do_pixelate) @@ -81,7 +87,9 @@ def __init__(self): self.sl_pixel_size.setValue(9) self.sb_pixel_size = Qt.QSpinBox() - self.sb_pixel_size.setRange(self.sl_pixel_size.minimum(), self.sl_pixel_size.maximum()) + self.sb_pixel_size.setRange( + self.sl_pixel_size.minimum(), self.sl_pixel_size.maximum() + ) self.sb_pixel_size.setValue(self.sl_pixel_size.value()) self.sl_pixel_size.sliderMoved.connect(self.sb_pixel_size.setValue) @@ -101,7 +109,7 @@ def __init__(self): layout_button.addWidget(self.cb_draw_margin) layout_button_2 = Qt.QHBoxLayout() - layout_button_2.addWidget(Qt.QLabel('Pixel size:')) + layout_button_2.addWidget(Qt.QLabel("Pixel size:")) layout_button_2.addWidget(self.sl_pixel_size) layout_button_2.addWidget(self.sb_pixel_size) @@ -117,30 +125,34 @@ def __init__(self): self._do_pixelate() - def load(self): + def load(self) -> None: image_filters = "Images (*.jpg *.jpeg *.png *.bmp)" - self.file_name = Qt.QFileDialog.getOpenFileName(self, "Load image", self.last_load_path, image_filters)[0] + self.file_name = Qt.QFileDialog.getOpenFileName( + self, "Load image", self.last_load_path, image_filters + )[0] if not self.file_name: return self.last_load_path = Qt.QFileInfo(self.file_name).absolutePath() - self.image_source = Image.open(self.file_name).convert('RGB') + self.image_source = Image.open(self.file_name).convert("RGB") self._do_pixelate() - def save_as(self): + def save_as(self) -> None: image_filters = "Images (*.jpg *.jpeg *.png *.bmp)" # C:\Images\img.png -> C:\Images\img_pixelate.png - file_name = '_pixelate'.join(os.path.splitext(self.file_name)) + file_name = "_pixelate".join(os.path.splitext(self.file_name)) - file_name = Qt.QFileDialog.getSaveFileName(self, "Save as image", file_name, image_filters)[0] + file_name = Qt.QFileDialog.getSaveFileName( + self, "Save as image", file_name, image_filters + )[0] if not file_name: return self.image_result.save(file_name) - def _do_pixelate(self): + def _do_pixelate(self) -> None: # Виджеты будут доступны только если стоит флаг на Result и картинка загружена ok = self.rb_result.isChecked() and self.image_source is not None self.pb_save_as.setEnabled(ok) @@ -157,7 +169,7 @@ def _do_pixelate(self): self.show_result() - def show_result(self): + def show_result(self) -> None: image_result = self.image_result if self.rb_result.isChecked() else self.image_source if not image_result: return @@ -165,20 +177,22 @@ def show_result(self): image_result = image_result.toqpixmap() size = self.lb_image_view.size() - pixmap = image_result.scaled(size, Qt.Qt.KeepAspectRatio, Qt.Qt.SmoothTransformation) + pixmap = image_result.scaled( + size, Qt.Qt.KeepAspectRatio, Qt.Qt.SmoothTransformation + ) self.lb_image_view.setPixmap(pixmap) height, width = self.image_source.size - title = WINDOW_TITLE + '. {}x{} ({}x{}). {}'.format(width, height, size.width(), size.height(), self.file_name) + title = f"{WINDOW_TITLE}. {width}x{height} ({size.width()}x{size.height()}). {self.file_name}" self.setWindowTitle(title) - def resizeEvent(self, e): + def resizeEvent(self, e) -> None: super().resizeEvent(e) self.show_result() -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = MainWindow() diff --git a/play mp3/pyglet_example.py b/play mp3/pyglet_example.py index 48c44a09e..7ee7eeb93 100644 --- a/play mp3/pyglet_example.py +++ b/play mp3/pyglet_example.py @@ -1,7 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import os +import pyglet # # SIMPLE EXAMPLE @@ -11,10 +15,8 @@ # pyglet.app.run() -def play(file_name): - import pyglet - import os - dll_file_name = os.path.join(os.path.dirname(__file__), 'avbin') +def play(file_name) -> None: + dll_file_name = os.path.join(os.path.dirname(__file__), "avbin") pyglet.lib.load_library(dll_file_name) player = pyglet.media.Player() @@ -23,7 +25,7 @@ def play(file_name): player.play() - def update(dt): + def update(dt) -> None: if not player.playing: # Отпишем функцию, иначе при повторном вызове, иначе # будет двойной вызов при следующем воспроизведении @@ -35,5 +37,5 @@ def update(dt): pyglet.app.run() -if __name__ == '__main__': - play(r'speak.mp3') +if __name__ == "__main__": + play(r"speak.mp3") diff --git a/play_audio/play.py b/play_audio/play.py index 4bed644be..7fc5567e8 100644 --- a/play_audio/play.py +++ b/play_audio/play.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - import sys +import os +import sys +import play_audio + + +if __name__ == "__main__": if len(sys.argv) > 1: - import play_audio play_audio.play(sys.argv[1]) - else: - import os file_name = os.path.basename(sys.argv[0]) - print('usage: {} [-h] audio_file_name'.format(file_name)) + print(f"usage: {file_name} [-h] audio_file_name") diff --git a/play_audio/play_audio.py b/play_audio/play_audio.py index 3211fcf9f..a6df2a2f7 100644 --- a/play_audio/play_audio.py +++ b/play_audio/play_audio.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -21,7 +21,7 @@ Phonon.createPath(audio, output) -def play(file_name): +def play(file_name) -> None: audio.setCurrentSource(Phonon.MediaSource(file_name)) audio.play() diff --git a/play_mp3_with_mp3play_module/play.py b/play_mp3_with_mp3play_module/play.py index ce2835749..10ac62ca3 100644 --- a/play_mp3_with_mp3play_module/play.py +++ b/play_mp3_with_mp3play_module/play.py @@ -1,23 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import time + # https://pypi.python.org/pypi/mp3play/ +import mp3play -def play(filename): - import mp3play +def play(filename) -> None: clip = mp3play.load(filename) clip.play() - import time time.sleep(min(30, clip.seconds())) clip.stop() -if __name__ == '__main__': +if __name__ == "__main__": # TODO: не работает: print 'Error %s for "%s": %s' % (str(err), txt, buf) # ^SyntaxError: invalid syntax - play(r'speak.mp3') + play(r"speak.mp3") diff --git a/play_sound__pyqt/main.py b/play_sound__pyqt/main.py index 8c3899bb5..8229d0c1d 100644 --- a/play_sound__pyqt/main.py +++ b/play_sound__pyqt/main.py @@ -1,12 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import os + +from multiprocessing import Process + from PyQt5.QtCore import QCoreApplication, QUrl, QTimer from PyQt5.QtMultimedia import QMediaPlaylist, QMediaContent, QMediaPlayer -import os def play(file_name: str): @@ -24,7 +27,7 @@ def play(file_name: str): player.setPlaylist(playlist) player.play() - def tick(): + def tick() -> None: if player.state() == QMediaPlayer.StoppedState: app.quit() @@ -35,24 +38,25 @@ def tick(): app.exec() -def play_async(file_name: str): +def play_async(file_name: str) -> None: # from threading import Thread # thread = Thread(target=play, args=(file_name,)) # thread.start() - from multiprocessing import Process process = Process(target=play, args=(file_name,)) process.start() -if __name__ == '__main__': - file_name = r'..\speak\play_mp3\speak_male.mp3' - +if __name__ == "__main__": + file_name = r"..\speak\play_mp3\speak_male.mp3" play(file_name) import time + play_async(file_name) time.sleep(0.2) + play_async(file_name) time.sleep(0.2) + play_async(file_name) diff --git a/play_sound__pyqt/play.py b/play_sound__pyqt/play.py index dddc6af95..75e100d02 100644 --- a/play_sound__pyqt/play.py +++ b/play_sound__pyqt/play.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - import sys +import os +import sys +from main import play + + +if __name__ == "__main__": if len(sys.argv) > 1: - from main import play play(sys.argv[1]) - else: - import os file_name = os.path.basename(sys.argv[0]) - print('usage: {} [-h] audio_file_name'.format(file_name)) + print(f"usage: {file_name} [-h] audio_file_name") diff --git a/play_url_mp3/play_url_mp3.py b/play_url_mp3/play_url_mp3.py index cd49e75fe..8a27d6685 100644 --- a/play_url_mp3/play_url_mp3.py +++ b/play_url_mp3/play_url_mp3.py @@ -1,35 +1,36 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from urllib.request import urlretrieve -from urllib.parse import urlparse - import os import sys +from urllib.request import urlretrieve +from urllib.parse import urlparse + from PySide.QtGui import * from PySide.phonon import Phonon -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) audio = Phonon.MediaObject() - audio.setCurrentSource(Phonon.MediaSource('')) + audio.setCurrentSource(Phonon.MediaSource("")) output = Phonon.AudioOutput(Phonon.MusicCategory) Phonon.createPath(audio, output) widget = QWidget() - widget.setWindowTitle('Simple url audio player') + widget.setWindowTitle("Simple url audio player") line_edit_url = QLineEdit() - line_edit_url.setText('https://www.dropbox.com/sh/a4bwzhn0s584u73/AACgDRii1rtCmoL3mXEwlXDga/CallYourName.mp3?dl=1') - + line_edit_url.setText( + "https://www.dropbox.com/sh/a4bwzhn0s584u73/AACgDRii1rtCmoL3mXEwlXDga/CallYourName.mp3?dl=1" + ) - def play(): + def play() -> None: url = line_edit_url.text() # Выдираем из url имя файла @@ -40,17 +41,15 @@ def play(): audio.setCurrentSource(Phonon.MediaSource(file_name)) audio.play() - - play_url = QPushButton('Play') + play_url = QPushButton("Play") play_url.clicked.connect(play) layout = QHBoxLayout() - layout.addWidget(QLabel('Url audio:')) + layout.addWidget(QLabel("Url audio:")) layout.addWidget(line_edit_url) layout.addWidget(play_url) widget.setLayout(layout) widget.show() - sys.exit(app.exec_()) diff --git a/play_url_mp3/setup.py b/play_url_mp3/setup.py index ef4a9cdf4..4b92e25f0 100644 --- a/play_url_mp3/setup.py +++ b/play_url_mp3/setup.py @@ -9,12 +9,11 @@ from cx_Freeze import setup, Executable -executables = [ - Executable('play_url_mp3.py') -] +executables = [Executable("play_url_mp3.py")] -setup(name='play_url_mp3', - version='0.1', - description='play_url_mp3.py', - executables=executables - ) +setup( + name="play_url_mp3", + version="0.1", + description="play_url_mp3.py", + executables=executables, +) diff --git a/playwright__examples/README.md b/playwright__examples/README.md new file mode 100644 index 000000000..b22502b53 --- /dev/null +++ b/playwright__examples/README.md @@ -0,0 +1,15 @@ +Install: +``` +pip install playwright==1.52.0 +``` + +Download browsers: +``` +playwright install +``` + +Download browsers (proxy): +``` +set HTTPS_PROXY=https://: +playwright install +``` diff --git a/playwright__examples/__init__.py b/playwright__examples/__init__.py new file mode 100644 index 000000000..f6a8eac64 --- /dev/null +++ b/playwright__examples/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + diff --git a/playwright__examples/get_cookies.py b/playwright__examples/get_cookies.py new file mode 100644 index 000000000..9af43b8ba --- /dev/null +++ b/playwright__examples/get_cookies.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time +from playwright.sync_api import sync_playwright + + +with sync_playwright() as p: + for browser_type in [p.chromium, p.firefox, p.webkit]: + browser = browser_type.launch() + page = browser.new_page() + + print(browser_type.name.upper()) + + print("Cookies:", page.context.cookies()) + + for k, v in [ + ("name", 123), + ("foo", "bar"), + ("browser", browser_type.name), + ]: + page.goto(f"https://httpbin.org/cookies/set/{k}/{v}") + time.sleep(1) + + rs = page.goto("https://httpbin.org/cookies") + print("From API:", rs.json()) + + cookies = page.context.cookies() + print(f"Cookies ({len(cookies)}):") + for cookie in cookies: + print(f" {cookie}") + + print() + + browser.close() +""" +CHROMIUM +Cookies: [] +From API: {'cookies': {'browser': 'chromium', 'foo': 'bar', 'name': '123'}} +Cookies (3): + {'name': 'name', 'value': '123', 'domain': 'httpbin.org', 'path': '/', 'expires': -1, 'httpOnly': False, 'secure': False, 'sameSite': 'Lax'} + {'name': 'foo', 'value': 'bar', 'domain': 'httpbin.org', 'path': '/', 'expires': -1, 'httpOnly': False, 'secure': False, 'sameSite': 'Lax'} + {'name': 'browser', 'value': 'chromium', 'domain': 'httpbin.org', 'path': '/', 'expires': -1, 'httpOnly': False, 'secure': False, 'sameSite': 'Lax'} + +FIREFOX +Cookies: [] +From API: {'cookies': {'browser': 'firefox', 'foo': 'bar', 'name': '123'}} +Cookies (3): + {'name': 'name', 'value': '123', 'domain': 'httpbin.org', 'path': '/', 'expires': -1, 'httpOnly': False, 'secure': False, 'sameSite': 'None'} + {'name': 'foo', 'value': 'bar', 'domain': 'httpbin.org', 'path': '/', 'expires': -1, 'httpOnly': False, 'secure': False, 'sameSite': 'None'} + {'name': 'browser', 'value': 'firefox', 'domain': 'httpbin.org', 'path': '/', 'expires': -1, 'httpOnly': False, 'secure': False, 'sameSite': 'None'} + +WEBKIT +Cookies: [] +From API: {'cookies': {}} +Cookies (0): + +""" diff --git a/playwright__examples/get_page_text.py b/playwright__examples/get_page_text.py new file mode 100644 index 000000000..a7e103d4a --- /dev/null +++ b/playwright__examples/get_page_text.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from playwright.sync_api import sync_playwright + + +with sync_playwright() as p: + browser = p.webkit.launch() + page = browser.new_page() + + rs = page.goto("https://httpbin.org/") + print(rs.text()) + + browser.close() diff --git a/playwright__examples/get_screenshot.py b/playwright__examples/get_screenshot.py new file mode 100644 index 000000000..edd891a28 --- /dev/null +++ b/playwright__examples/get_screenshot.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path +from playwright.sync_api import sync_playwright + +DIR: Path = Path(__file__).resolve().parent +DIR_SCREENSHOTS: Path = DIR / "screenshots" +DIR_SCREENSHOTS.mkdir(parents=True, exist_ok=True) + + +with sync_playwright() as p: + for browser_type in [p.chromium, p.firefox, p.webkit]: + print(browser_type.name) + + browser = browser_type.launch() + page = browser.new_page() + + page.goto("https://github.com/gil9red") + + img_date: bytes = page.screenshot() + print(f"img_date: {img_date[:50]}...") + + path_page: Path = DIR_SCREENSHOTS / f"{browser_type.name}_page.png" + page.screenshot(path=path_page) + print(f"path_page: {path_page}") + + path_full_page: Path = DIR_SCREENSHOTS / f"{browser_type.name}_full_page.png" + page.screenshot(path=path_full_page, full_page=True) + print(f"path_full_page: {path_full_page}") + + print() diff --git a/playwright__examples/get_user_agents.py b/playwright__examples/get_user_agents.py new file mode 100644 index 000000000..2b657fb5b --- /dev/null +++ b/playwright__examples/get_user_agents.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from playwright.sync_api import sync_playwright + + +with sync_playwright() as p: + for browser_type in [p.chromium, p.firefox, p.webkit]: + browser = browser_type.launch() + page = browser.new_page() + + print(browser_type.name.upper()) + print("Local:", page.evaluate("navigator.userAgent")) + + context = browser.new_context(user_agent="Abc 123") + print("Set local:", context.new_page().evaluate("navigator.userAgent")) + + rs = page.goto("https://httpbin.org/user-agent") + print("Remote:", rs.json()["user-agent"]) + + print() + + browser.close() +""" +CHROMIUM +Local: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/131.0.6778.33 Safari/537.36 +Set local: Abc 123 +Remote: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/131.0.6778.33 Safari/537.36 + +FIREFOX +Local: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0 +Set local: Abc 123 +Remote: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0 + +WEBKIT +Local: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15 +Set local: Abc 123 +Remote: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15 +""" diff --git a/playwright__examples/human_automation/__init__.py b/playwright__examples/human_automation/__init__.py new file mode 100644 index 000000000..3c994a1db --- /dev/null +++ b/playwright__examples/human_automation/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" diff --git a/playwright__examples/human_automation/examples/__init__.py b/playwright__examples/human_automation/examples/__init__.py new file mode 100644 index 000000000..3c994a1db --- /dev/null +++ b/playwright__examples/human_automation/examples/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" diff --git a/playwright__examples/human_automation/examples/github.com_gil9red.py b/playwright__examples/human_automation/examples/github.com_gil9red.py new file mode 100644 index 000000000..dfb200a0f --- /dev/null +++ b/playwright__examples/human_automation/examples/github.com_gil9red.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# playwright>=1.61.0 +from playwright.sync_api import sync_playwright + +from human_automation.human_automation import HumanAutomation + +URL: str = "https://github.com/gil9red" + + +with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + page = browser.new_page() + + human_automation = HumanAutomation(page) + + page.goto(URL, wait_until="domcontentloaded") + + part_url: str = "tab=repositories" + + def click_on_repositories() -> None: + link_locator = page.locator(f'nav a[href *= "{part_url}"]').first + human_automation.click(link_locator) + + human_automation.ensure_change_url( + action=click_on_repositories, + is_ok_url=lambda url: part_url in url, + ) + + print("\n") + human_automation.wait(300, 600) + + part_url: str = "/SimplePyScripts" + + def find_repository() -> None: + find_selector = 'input[placeholder="Find a repository…"]' + find_locator = page.locator(find_selector) + human_automation.type_text(find_locator, "SimplePyScripts") + + link_selector = ( + f'#user-repositories-list a[itemprop *= "name"][href *= "{part_url}"]' + ) + locator = page.locator(link_selector).first + + human_automation.click(locator) + + human_automation.ensure_change_url( + action=find_repository, + is_ok_url=lambda url: part_url in url, + ) + + human_automation.wait(300, 600) + print("\n") + + human_automation.move_mouse_to_center() + + # Step 1: Go /playwright__examples + part_url: str = "/playwright__examples" + + def click_on_step_1() -> None: + dir_locator = page.get_by_role("link", name=part_url[1:]).first + human_automation.click(dir_locator) + + human_automation.ensure_change_url( + action=click_on_step_1, + is_ok_url=lambda url: part_url in url, + ) + + print("\n") + human_automation.wait(300, 600) + + # Step 2: Go /playwright__examples/screenshots + part_url: str = f"{part_url}/screenshots" + + def click_on_step_2() -> None: + dir_locator = page.locator(f'a[href$="{part_url}"]:visible').first + human_automation.click(dir_locator) + + human_automation.ensure_change_url( + action=click_on_step_2, + is_ok_url=lambda url: url.endswith(part_url), + ) + + print("\n") + human_automation.wait(300, 600) + + # Step 3: Go /playwright__examples/screenshots/stealth + part_url: str = f"{part_url}/stealth" + + def click_on_step_3() -> None: + dir_locator = page.locator(f'a[href$="{part_url}"]:visible').first + human_automation.click(dir_locator) + + human_automation.ensure_change_url( + action=click_on_step_3, + is_ok_url=lambda url: part_url in url, + ) + + print("\n") + human_automation.wait(300, 600) + + # Step 4: Go /playwright__examples/screenshots/stealth/stealth_firefox_page.png + part_url: str = f"{part_url}/stealth_firefox_page.png" + + def click_on_step_4() -> None: + dir_locator = page.locator(f'a[href$="{part_url}"]:visible').first + human_automation.click(dir_locator) + + human_automation.ensure_change_url( + action=click_on_step_4, + is_ok_url=lambda url: part_url in url, + ) + + print("\n") + human_automation.wait(300, 600) + + # Download /playwright__examples/screenshots/stealth/stealth_firefox_page.png + img_locator = page.locator(f'img[src *= "{part_url}"]').first + print(img_locator) + + img_url: str = img_locator.get_attribute("src") + full_url: str = page.evaluate(f"new URL('{img_url}', window.location.href).href") + print(full_url) + + response = page.request.get(full_url) + img_bytes: bytes = response.body() + print(f"img_bytes ({len(img_bytes)} bytes): {img_bytes[:50]}...") + + page.wait_for_timeout(2_000) diff --git a/playwright__examples/human_automation/examples/store_steampowered_com.py b/playwright__examples/human_automation/examples/store_steampowered_com.py new file mode 100644 index 000000000..f5c093484 --- /dev/null +++ b/playwright__examples/human_automation/examples/store_steampowered_com.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# playwright>=1.61.0 +from playwright.sync_api import sync_playwright + +from human_automation.human_automation import HumanAutomation + +URL: str = "https://store.steampowered.com/" + + +with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + page = browser.new_page() + + human_automation = HumanAutomation(page) + + page.goto(URL, wait_until="domcontentloaded") + + categories_locator = page.get_by_label("Меню магазина").locator( + "button > div:has-text('Категории')" + ) + print(categories_locator) + human_automation.click(categories_locator) + + # View all tags + all_tags_locator = page.locator("a:has-text('Просмотреть все метки')") + print(all_tags_locator) + human_automation.click(all_tags_locator) + + # Free To Play + free_to_play_locator = page.get_by_role("button", name="Бесплатная игра") + print(categories_locator) + human_automation.click(free_to_play_locator) + + go_to_free_to_play_locator = page.get_by_text("Просмотреть все") + print(go_to_free_to_play_locator) + human_automation.click(go_to_free_to_play_locator) + + filters_locator = page.locator("//div[text()='Фильтры']/..").first + print(filters_locator) + human_automation.move_to(filters_locator) + + genres_frame_locator = filters_locator.locator("//div[text()='Жанры']/../..").first + print(genres_frame_locator) + human_automation.move_to(genres_frame_locator) + + genres_locator = genres_frame_locator.locator("//div[text()='Жанры']") + print(genres_locator) + human_automation.click(genres_locator) + + genres_all_locator = genres_frame_locator.locator("//div[text()='Показать больше']") + print(genres_all_locator) + human_automation.click(genres_all_locator) + + genre_shooter_locator = genres_frame_locator.locator("//div/a[text()='Шутер']") + print(genre_shooter_locator) + human_automation.click(genre_shooter_locator) + + sub_genres_frame_locator = filters_locator.locator( + "//div[text()='Поджанры']/../.." + ).first + print(sub_genres_frame_locator) + human_automation.move_to(sub_genres_frame_locator) + + genres_locator = sub_genres_frame_locator.locator("//div[text()='Поджанры']") + print(genres_locator) + human_automation.click(genres_locator) + + sub_genres_all_locator = sub_genres_frame_locator.locator( + "//div[text()='Показать больше']" + ) + print(sub_genres_all_locator) + human_automation.click(sub_genres_all_locator) + + fps_locator = sub_genres_frame_locator.locator( + "//div/a[text()='Шутер от первого лица']" + ) + print(fps_locator) + human_automation.click(fps_locator) + + human_automation.click(page.get_by_role("button", name="Показать больше")) + + human_automation.move_to( + page.locator("a[href *= '/app/'][role='button']:has(img[alt]):visible").first + ) + human_automation.wait(2_000, 4_000) + + for locator in page.locator( + "a[href *= '/app/'][role='button']:has(img[alt]):visible" + ).all(): + print(locator) + + name: str = locator.locator("img[alt]").get_attribute("alt") + URL: str = locator.get_attribute("href") + print(f"{name!r}: {URL}") + + human_automation.move_to(locator) + + print() + + page.wait_for_timeout(5_000) diff --git a/playwright__examples/human_automation/examples/translate_google_ru.py b/playwright__examples/human_automation/examples/translate_google_ru.py new file mode 100644 index 000000000..f3ad0c2bb --- /dev/null +++ b/playwright__examples/human_automation/examples/translate_google_ru.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from textwrap import dedent + +# playwright>=1.61.0 +from playwright.sync_api import Page, sync_playwright + +from human_automation.human_automation import HumanAutomation + + +def get_text(page: Page, lang: str) -> str: + return "\n".join( + span.text_content().strip() + for span in page.locator(f'span[lang="{lang}"] > span > span').all() + ) + + +URL: str = "https://translate.google.ru/?sl=ru&tl=en&op=translate" + + +with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + page = browser.new_page() + + human_automation = HumanAutomation(page) + + page.goto(URL, wait_until="domcontentloaded") + + input_locator = page.locator('textarea[aria-label="Исходный текст"]') + + human_automation.type_text( + input_locator, + text=dedent(""" + Съешь же ещё этих мягких + французских булок, + да выпей же чаю + """).strip(), + ) + + page.wait_for_timeout(2_000) + + text: str = get_text(page, lang="en") + print(f"Text ({len(text)}):\n{text}") + + page.wait_for_timeout(2_000) + + human_automation.click('button[aria-label *= "Обратный перевод"]:visible') + + page.wait_for_timeout(2_000) + + human_automation.type_text( + input_locator, + text=dedent(""" + The quick brown fox jumps + over the lazy dog + """).strip(), + ) + + page.wait_for_timeout(2_000) + + text: str = get_text(page, lang="ru") + print(f"Text ({len(text)}):\n{text}") + + page.wait_for_timeout(5_000) diff --git a/playwright__examples/human_automation/human_automation.py b/playwright__examples/human_automation/human_automation.py new file mode 100644 index 000000000..85c5bcd23 --- /dev/null +++ b/playwright__examples/human_automation/human_automation.py @@ -0,0 +1,421 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import functools +import logging +import time +import random +import re +import string + +from typing import Any, Callable, TypedDict + +# playwright>=1.61.0 +from playwright.sync_api import Page, Locator, TimeoutError + +# playwright-stealth>=2.0.3 +from playwright_stealth import Stealth + +# python-ghost-cursor==0.1.1 +from python_ghost_cursor.playwright_sync import create_cursor +from python_ghost_cursor.playwright_sync._spoof import GhostCursor + +type Point = TypedDict("Point", {"x": int, "y": int}) + + +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def log_action[**P, R](func: Callable[P, R]) -> Callable[P, R]: + @functools.wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + func_name: str = func.__name__ + + locator: Any | None = kwargs.get("locator") + if not locator and len(args) > 1: + locator = args[1] + if not isinstance(locator, str | Locator): + locator = None + locator_str: str = f" to: {locator}" if locator else "" + + log.debug(f"Started {func_name}{locator_str}") + + start_time: float = time.perf_counter() + try: + result: R = func(*args, **kwargs) + + execution_time_ms: int = int((time.perf_counter() - start_time) * 1000) + log.debug(f"Finished {func_name}{locator_str} ({execution_time_ms}ms)") + return result + + except Exception as e: + execution_time_ms: int = int((time.perf_counter() - start_time) * 1000) + log.error( + f"Failed {func_name}{locator_str} after {execution_time_ms}ms. Error: {e}" + ) + raise e + + return wrapper + + +stealth = Stealth() + + +def get_center_page(page: Page) -> Point: + # Get the size of the current browser window + viewport: dict[str, int] | None = page.viewport_size + + base_x: int + base_y: int + if viewport: + base_x = viewport["width"] // 2 + base_y = viewport["height"] // 2 + else: + base_x, base_y = 640, 360 + + offset_x: int = random.randint(-40, 40) + offset_y: int = random.randint(-40, 40) + + target_x: int = base_x + offset_x + target_y: int = base_y + offset_y + + return {"x": target_x, "y": target_y} + + +def inject_cursor(page: Page) -> None: + page.add_init_script(""" + (() => { + const initCursor = () => { + const box = document.createElement('div'); + box.id = 'playwright-fake-cursor'; + box.style.position = 'fixed'; + box.style.top = '0'; + box.style.left = '0'; + box.style.width = '14px'; + box.style.height = '14px'; + box.style.background = 'rgba(255, 0, 0, 0.7)'; + box.style.border = '2px solid white'; + box.style.borderRadius = '50%'; + box.style.pointerEvents = 'none'; + box.style.zIndex = '999999'; + box.style.transform = 'translate(-50%, -50%)'; + document.body.appendChild(box); + + document.addEventListener('mousemove', (e) => { + box.style.left = e.clientX + 'px'; + box.style.top = e.clientY + 'px'; + }); + }; + + // If the DOM has already been loaded (unlikely for init_script, but useful for safety) + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initCursor); + } else { + initCursor(); + } + })(); + """) + + +TYPO_CHAR_SETS: dict[str, str] = { + "eng_lower": string.ascii_lowercase, + "eng_upper": string.ascii_uppercase, + "rus_lower": "абвгдеёжзийклмнопрстуфхцчшщъыьэюя", + "rus_upper": "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ", + "digits": string.digits, + "punctuation": string.punctuation, +} + +WAIT_MS: tuple[int, int] = 100, 250 + + +class HumanAutomation: + page: Page + cursor: GhostCursor + typo_char_sets: dict[str, str] + + def __init__( + self, + page: Page, + typo_char_sets: dict[str, str] | None = None, + ) -> None: + if typo_char_sets is None: + typo_char_sets = TYPO_CHAR_SETS + + self.page = page + self.cursor = create_cursor(page, start=get_center_page(page)) + self.typo_char_sets = typo_char_sets + + stealth.apply_stealth_sync(page) + inject_cursor(page) + + @log_action + def wait(self, min_time_ms: int, max_time_ms: int) -> None: + time_ms: float = random.randint(min_time_ms, max_time_ms) + self.page.wait_for_timeout(time_ms) + + @log_action + def move_to( + self, + locator: Locator | str, + wait_ms: tuple[int, int] = WAIT_MS, + ) -> None: + if isinstance(locator, str): + locator = self.page.locator(locator) + + self.scroll_to_element(locator) + + self.cursor.move(locator) + self.wait(*wait_ms) + + @log_action + def click( + self, + locator: Locator | str, + wait_ms: tuple[int, int] = WAIT_MS, + ) -> None: + if isinstance(locator, str): + locator = self.page.locator(locator) + + # NOTE: move_to is not suitable here - the cursor moves twice + # It internally calls cursor.move, which moves the cursor + # click internally calls cursor.click, which also calls cursor.move + self.scroll_to_element(locator) + + # NOTE: If this is a link, removing the 'target' attribute will prevent opening in a new tab + locator.evaluate("el => el.removeAttribute('target')") + + self.cursor.click(locator) + self.wait(*wait_ms) + + @log_action + def scroll_to_element( + self, + locator: Locator | str, + margin: int = 100, + wait_ms: tuple[int, int] = WAIT_MS, + ) -> None: + """ + Simulates human scrolling. + If the element is fully visible on the screen, no scrolling occurs. + If the element is hidden (at the top or bottom), it will smoothly scroll closer to the center. + """ + + if isinstance(locator, str): + locator = self.page.locator(locator) + + while True: + box: dict[str, float] | None + try: + box = locator.bounding_box(timeout=100) + except TimeoutError: + box = None + + log.debug(f"Box: {box}") + if not box: + # If the element is not in the DOM at all, scroll down randomly + self.page.mouse.wheel(0, random.randint(250, 450)) + self.wait(*wait_ms) + continue + + viewport_height: int = self.page.viewport_size["height"] + log.debug(f"Viewport height: {viewport_height}") + + # Find the boundaries of the element relative to the screen + element_top = box["y"] + element_height = box["height"] + element_bottom = element_top + element_height + + is_top_visible = element_top >= margin + is_bottom_within_viewport = element_bottom <= (viewport_height - margin) + is_too_large = element_height > (viewport_height - (margin * 2)) + is_bottom_visible = is_bottom_within_viewport or is_too_large + if is_top_visible and is_bottom_visible: # The element is perfectly visible + break + + # If the element is not on the screen, calculate the distance to center for scrolling + element_center_y = element_top + (element_height / 2) + screen_center_y = viewport_height / 2 + distance_to_center = element_center_y - screen_center_y + + # If we are already very close to the center, stop + if abs(distance_to_center) < random.randint(40, 80): + break + + scroll_step: int + if distance_to_center > 0: + # Scroll down (target below screen) + scroll_step = random.randint(150, 350) + else: + # Scroll up (target above screen) + scroll_step = random.randint(-350, -150) + + self.page.mouse.wheel(0, scroll_step) + self.wait(*wait_ms) + + self.wait(*wait_ms) + + def _typo_effect( + self, + char: str, + typo_chance: float, + wait_ms: tuple[int, int] = WAIT_MS, + ) -> None: + # Logic for random typo (don't break spaces) + if char == " " or random.random() >= typo_chance: + return + + pool = "" + for s_set in self.typo_char_sets.values(): + if char in s_set: + pool = s_set + break + + # If the symbol is rare and is not in the pools, we create a default list (letters + numbers) + if not pool: + pool: str = self.typo_char_sets["eng_lower"] + self.typo_char_sets["digits"] + + valid_choices: list[str] = [c for c in pool if c != char] + if not valid_choices: + log.warning(f"No valid choices found for character {char!r}.") + return + + wrong_char: str = random.choice(valid_choices) + + self.page.keyboard.type(wrong_char) + self.wait(60, 140) + self.wait(180, 350) # Pause for recognizing the error + self.page.keyboard.press("Backspace") + self.wait(*wait_ms) + + @log_action + def type_text( + self, + locator: Locator | str, + text: str, + typo_chance: float = 0.07, + max_attempts: int = 3, + strict_validation: bool = False, + wait_ms: tuple[int, int] = WAIT_MS, + ) -> bool: + """ + Types text like a human: with unique pauses, random typos, + deletion of them through Backspace and final validation of the input value. + """ + + if isinstance(locator, str): + locator = self.page.locator(locator) + + log.info(f"Typing text ({len(text)}): {text!r}") + + for attempt in range(1, max_attempts + 1): + # Click on the field to bring focus + self.click(locator) + + # Realistically clear the field through keyboard emulation of Ctrl+A -> Backspace + self.page.keyboard.down("Control") + self.page.keyboard.press("a") + self.page.keyboard.up("Control") + self.page.keyboard.press("Backspace") + self.wait(*wait_ms) + + # Typing character by character + for char in text: + self._typo_effect(char, typo_chance=typo_chance, wait_ms=wait_ms) + + # Type correct character + self.page.keyboard.type(char) + + # Typing rhythm (delay between keys) + min_time_ms: int = 50 + max_time_ms: int = 160 + # 4% chance that the person thought about a word in the middle of it + if random.random() < 0.04: + min_time_ms += 350 + max_time_ms += 700 + self.wait(min_time_ms, max_time_ms) + + # Validate the actually typed text in the field + actual_text: str = locator.input_value() or "" + + is_valid: bool + if strict_validation: + is_valid = actual_text == text + else: + + def _remove_all_spaces(text: str) -> str: + return re.sub(r"\s+", "", text) + + is_valid = _remove_all_spaces(actual_text) == _remove_all_spaces(text) + + if is_valid: + return True + + log.warning(f"Expected text {text!r}, but got {actual_text!r}.") + + self.wait(400, 800) + + raise RuntimeError( + f"Failed to correctly type {text!r} after {max_attempts} attempts." + ) + + @log_action + def move_mouse_to_center(self) -> None: + """ + Smoothly moves the mouse cursor to a random area around the center of the screen. + Takes: page (a Playwright page object) + """ + + # Move the cursor along a human-like trajectory + point: Point = get_center_page(self.page) + self.cursor.move_to(point) + + # Simulate a small pause after movement (200-600 ms) + self.wait(200, 600) + + @log_action + def ensure_change_url( + self, + action: Callable[[], None], + is_ok_url: Callable[[str], bool], + max_attempts: int = 3, + ) -> None: + prev_url: str = self.page.url + log.info(f"Current URL: {prev_url!r}") + + attempt: int = 0 + while True: + # If the URL substring is found and the URL has changed + if is_ok_url(self.page.url) and prev_url != self.page.url: + break + + try: + action() + + log.info("Waiting for url load") + self.page.wait_for_url( + is_ok_url, timeout=5_000, wait_until="domcontentloaded" + ) + log.info(f"Page changed! New URL: {self.page.url!r}") + + break + + except Exception: + log.exception("Error:") + log.info("Trying to move the cursor and retry") + attempt += 1 + self.cursor.move_to( + { + "x": 10 + random.randint(-10, 10), + "y": 500 + random.randint(-100, 100), + } + ) + if attempt == max_attempts: + log.info(f"Loading {prev_url!r}") + attempt = 0 + self.page.goto(prev_url) + self.wait(4_000, 5_000) diff --git a/playwright__examples/human_automation/screenshots/github.com_gil9red.gif b/playwright__examples/human_automation/screenshots/github.com_gil9red.gif new file mode 100644 index 000000000..14df4e414 Binary files /dev/null and b/playwright__examples/human_automation/screenshots/github.com_gil9red.gif differ diff --git a/playwright__examples/human_automation/screenshots/store_steampowered_com.gif b/playwright__examples/human_automation/screenshots/store_steampowered_com.gif new file mode 100644 index 000000000..eaa969fa6 Binary files /dev/null and b/playwright__examples/human_automation/screenshots/store_steampowered_com.gif differ diff --git a/playwright__examples/human_automation/screenshots/translate_google_ru.gif b/playwright__examples/human_automation/screenshots/translate_google_ru.gif new file mode 100644 index 000000000..7d25d9d64 Binary files /dev/null and b/playwright__examples/human_automation/screenshots/translate_google_ru.gif differ diff --git a/playwright__examples/parse_with_bs4.py b/playwright__examples/parse_with_bs4.py new file mode 100644 index 000000000..4e21a8a67 --- /dev/null +++ b/playwright__examples/parse_with_bs4.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from bs4 import BeautifulSoup +from playwright.sync_api import sync_playwright + + +with sync_playwright() as p: + browser = p.firefox.launch() + page = browser.new_page() + + rs = page.goto("https://httpbin.org/") + text = rs.text() + browser.close() + + soup = BeautifulSoup(text, "html.parser") + print(soup.select_one("a.github-corner")["href"]) + print(soup.select_one(".description").get_text(strip=True, separator="\n")) + """ + https://github.com/requests/httpbin + A simple HTTP Request & Response Service. + Run locally: + $ docker run -p 80:80 kennethreitz/httpbin + """ diff --git a/playwright__examples/print_executable_path.py b/playwright__examples/print_executable_path.py new file mode 100644 index 000000000..fb81bd4be --- /dev/null +++ b/playwright__examples/print_executable_path.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from playwright.sync_api import sync_playwright + + +with sync_playwright() as p: + for browser_type in [p.chromium, p.firefox, p.webkit]: + print(browser_type.name, browser_type.executable_path) +r""" +chromium C:\Users\ipetrash\AppData\Local\ms-playwright\chromium-1148\chrome-win\chrome.exe +firefox C:\Users\ipetrash\AppData\Local\ms-playwright\firefox-1466\firefox\firefox.exe +webkit C:\Users\ipetrash\AppData\Local\ms-playwright\webkit-2104\Playwright.exe +""" diff --git a/playwright__examples/screenshots/chromium_full_page.png b/playwright__examples/screenshots/chromium_full_page.png new file mode 100644 index 000000000..547400daa Binary files /dev/null and b/playwright__examples/screenshots/chromium_full_page.png differ diff --git a/playwright__examples/screenshots/chromium_page.png b/playwright__examples/screenshots/chromium_page.png new file mode 100644 index 000000000..632b26e62 Binary files /dev/null and b/playwright__examples/screenshots/chromium_page.png differ diff --git a/playwright__examples/screenshots/firefox_full_page.png b/playwright__examples/screenshots/firefox_full_page.png new file mode 100644 index 000000000..5a2ffe6b0 Binary files /dev/null and b/playwright__examples/screenshots/firefox_full_page.png differ diff --git a/playwright__examples/screenshots/firefox_page.png b/playwright__examples/screenshots/firefox_page.png new file mode 100644 index 000000000..611bc0d79 Binary files /dev/null and b/playwright__examples/screenshots/firefox_page.png differ diff --git a/playwright__examples/screenshots/stealth/normal_chromium_page.png b/playwright__examples/screenshots/stealth/normal_chromium_page.png new file mode 100644 index 000000000..0e2e6c809 Binary files /dev/null and b/playwright__examples/screenshots/stealth/normal_chromium_page.png differ diff --git a/playwright__examples/screenshots/stealth/normal_firefox_page.png b/playwright__examples/screenshots/stealth/normal_firefox_page.png new file mode 100644 index 000000000..f25c69aa1 Binary files /dev/null and b/playwright__examples/screenshots/stealth/normal_firefox_page.png differ diff --git a/playwright__examples/screenshots/stealth/normal_webkit_page.png b/playwright__examples/screenshots/stealth/normal_webkit_page.png new file mode 100644 index 000000000..53f89ea02 Binary files /dev/null and b/playwright__examples/screenshots/stealth/normal_webkit_page.png differ diff --git a/playwright__examples/screenshots/stealth/stealth_chromium_page.png b/playwright__examples/screenshots/stealth/stealth_chromium_page.png new file mode 100644 index 000000000..36d09711d Binary files /dev/null and b/playwright__examples/screenshots/stealth/stealth_chromium_page.png differ diff --git a/playwright__examples/screenshots/stealth/stealth_firefox_page.png b/playwright__examples/screenshots/stealth/stealth_firefox_page.png new file mode 100644 index 000000000..976fe5b9b Binary files /dev/null and b/playwright__examples/screenshots/stealth/stealth_firefox_page.png differ diff --git a/playwright__examples/screenshots/stealth/stealth_webkit_page.png b/playwright__examples/screenshots/stealth/stealth_webkit_page.png new file mode 100644 index 000000000..0c92edfc2 Binary files /dev/null and b/playwright__examples/screenshots/stealth/stealth_webkit_page.png differ diff --git a/playwright__examples/screenshots/webkit_full_page.png b/playwright__examples/screenshots/webkit_full_page.png new file mode 100644 index 000000000..5cd17dee8 Binary files /dev/null and b/playwright__examples/screenshots/webkit_full_page.png differ diff --git a/playwright__examples/screenshots/webkit_page.png b/playwright__examples/screenshots/webkit_page.png new file mode 100644 index 000000000..68cc4d083 Binary files /dev/null and b/playwright__examples/screenshots/webkit_page.png differ diff --git a/playwright__examples/stealth.py b/playwright__examples/stealth.py new file mode 100644 index 000000000..395e6ba4b --- /dev/null +++ b/playwright__examples/stealth.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +from playwright.sync_api import sync_playwright +from playwright_stealth import Stealth + +DIR: Path = Path(__file__).resolve().parent +DIR_SCREENSHOTS: Path = DIR / "screenshots" / "stealth" +DIR_SCREENSHOTS.mkdir(parents=True, exist_ok=True) + +URL: str = "https://bot.sannysoft.com/" + +stealth = Stealth() + + +with sync_playwright() as p: + for browser_type in [p.chromium, p.firefox, p.webkit]: + print(browser_type.name) + + browser = browser_type.launch() + page = browser.new_page() + + page.goto(URL) + print("navigator.webdriver (default):", page.evaluate("navigator.webdriver")) + + normal_screenshot_path: Path = ( + DIR_SCREENSHOTS / f"normal_{browser_type.name}_page.png" + ) + page.screenshot(path=normal_screenshot_path) + + stealth.apply_stealth_sync(page) + page.goto(URL) + print("navigator.webdriver (stealth):", page.evaluate("navigator.webdriver")) + + stealth_screenshot_path: Path = ( + DIR_SCREENSHOTS / f"stealth_{browser_type.name}_page.png" + ) + page.screenshot(path=stealth_screenshot_path) + + browser.close() # NOTE: Close in loop + print() + +""" +chromium +navigator.webdriver (default): True +navigator.webdriver (stealth): False + +firefox +navigator.webdriver (default): True +navigator.webdriver (stealth): False + +webkit +navigator.webdriver (default): True +navigator.webdriver (stealth): False +""" diff --git a/plural_days.py b/plural_days.py index d3ce2d3be..66dbba1f9 100644 --- a/plural_days.py +++ b/plural_days.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" def plural_days(n: int) -> str: - days = ['день', 'дня', 'дней'] + days = ["день", "дня", "дней"] if n % 10 == 1 and n % 100 != 11: p = 0 @@ -14,9 +14,9 @@ def plural_days(n: int) -> str: else: p = 2 - return f'{n} {days[p]}' + return f"{n} {days[p]}" -if __name__ == '__main__': +if __name__ == "__main__": for i in range(100 + 1): print(plural_days(i)) diff --git a/png2svg.py b/png2svg.py index 2613b31ba..3b5745afc 100644 --- a/png2svg.py +++ b/png2svg.py @@ -1,13 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os.path import sys - try: from PyQt5.QtWidgets import QApplication from PyQt5.QtGui import QPainter, QImage @@ -26,23 +25,21 @@ from PySide.QtSvg import QSvgGenerator -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) - file_name = r'C:\Users\ipetrash\Desktop\explorer.png' + file_name = r"C:\Users\ipetrash\Desktop\explorer.png" im = QImage() im.load(file_name) w, h = im.size().width(), im.size().height() gen = QSvgGenerator() - gen.setFileName(os.path.splitext(file_name)[0] + '.svg') + gen.setFileName(os.path.splitext(file_name)[0] + ".svg") gen.setSize(im.size()) gen.setViewBox(QRect(0, 0, w, h)) painter = QPainter() painter.begin(gen) - painter.drawImage(QPointF(0, 0), - im, - QRectF(0, 0, w, h)) + painter.drawImage(QPointF(0, 0), im, QRectF(0, 0, w, h)) painter.end() diff --git a/pretty_html/pretty_html.py b/pretty_html/pretty_html.py index 0163ea195..e2207e771 100644 --- a/pretty_html/pretty_html.py +++ b/pretty_html/pretty_html.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re @@ -10,11 +10,11 @@ # SOURCE: https://stackoverflow.com/a/15513483/5909792 orig_prettify = BeautifulSoup.prettify -r = re.compile(r'^(\s*)', re.MULTILINE) +r = re.compile(r"^(\s*)", re.MULTILINE) def prettify(self, encoding=None, formatter="minimal", indent_width=4): - return r.sub(r'\1' * indent_width, orig_prettify(self, encoding, formatter)) + return r.sub(r"\1" * indent_width, orig_prettify(self, encoding, formatter)) BeautifulSoup.prettify = prettify @@ -26,17 +26,17 @@ def process_html_string(text: str) -> str: """ - start = text.index('<') - end = text.rindex('>') - return text[start:end + 1] + start = text.index("<") + end = text.rindex(">") + return text[start : end + 1] def pretty_html(text: str) -> str: text = process_html_string(text) - root = BeautifulSoup(text, 'html.parser') + root = BeautifulSoup(text, "html.parser") return root.prettify() -if __name__ == '__main__': - text = '

    ' +if __name__ == "__main__": + text = "

    " print(pretty_html(text)) diff --git a/pretty_html/pretty_html_gui.py b/pretty_html/pretty_html_gui.py index 184374e6c..97317a60a 100644 --- a/pretty_html/pretty_html_gui.py +++ b/pretty_html/pretty_html_gui.py @@ -1,44 +1,75 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from os.path import split as path_split -import traceback import sys +import traceback + +from os.path import split as path_split try: from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QPlainTextEdit, QLabel, QSizePolicy, QPushButton, - QSplitter, QHBoxLayout, QMessageBox, QErrorMessage, QTextEdit, QApplication + QWidget, + QVBoxLayout, + QPlainTextEdit, + QLabel, + QSizePolicy, + QPushButton, + QSplitter, + QHBoxLayout, + QMessageBox, + QErrorMessage, + QTextEdit, + QApplication, ) from PyQt5.QtCore import Qt except: try: from PyQt4.QtGui import ( - QWidget, QVBoxLayout, QPlainTextEdit, QLabel, QSizePolicy, QPushButton, - QSplitter, QHBoxLayout, QMessageBox, QErrorMessage, QTextEdit, QApplication + QWidget, + QVBoxLayout, + QPlainTextEdit, + QLabel, + QSizePolicy, + QPushButton, + QSplitter, + QHBoxLayout, + QMessageBox, + QErrorMessage, + QTextEdit, + QApplication, ) from PyQt4.QtCore import Qt except: from PySide.QtGui import ( - QWidget, QVBoxLayout, QPlainTextEdit, QLabel, QSizePolicy, QPushButton, - QSplitter, QHBoxLayout, QMessageBox, QErrorMessage, QTextEdit, QApplication + QWidget, + QVBoxLayout, + QPlainTextEdit, + QLabel, + QSizePolicy, + QPushButton, + QSplitter, + QHBoxLayout, + QMessageBox, + QErrorMessage, + QTextEdit, + QApplication, ) from PySide.QtCore import Qt from pretty_html import pretty_html -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) @@ -46,7 +77,7 @@ def log_uncaught_exceptions(ex_cls, ex, tb): class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle(path_split(__file__)[1]) @@ -61,9 +92,9 @@ def __init__(self): self.label_error.setTextInteractionFlags(Qt.TextSelectableByMouse) self.label_error.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.button_detail_error = QPushButton('...') + self.button_detail_error = QPushButton("...") self.button_detail_error.setFixedSize(20, 20) - self.button_detail_error.setToolTip('Detail error') + self.button_detail_error.setToolTip("Detail error") self.button_detail_error.hide() self.last_error_message = None @@ -87,7 +118,7 @@ def __init__(self): self.setLayout(layout) - def input_text_changed(self): + def input_text_changed(self) -> None: self.label_error.clear() self.button_detail_error.hide() @@ -110,13 +141,13 @@ def input_text_changed(self): self.last_detail_error_message = str(tb) self.button_detail_error.show() - self.label_error.setText('Error: ' + self.last_error_message) + self.label_error.setText("Error: " + self.last_error_message) - def show_detail_error_message(self): - message = self.last_error_message + '\n\n' + self.last_detail_error_message + def show_detail_error_message(self) -> None: + message = self.last_error_message + "\n\n" + self.last_detail_error_message mb = QErrorMessage() - mb.setWindowTitle('Error') + mb.setWindowTitle("Error") # Сообщение ошибки содержит отступы, символы-переходы на следующую строку, # которые поломаются при вставке через QErrorMessage.showMessage, и нет возможности # выбрать тип текста, то делаем такой хак. @@ -125,7 +156,7 @@ def show_detail_error_message(self): mb.exec_() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() @@ -133,9 +164,9 @@ def show_detail_error_message(self): mw.show() mw.text_edit_input.setPlainText( - 'Example' - '

    This is an example of a simple HTML page with one paragraph.

    ' - '' + "Example" + "

    This is an example of a simple HTML page with one paragraph.

    " + "" ) app.exec_() diff --git a/pretty_money_format.py b/pretty_money_format.py index 380068b12..da8159729 100644 --- a/pretty_money_format.py +++ b/pretty_money_format.py @@ -1,28 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import re def pretty_money_format(money) -> str: money = str(money) + money = re.sub(r"[^.,\d]", "", money) - import re - money = re.sub(r'[^.,\d]', '', money) - - if money.count('.') + money.count(',') > 1: - raise Exception('Invalid money format: "{}".'.format(money)) + if money.count(".") + money.count(",") > 1: + raise Exception(f'Invalid money format: "{money}".') - money_sep = '' - if money.count('.'): - money_sep = '.' - elif money.count(','): - money_sep = ',' + money_sep = "" + if money.count("."): + money_sep = "." + elif money.count(","): + money_sep = "," if money_sep: major, minor = money.split(money_sep) else: - major, minor = money, '' + major, minor = money, "" if len(major) > 3: new_major = list() @@ -36,29 +37,29 @@ def pretty_money_format(money) -> str: if i > 0 and i % 3 == 0: new_major.append(" ") - major = ''.join(reversed(new_major)) + major = "".join(reversed(new_major)) money = major + money_sep + minor return money.strip() -if __name__ == '__main__': - assert pretty_money_format(1000) == '1 000' - assert pretty_money_format(1000.) == '1 000.0' - assert pretty_money_format(1000000) == '1 000 000' - assert pretty_money_format(10000.56) == '10 000.56' - assert pretty_money_format(1000000.0) == '1 000 000.0' - assert pretty_money_format(100000) == '100 000' - - assert pretty_money_format('1000') == '1 000' - assert pretty_money_format('1000.') == '1 000.' - assert pretty_money_format('1000000') == '1 000 000' - assert pretty_money_format('10000.50') == '10 000.50' - assert pretty_money_format('1000000.0') == '1 000 000.0' - assert pretty_money_format('100000') == '100 000' - - assert pretty_money_format('10 00') == '1 000' - assert pretty_money_format('100 00 00') == '1 000 000' - assert pretty_money_format('1 0000.50$') == '10 000.50' - assert pretty_money_format('100 00 00.0') == '1 000 000.0' - assert pretty_money_format('100!000#') == '100 000' +if __name__ == "__main__": + assert pretty_money_format(1000) == "1 000" + assert pretty_money_format(1000.0) == "1 000.0" + assert pretty_money_format(1000000) == "1 000 000" + assert pretty_money_format(10000.56) == "10 000.56" + assert pretty_money_format(1000000.0) == "1 000 000.0" + assert pretty_money_format(100000) == "100 000" + + assert pretty_money_format("1000") == "1 000" + assert pretty_money_format("1000.") == "1 000." + assert pretty_money_format("1000000") == "1 000 000" + assert pretty_money_format("10000.50") == "10 000.50" + assert pretty_money_format("1000000.0") == "1 000 000.0" + assert pretty_money_format("100000") == "100 000" + + assert pretty_money_format("10 00") == "1 000" + assert pretty_money_format("100 00 00") == "1 000 000" + assert pretty_money_format("1 0000.50$") == "10 000.50" + assert pretty_money_format("100 00 00.0") == "1 000 000.0" + assert pretty_money_format("100!000#") == "100 000" diff --git a/pretty_xml/pretty_xml.py b/pretty_xml/pretty_xml.py index 21a00e7a2..76f734fe2 100644 --- a/pretty_xml/pretty_xml.py +++ b/pretty_xml/pretty_xml.py @@ -1,42 +1,67 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def process_xml_string(xml_string): +from xml.dom.minidom import parseString +from lxml import etree + + +def process_xml_string(xml_string: str) -> str: """ Функция из текста выдирает строку с xml -- она должна начинаться на < и заканчиваться > """ - start = xml_string.index('<') - end = xml_string.rindex('>') - return xml_string[start:end + 1] + start = xml_string.index("<") + end = xml_string.rindex(">") + return xml_string[start : end + 1] -def pretty_xml_minidom(xml_string, ind=2): +def pretty_xml_minidom(xml_string: str, indent: int = 4) -> str: """Функция принимает строку xml и выводит xml с отступами.""" - from xml.dom.minidom import parseString + xml_string: str = process_xml_string(xml_string) - xml_string = process_xml_string(xml_string) - xml_utf8 = parseString(xml_string).toprettyxml(indent=' ' * ind, encoding='utf-8') - return xml_utf8.decode('utf-8') + xml_utf8 = parseString(xml_string).toprettyxml( + indent=" " * indent, + encoding="utf-8", + ) + return xml_utf8.decode("utf-8") -def pretty_xml_lxml(xml_string): +def pretty_xml_lxml(xml_string: str) -> str: """Функция принимает строку xml и выводит xml с отступами.""" - from lxml import etree + xml_string: str = process_xml_string(xml_string) + + root = etree.fromstring( + text=xml_string, + parser=etree.XMLParser( + remove_blank_text=True, + # NOTE: Для обработки больших XML + huge_tree=True, + ), + ) - xml_string = process_xml_string(xml_string) - root = etree.fromstring(xml_string) - return etree.tostring(root, pretty_print=True, encoding='utf-8').decode('utf-8') + return etree.tostring( + root, + pretty_print=True, + encoding="utf-8", + ).decode("utf-8") -if __name__ == '__main__': - xml = '' +if __name__ == "__main__": + xml = """ + Простой хлеб + МукаДрожжиТёплая вода + + """ + + # TODO: Плохо работает print(pretty_xml_minidom(xml)) + print(pretty_xml_lxml(xml)) diff --git a/pretty_xml/pretty_xml_gui.py b/pretty_xml/pretty_xml_gui.py index 864b6817e..9ca28ee36 100644 --- a/pretty_xml/pretty_xml_gui.py +++ b/pretty_xml/pretty_xml_gui.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from os.path import split as path_split +import sys import traceback +from pathlib import Path + # TODO: избавиться от `import *` try: @@ -23,31 +25,28 @@ from PySide.QtGui import * from PySide.QtCore import Qt +from pretty_xml import pretty_xml_lxml as to_pretty_xml -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) + +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions -from pretty_xml import pretty_xml_minidom as to_pretty_xml - - class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle(path_split(__file__)[1]) + self.setWindowTitle(Path(__file__).name) - layout = QVBoxLayout() + layout = QVBoxLayout(self) self.text_edit_input = QPlainTextEdit() self.text_edit_output = QPlainTextEdit() @@ -57,13 +56,13 @@ def __init__(self): self.label_error.setTextInteractionFlags(Qt.TextSelectableByMouse) self.label_error.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.button_detail_error = QPushButton('...') + self.button_detail_error = QPushButton("...") self.button_detail_error.setFixedSize(20, 20) - self.button_detail_error.setToolTip('Detail error') + self.button_detail_error.setToolTip("Detail error") self.button_detail_error.hide() - self.last_error_message = None - self.last_detail_error_message = None + self.last_error_message = "" + self.last_detail_error_message = "" self.button_detail_error.clicked.connect(self.show_detail_error_message) self.text_edit_input.textChanged.connect(self.input_text_changed) @@ -81,14 +80,12 @@ def __init__(self): layout.addLayout(layout_error) - self.setLayout(layout) - - def input_text_changed(self): + def input_text_changed(self) -> None: self.label_error.clear() self.button_detail_error.hide() - self.last_error_message = None - self.last_detail_error_message = None + self.last_error_message = "" + self.last_detail_error_message = "" try: text = self.text_edit_input.toPlainText() @@ -106,13 +103,13 @@ def input_text_changed(self): self.last_detail_error_message = str(tb) self.button_detail_error.show() - self.label_error.setText('Error: ' + self.last_error_message) + self.label_error.setText("Error: " + self.last_error_message) - def show_detail_error_message(self): - message = self.last_error_message + '\n\n' + self.last_detail_error_message + def show_detail_error_message(self) -> None: + message = self.last_error_message + "\n\n" + self.last_detail_error_message mb = QErrorMessage() - mb.setWindowTitle('Error') + mb.setWindowTitle("Error") # Сообщение ошибки содержит отступы, символы-переходы на следующую строку, # которые поломаются при вставке через QErrorMessage.showMessage, и нет возможности # выбрать тип текста, то делаем такой хак. @@ -121,11 +118,20 @@ def show_detail_error_message(self): mb.exec_() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() mw.resize(650, 500) mw.show() + mw.text_edit_input.setPlainText( + """ + Простой хлеб + МукаДрожжиТёплая вода + + """ + ) + app.exec_() diff --git a/prevent_multiple_instances__mutex.py b/prevent_multiple_instances__mutex.py index 33c1c6fa5..c8bb31791 100644 --- a/prevent_multiple_instances__mutex.py +++ b/prevent_multiple_instances__mutex.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -15,7 +15,7 @@ # Prevent multiple instances mutex = win32event.CreateMutex(None, 1, "MY_MUTEX_PYTHON") if win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS: - print('Already running!') + print("Already running!") mutex = None sys.exit(0) diff --git a/prime_numbers.py b/prime_numbers.py index 45789a853..c7b162426 100644 --- a/prime_numbers.py +++ b/prime_numbers.py @@ -1,25 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/3035188/5909792 -def get_prime_numbers(n: int) -> list: - """ Returns a list of primes < n """ +def get_prime_numbers(n: int) -> list[int]: + """Returns a list of primes < n""" + sieve = [True] * n - for i in range(3, int(n**0.5)+1, 2): + for i in range(3, int(n**0.5) + 1, 2): if sieve[i]: - sieve[i*i::2*i] = [False] * ((n-i*i-1)//(2*i)+1) + sieve[i * i : : 2 * i] = [False] * ((n - i * i - 1) // (2 * i) + 1) return [2] + [i for i in range(3, n, 2) if sieve[i]] -if __name__ == '__main__': +if __name__ == "__main__": assert get_prime_numbers(10) == [2, 3, 5, 7] - assert get_prime_numbers(100) == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, - 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97] + assert get_prime_numbers(100) == [ + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, + 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97 + ] print(sum(get_prime_numbers(2_000_000))) assert sum(get_prime_numbers(2_000_000)) == 142913828922 diff --git a/print_System_Info.py b/print_System_Info.py index fd7eb83ed..2ddd03ac5 100644 --- a/print_System_Info.py +++ b/print_System_Info.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/Pure-L0G1C/FleX/blob/da8f30f9204a65df57063ed74b3e79a2a79a7bfc/payload/modules/sysinfo.py @@ -12,11 +12,11 @@ system_info = { - 'System': system(), - 'Release': release(), - 'Version': version(), - 'Machine': machine(), - 'Username': getuser(), - 'Architecture': architecture()[0] + "System": system(), + "Release": release(), + "Version": version(), + "Machine": machine(), + "Username": getuser(), + "Architecture": architecture()[0], } print(system_info) diff --git a/print__hprof_or_big_size_file.py b/print__hprof_or_big_size_file.py index 001869340..13ac6acd4 100644 --- a/print__hprof_or_big_size_file.py +++ b/print__hprof_or_big_size_file.py @@ -1,26 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +from multiprocessing.dummy import Pool as ThreadPool from os import walk from os.path import join, getsize from timeit import default_timer -from multiprocessing.dummy import Pool as ThreadPool from human_byte_size import sizeof_fmt -DIRS = [r'C:\DEV__TX', r'C:\DEV__OPTT', r'C:\DEV__RADIX'] +DIRS = [r"C:\DEV__TX", r"C:\DEV__OPTT", r"C:\DEV__RADIX"] def is_hprof_and_more_1GB(file_name: str) -> bool: - return '.hprof' in file_name and getsize(file_name) >= 1_000_000_000 # 1 GB + return ".hprof" in file_name and getsize(file_name) >= 1_000_000_000 # 1 GB def find_files_by_dir(root_dir: str, match_func) -> list: - print(f'Start check: {root_dir!r}') + print(f"Start check: {root_dir!r}") items = [] t = default_timer() @@ -34,7 +34,7 @@ def find_files_by_dir(root_dir: str, match_func) -> list: print(text) items.append(text) - print(f'Finish check: {root_dir!r}. Elapsed: {default_timer() - t:.3f} secs') + print(f"Finish check: {root_dir!r}. Elapsed: {default_timer() - t:.3f} secs") return items @@ -43,15 +43,17 @@ def find_files_by_dirs(dirs: list, match_func=is_hprof_and_more_1GB) -> list: items = [] with ThreadPool() as pool: - for result in pool.map(lambda root_dir: find_files_by_dir(root_dir, match_func), dirs): + for result in pool.map( + lambda root_dir: find_files_by_dir(root_dir, match_func), dirs + ): items += result return items -if __name__ == '__main__': +if __name__ == "__main__": items = find_files_by_dirs(DIRS) print() - print(f'Result ({len(items)}):') - print('\n'.join(items)) + print(f"Result ({len(items)}):") + print("\n".join(items)) diff --git a/print__hprof_or_big_size_file__notify_with_MessageBox.py b/print__hprof_or_big_size_file__notify_with_MessageBox.py index cfa257acd..a07b65b56 100644 --- a/print__hprof_or_big_size_file__notify_with_MessageBox.py +++ b/print__hprof_or_big_size_file__notify_with_MessageBox.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import re import time + from pathlib import Path -import re from PyQt5.QtWidgets import QApplication, QMessageBox from PyQt5.QtCore import Qt @@ -14,7 +15,7 @@ from print__hprof_or_big_size_file import find_files_by_dirs, DIRS -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) while True: @@ -22,11 +23,13 @@ if not result: continue - text = f'Files .hprof ({len(result)}):\n' + '\n'.join(result) + text = f"Files .hprof ({len(result)}):\n" + "\n".join(result) - msg_box = QMessageBox(QMessageBox.Information, 'Found .hprof!', text) + msg_box = QMessageBox(QMessageBox.Information, "Found .hprof!", text) msg_box.setTextInteractionFlags(Qt.TextSelectableByMouse) - remove_all_files_button = msg_box.addButton("Remove all files", QMessageBox.DestructiveRole) + remove_all_files_button = msg_box.addButton( + "Remove all files", QMessageBox.DestructiveRole + ) msg_box.addButton(QMessageBox.Ok) msg_box.exec() @@ -40,6 +43,8 @@ try: Path(file_name).unlink() except Exception as e: - QMessageBox.warning(None, "Warning", f"Error while removed {file_name!r}: {e}") + QMessageBox.warning( + None, "Warning", f"Error while removed {file_name!r}: {e}" + ) time.sleep(5 * 60 * 60) diff --git a/print__win__netstat/main.py b/print__win__netstat/main.py index 197ca5f78..89f572b5a 100644 --- a/print__win__netstat/main.py +++ b/print__win__netstat/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: Чтобы узнать кто какие занял/прослушивает порты @@ -11,7 +11,7 @@ # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/c24fb90f1c6b792ebd77e2f916ef5ec7d741c494/ascii_table__simple_pretty__rjust.py -def print_pretty_table(data, cell_sep=' | '): +def print_pretty_table(data, cell_sep=" | "): rows = len(data) cols = len(data[0]) @@ -32,27 +32,27 @@ def print_pretty_table(data, cell_sep=' | '): # Append header separate if not header_ok: - print(' + '.join('-' * width for width in col_width)) + print(" + ".join("-" * width for width in col_width)) header_ok = True def get_raw_data() -> str: - cmd = 'netstat -ano -p tcp' + cmd = "netstat -ano -p tcp" return check_output(cmd, universal_newlines=True).strip() -def get_data() -> (list, list): +def get_data() -> tuple[list, list]: text = get_raw_data() - lines = text.split('\n')[3:] + lines = text.split("\n")[3:] - headers = ['Proto', 'Local Address', 'Foreign Address', 'State', 'PID'] + headers = ["Proto", "Local Address", "Foreign Address", "State", "PID"] return headers, [line.split() for line in lines] -if __name__ == '__main__': +if __name__ == "__main__": print(get_raw_data()) - print('\n\n') + print("\n\n") headers, rows = get_data() diff --git a/print__win__wmi/print__baseboard__info.py b/print__win__wmi/print__baseboard__info.py index 29309a6c5..3788cb4ca 100644 --- a/print__win__wmi/print__baseboard__info.py +++ b/print__win__wmi/print__baseboard__info.py @@ -1,29 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from subprocess import check_output import xml.etree.ElementTree as ET +from subprocess import check_output def get_baseboard_info() -> dict: # Motherboard model - cmd = 'wmic baseboard get product, Manufacturer, version, serialnumber /format:RAWXML' + cmd = "wmic baseboard get product, Manufacturer, version, serialnumber /format:RAWXML" text = check_output(cmd) root = ET.fromstring(text) result = dict() - for x in root.iter('PROPERTY'): - key = x.attrib['NAME'] - result[key] = x.find('VALUE').text + for x in root.iter("PROPERTY"): + key = x.attrib["NAME"] + result[key] = x.find("VALUE").text return result -if __name__ == '__main__': +if __name__ == "__main__": info = get_baseboard_info() print(info) - print(info['Product']) + print(info["Product"]) diff --git "a/print__\320\223 \320\225 \320\235 \320\230 \320\220 \320\233 \320\254 \320\235 \320\236 \320\225 \321\204\320\276\321\202\320\276.py" "b/print__\320\223 \320\225 \320\235 \320\230 \320\220 \320\233 \320\254 \320\235 \320\236 \320\225 \321\204\320\276\321\202\320\276.py" index b5f03b99a..6f6965d78 100644 --- "a/print__\320\223 \320\225 \320\235 \320\230 \320\220 \320\233 \320\254 \320\235 \320\236 \320\225 \321\204\320\276\321\202\320\276.py" +++ "b/print__\320\223 \320\225 \320\235 \320\230 \320\220 \320\233 \320\254 \320\235 \320\236 \320\225 \321\204\320\276\321\202\320\276.py" @@ -1,19 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -text = 'Г Е Н И А Л Ь Н О Е фото ' +text = "Г Е Н И А Л Ь Н О Е фото " length = len(text) for start in range(length + 1): - new_text = '' + new_text = "" for i in range(length): j = (start + i) % length new_text += text[j] - if new_text[0] != ' ': + if new_text[0] != " ": print(new_text) # Г Е Н И А Л Ь Н О Е фото diff --git a/print_ascii_art.py b/print_ascii_art.py index 9bd0e15dd..c5f0ca33c 100644 --- a/print_ascii_art.py +++ b/print_ascii_art.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -art = '''\ +art = """\ ..sSs$$$$$$b. .$$$$$$$$$$$$$$$. .$$$$$$$$$$$$$$$$$$$$$b. @@ -68,6 +68,6 @@ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ dp $ -''' +""" print(art) diff --git a/print_attr_from_object.py b/print_attr_from_object.py index 7815dfae0..fae7fd4ba 100644 --- a/print_attr_from_object.py +++ b/print_attr_from_object.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" obj = 1 -print(list(filter(lambda x: not x.startswith('_'), dir(obj)))) -for i in list(filter(lambda x: not x.startswith('_'), dir(obj))): - print(i, ':', getattr(obj, i)) +print(list(filter(lambda x: not x.startswith("_"), dir(obj)))) +for i in list(filter(lambda x: not x.startswith("_"), dir(obj))): + print(i, ":", getattr(obj, i)) print() diff --git a/print_class_name.py b/print_class_name.py index 9400ec27b..945a046c6 100644 --- a/print_class_name.py +++ b/print_class_name.py @@ -1,19 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" class A: pass + a = A() print(A.__name__) # A print(a.__class__.__name__) # A class B: - def __init__(self): + def __init__(self) -> None: print(self.__class__.__name__) # B + B() diff --git a/print_connections__radixware_explorer.py b/print_connections__radixware_explorer.py deleted file mode 100644 index e5547bc68..000000000 --- a/print_connections__radixware_explorer.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -FILE_NAME = r'%APPDATA%\radixware.org\explorer\connections.xml' - -import os -ABS_FILE_NAME = os.path.expandvars(FILE_NAME) - - -with open(ABS_FILE_NAME, 'rb') as f: - from bs4 import BeautifulSoup - root = BeautifulSoup(f.read(), 'html.parser') - - -def get_tag_text(tag): - return '' if tag is None else tag.text - - -headers = ['NAME', 'ID', 'COMMENT', 'USERNAME', 'STATIONNAME', 'INITIALADDRESS', 'LANGUAGE', - 'COUNTRY', 'EXPLORERROOTID', 'TRACELEVEL'] -rows = [] - -for connection in root.select('connection'): - rows.append([ - connection['name'], - connection['id'], - get_tag_text(connection.comment), - get_tag_text(connection.username), - get_tag_text(connection.stationname), - get_tag_text(connection.initialaddress), - get_tag_text(connection.language), - get_tag_text(connection.country), - get_tag_text(connection.explorerrootid), - get_tag_text(connection.tracelevel), - ]) - - -# pip install tabulate -from tabulate import tabulate -print(tabulate(rows, headers=headers, tablefmt="grid")) diff --git a/print_exif/main.py b/print_exif/main.py index 9beb97b26..b45e4113a 100644 --- a/print_exif/main.py +++ b/print_exif/main.py @@ -1,25 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import base64 + +# pip install exifread +import exifread def get_exif_tags(file_object_or_file_name, as_category=True): if type(file_object_or_file_name) == str: # Open image file for reading (binary mode) - file_object_or_file_name = open(file_object_or_file_name, mode='rb') + file_object_or_file_name = open(file_object_or_file_name, mode="rb") # Return Exif tags - # pip install exifread - import exifread tags = exifread.process_file(file_object_or_file_name) tags_by_value = dict() if not tags: - print('Not tags') + print("Not tags") return tags_by_value - print('Tags ({}):'.format(len(tags))) + print(f"Tags ({len(tags)}):") for tag, value in tags.items(): # Process value @@ -28,9 +32,9 @@ def get_exif_tags(file_object_or_file_name, as_category=True): try: # If last 2 items equals [0, 0] if value.values[-2:] == [0, 0]: - value = bytes(value.values[:-2]).decode('utf-16') + value = bytes(value.values[:-2]).decode("utf-16") else: - value = bytes(value.values).decode('utf-16') + value = bytes(value.values).decode("utf-16") except: value = str(value.values) @@ -42,18 +46,17 @@ def get_exif_tags(file_object_or_file_name, as_category=True): except: # Example tag JPEGThumbnail if type(value) == bytes: - import base64 value = base64.b64encode(value).decode() - print(' "{}": {}'.format(tag, value)) + print(f' "{tag}": {value}') if not as_category: tags_by_value[tag] = value else: # Fill categories_by_tag - if ' ' in tag: - category, sub_tag = tag.split(' ', maxsplit=1) + if " " in tag: + category, sub_tag = tag.split(" ", maxsplit=1) if category not in tags_by_value: tags_by_value[category] = dict() @@ -68,10 +71,11 @@ def get_exif_tags(file_object_or_file_name, as_category=True): return tags_by_value -if __name__ == '__main__': +if __name__ == "__main__": import json - print('TAGS_BY_VALUE:') - f = open('exif_this.jpg', mode='rb') + + print("TAGS_BY_VALUE:") + f = open("exif_this.jpg", mode="rb") tags_by_value = get_exif_tags(f, as_category=False) print(json.dumps(tags_by_value, sort_keys=True, ensure_ascii=False, indent=4)) # @@ -91,9 +95,9 @@ def get_exif_tags(file_object_or_file_name, as_category=True): # "Image XPTitle": "print_exif" # } - print('\n\n') - print('CATEGORIES_BY_TAG:') - f = open('exif_this.jpg', mode='rb') + print("\n\n") + print("CATEGORIES_BY_TAG:") + f = open("exif_this.jpg", mode="rb") categories_by_tag = get_exif_tags(f) print(json.dumps(categories_by_tag, sort_keys=True, ensure_ascii=False, indent=4)) # diff --git a/print_list_subdirectories_size.py b/print_list_subdirectories_size.py index b95ec9591..f513ccac2 100644 --- a/print_list_subdirectories_size.py +++ b/print_list_subdirectories_size.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os @@ -10,14 +10,14 @@ from human_byte_size import sizeof_fmt -# Словарь нужен чтобы помнить размер папки. Когда итератор дойдет до родительской папки +# Словарь нужен, чтобы помнить размер папки. Когда итератор дойдет до родительской папки # в словаре уже будут размер вложенных папок dir_sizes = dict() -for root, dirs, files in os.walk('.', topdown=False): +for root, dirs, files in os.walk(".", topdown=False): size = sum(getsize(join(root, f)) for f in files) size += sum(dir_sizes[join(root, d)] for d in dirs) dir_sizes[root] = size for path, total_size in sorted(dir_sizes.items(), key=lambda x: x[0]): - print('{} : {} ({})'.format(path, sizeof_fmt(total_size), total_size)) + print(f"{path} : {sizeof_fmt(total_size)} ({total_size})") diff --git a/print_paths_from_LNKs_of_directory_by_specific_Disc.py b/print_paths_from_LNKs_of_directory_by_specific_Disc.py index fce17a400..7dd065fdf 100644 --- a/print_paths_from_LNKs_of_directory_by_specific_Disc.py +++ b/print_paths_from_LNKs_of_directory_by_specific_Disc.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from glob import iglob import os +from glob import iglob # pip install winshell import winshell -path_desktop_lnk = os.path.expanduser(r'~\Desktop\Пройти\*.lnk') +path_desktop_lnk = os.path.expanduser(r"~\Desktop\Пройти\*.lnk") need_disc = "E:" paths = [] diff --git a/print_sizes_of_directory_LNKs_desktop.py b/print_sizes_of_directory_LNKs_desktop.py index 57ebfa597..e0f98f865 100644 --- a/print_sizes_of_directory_LNKs_desktop.py +++ b/print_sizes_of_directory_LNKs_desktop.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import os + from collections import defaultdict from glob import iglob -import os # pip install winshell import winshell @@ -15,10 +16,10 @@ # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/b6ac435ee171e48ed35044e8e61e199de641a6e7/get_dir_total_size__using_glob.py -def get_dir_total_size(dir_name: str) -> (int, str): +def get_dir_total_size(dir_name: str) -> tuple[int, str]: total_size = 0 - for file_name in iglob(dir_name + '/**/*', recursive=True): + for file_name in iglob(dir_name + "/**/*", recursive=True): try: if os.path.isfile(file_name): total_size += os.path.getsize(file_name) @@ -31,7 +32,7 @@ def get_dir_total_size(dir_name: str) -> (int, str): disc_by_number = defaultdict(int) -path_desktop_lnk = os.path.expanduser(r'~\Desktop\Пройдено\*.lnk') +path_desktop_lnk = os.path.expanduser(r"~\Desktop\Пройдено\*.lnk") paths = [] @@ -39,7 +40,7 @@ def get_dir_total_size(dir_name: str) -> (int, str): shortcut = winshell.shortcut(file_name) path = shortcut.path - if path.endswith('.exe') and os.path.isfile(path): + if path.endswith(".exe") and os.path.isfile(path): path = os.path.dirname(path) paths.append(path) @@ -53,7 +54,7 @@ def get_dir_total_size(dir_name: str) -> (int, str): for file_name in paths: size, size_str = get_dir_total_size(file_name) - print(f'{size:<15} {size_str:10} {file_name}') + print(f"{size:<15} {size_str:10} {file_name}") total_size += size disc = file_name[0] @@ -63,22 +64,25 @@ def get_dir_total_size(dir_name: str) -> (int, str): disc_by_total_items[disc].append((size, size_str, file_name)) print() -print(f'Total size: {total_size} {sizeof_fmt(total_size)}') +print(f"Total size: {total_size} {sizeof_fmt(total_size)}") for disc in sorted(total_size_by_disc): size = total_size_by_disc[disc] - print(f' {disc} {size:<15} {sizeof_fmt(size)}') + print(f" {disc} {size:<15} {sizeof_fmt(size)}") print() -print('Top all:') -for size, size_str, file_name in sorted(total_items, key=lambda x: x[0], reverse=True)[:5]: - print(f' {size:<15} bytes {size_str:10} {file_name}') +print("Top all:") +items = sorted(total_items, key=lambda x: x[0], reverse=True) +for size, size_str, file_name in items[:5]: + print(f" {size:<15} bytes {size_str:10} {file_name}") print() for disc, total_items in disc_by_total_items.items(): - print(f'Top of {disc}:') - for size, size_str, file_name in sorted(total_items, key=lambda x: x[0], reverse=True)[:3]: - print(f' {size:<15} bytes {size_str:10} {file_name}') + print(f"Top of {disc}:") + for size, size_str, file_name in sorted( + total_items, key=lambda x: x[0], reverse=True + )[:3]: + print(f" {size:<15} bytes {size_str:10} {file_name}") print() diff --git a/print_sql_db_tables__using_sqlalchemy.py b/print_sql_db_tables__using_sqlalchemy.py index 0e44c469c..31a1e9930 100644 --- a/print_sql_db_tables__using_sqlalchemy.py +++ b/print_sql_db_tables__using_sqlalchemy.py @@ -1,29 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': - import os - DIR = os.path.dirname(__file__) - DB_FILE_NAME = 'sqlite:///' + os.path.join(DIR, 'database') +import os - # Создаем базу, включаем логирование и автообновление подключения каждые 2 часа (7200 секунд) - from sqlalchemy import create_engine - engine = create_engine( - DB_FILE_NAME, - # echo=True, - pool_recycle=7200 - ) +from sqlalchemy import create_engine +from sqlalchemy.engine import reflection - from sqlalchemy.engine import reflection - inspector = reflection.Inspector.from_engine(engine) - table_names = inspector.get_table_names() - for table in table_names: - columns = [column_info['name'] for column_info in inspector.get_columns(table)] - print('{}: {}'.format(table, ', '.join(columns))) +DIR = os.path.dirname(__file__) +DB_FILE_NAME = "sqlite:///" + os.path.join(DIR, "database") - for row in engine.execute("select * from {}".format(table)): - print(row.values()) +# Создаем базу, включаем логирование и автообновление подключения каждые 2 часа (7200 секунд) +engine = create_engine( + DB_FILE_NAME, + # echo=True, + pool_recycle=7200, +) + +inspector = reflection.Inspector.from_engine(engine) + +table_names = inspector.get_table_names() +for table in table_names: + columns = [column_info["name"] for column_info in inspector.get_columns(table)] + print(f"{table}: {', '.join(columns)}") + + for row in engine.execute(f"select * from {table}"): + print(row.values()) diff --git a/profile__example.py b/profile__example.py index d932e9203..22b38634e 100644 --- a/profile__example.py +++ b/profile__example.py @@ -1,31 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# LOOK: 1000 0.016 0.000 0.016 0.000 profile__example.py:14(_has) +import profile -def foo(number=100): - def _inc_i(): +def foo(number=100) -> None: + def _inc_i() -> None: nonlocal i i += 1 - def _has(value, items): + def _has(value, items) -> bool: return value in items - items = ['1234567890' for _ in range(number)] + items = ["1234567890" for _ in range(number)] print(items) i = 0 for i in range(number): _inc_i() - _has('5', items) + _has("5", items) print(i) -import profile -profile.run(r'foo(1000)') +profile.run(r"foo(1000)") +# LOOK: 1000 0.016 0.000 0.016 0.000 profile__example.py:14(_has) diff --git a/programmer_day.py b/programmer_day.py index 9601fe536..6a6785d1f 100644 --- a/programmer_day.py +++ b/programmer_day.py @@ -1,20 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from datetime import datetime, timedelta -day_of_year = int(datetime.today().strftime('%j')) -print('day_of_year:', day_of_year) +from datetime import date, timedelta -last_programmer_day = 256 - day_of_year -print('last_programmer_day:', last_programmer_day) -if last_programmer_day >= 0: - programmer_day = (datetime.today() + timedelta(days=last_programmer_day)) - print('\nday of programmer_day:', programmer_day.strftime('%j')) - print('programmer_day:', programmer_day) +day_of_year: int = int(date.today().strftime("%j")) +print(f"Current day of year: {day_of_year}") + +last_programmer_day: int = 256 - day_of_year +print(f"Until Programmer's day: {last_programmer_day}") +if last_programmer_day >= 0: + programmer_day = date.today() + timedelta(days=last_programmer_day) + print() + print(f"Day of programmer day: {programmer_day:%j}") + print(f"Programmer day: {programmer_day}") else: - print('Has already passed') + print("Has already passed") diff --git a/proxy.py__examples/ca_certificate.py b/proxy.py__examples/ca_certificate.py index 4578c9d69..878b916fb 100644 --- a/proxy.py__examples/ca_certificate.py +++ b/proxy.py__examples/ca_certificate.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # NOTE: Analog https://github.com/abhinavsingh/proxy.py/blob/754d5a35d28347716dfaefea91418826148ed903/Makefile#L60 @@ -11,22 +11,30 @@ from config import CA_KEY_FILE_PATH, CA_CERT_FILE_PATH, CA_SIGNING_KEY_FILE_PATH -def main(): +def main() -> None: # Generate CA key - os.system(f'python -m proxy.common.pki gen_private_key --private-key-path {CA_KEY_FILE_PATH}') - os.system(f'python -m proxy.common.pki remove_passphrase --private-key-path {CA_KEY_FILE_PATH}') + os.system( + f"python -m proxy.common.pki gen_private_key --private-key-path {CA_KEY_FILE_PATH}" + ) + os.system( + f"python -m proxy.common.pki remove_passphrase --private-key-path {CA_KEY_FILE_PATH}" + ) # Generate CA certificate os.system( - f'python -m proxy.common.pki gen_public_key --private-key-path {CA_KEY_FILE_PATH} ' - f'--public-key-path {CA_CERT_FILE_PATH}' + f"python -m proxy.common.pki gen_public_key --private-key-path {CA_KEY_FILE_PATH} " + f"--public-key-path {CA_CERT_FILE_PATH}" ) # Generate key that will be used to generate domain certificates on the fly # Generated certificates are then signed with CA certificate / key generated above - os.system(f'python -m proxy.common.pki gen_private_key --private-key-path {CA_SIGNING_KEY_FILE_PATH}') - os.system(f'python -m proxy.common.pki remove_passphrase --private-key-path {CA_SIGNING_KEY_FILE_PATH}') + os.system( + f"python -m proxy.common.pki gen_private_key --private-key-path {CA_SIGNING_KEY_FILE_PATH}" + ) + os.system( + f"python -m proxy.common.pki remove_passphrase --private-key-path {CA_SIGNING_KEY_FILE_PATH}" + ) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/proxy.py__examples/config.py b/proxy.py__examples/config.py index a3cd80774..682255cb5 100644 --- a/proxy.py__examples/config.py +++ b/proxy.py__examples/config.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # TODO: support pathlib -CA_KEY_FILE_PATH = 'ca-key.pem' -CA_CERT_FILE_PATH = 'ca-cert.pem' -CA_SIGNING_KEY_FILE_PATH = 'ca-signing-key.pem' +CA_KEY_FILE_PATH = "ca-key.pem" +CA_CERT_FILE_PATH = "ca-cert.pem" +CA_SIGNING_KEY_FILE_PATH = "ca-signing-key.pem" diff --git a/proxy.py__examples/hello_world.py b/proxy.py__examples/hello_world.py index 2bb3d8570..0a1ecc404 100644 --- a/proxy.py__examples/hello_world.py +++ b/proxy.py__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/abhinavsingh/proxy.py @@ -9,8 +9,6 @@ import ipaddress -from typing import Optional - # pip install --upgrade proxy.py import proxy from proxy.common.utils import bytes_ @@ -21,13 +19,13 @@ # NOTE: Analog https://github.com/gil9red/SimplePyScripts/blob/3d8afdec07c2cfbc94fcb3fd792721e1e8565a42/http.server__examples/simple_http_proxy_server/main.py#L18 class AddHeadersPlugin(HttpProxyBasePlugin): - def before_upstream_connection(self, request: HttpParser) -> Optional[HttpParser]: + def before_upstream_connection(self, request: HttpParser) -> HttpParser | None: return request - def handle_client_request(self, request: HttpParser) -> Optional[HttpParser]: + def handle_client_request(self, request: HttpParser) -> HttpParser | None: # NOTE: Add custom header - request.add_header(b'x-my-proxy', b'hell yeah!') - request.add_header(b'x-my-client-ip', bytes_(self.client.addr[0])) + request.add_header(b"x-my-proxy", b"hell yeah!") + request.add_header(b"x-my-client-ip", bytes_(self.client.addr[0])) return request @@ -38,19 +36,19 @@ def on_upstream_connection_close(self) -> None: pass -def main(*args, **kwargs): +def main(*args, **kwargs) -> None: proxy.main( *args, - hostname=ipaddress.ip_address('127.0.0.1'), + hostname=ipaddress.ip_address("127.0.0.1"), port=33333, plugins=[ - 'proxy.plugin.CacheResponsesPlugin', # Adding plugin v1 - FilterByUpstreamHostPlugin, # Adding plugin v2 - AddHeadersPlugin, # Adding custom plugin + "proxy.plugin.CacheResponsesPlugin", # Adding plugin v1 + FilterByUpstreamHostPlugin, # Adding plugin v2 + AddHeadersPlugin, # Adding custom plugin ], **kwargs ) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/proxy.py__examples/hello_world__with_TLS_HTTPS_interception.py b/proxy.py__examples/hello_world__with_TLS_HTTPS_interception.py index 378086684..65c26694f 100644 --- a/proxy.py__examples/hello_world__with_TLS_HTTPS_interception.py +++ b/proxy.py__examples/hello_world__with_TLS_HTTPS_interception.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/abhinavsingh/proxy.py#tls-interception @@ -22,7 +22,7 @@ ca_certificate.main() -if __name__ == '__main__': +if __name__ == "__main__": main( ca_key_file=CA_KEY_FILE_PATH, ca_cert_file=CA_CERT_FILE_PATH, diff --git a/proxy.py__examples/test.py b/proxy.py__examples/test.py index 0fe92ef67..cb39e7c5f 100644 --- a/proxy.py__examples/test.py +++ b/proxy.py__examples/test.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os.path @@ -20,13 +20,13 @@ proxies = { - "http": 'localhost:33333', - "https": 'localhost:33333', + "http": "localhost:33333", + "https": "localhost:33333", } # NOTE: When error "ssl.SSLError: [X509: KEY_VALUES_MISMATCH] key values mismatch (_ssl.c:3845)" # try remove all files from "C:\Users\\.proxy\certificates" -for url in ['http://httpbin.org/headers', 'https://httpbin.org/headers']: +for url in ["http://httpbin.org/headers", "https://httpbin.org/headers"]: print(url) # NOTE: The "verify" parameter is needed for HTTPS interception @@ -38,9 +38,9 @@ DEBUG_LOG and print(rs.headers) DEBUG_LOG and print(rs.content) - for header, value in rs.json()['headers'].items(): - if header.lower().startswith('x-my'): - print(f'{header}: {value!r}') + for header, value in rs.json()["headers"].items(): + if header.lower().startswith("x-my"): + print(f"{header}: {value!r}") print() diff --git a/psutil_example/cpu_info.py b/psutil_example/cpu_info.py index ff4921e88..4c734f2a0 100644 --- a/psutil_example/cpu_info.py +++ b/psutil_example/cpu_info.py @@ -1,32 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil -print('CPU info:', psutil.cpu_times()) + +print("CPU info:", psutil.cpu_times()) print() for _ in range(3): info = psutil.cpu_percent(interval=1) - print('CPU percent (interval=1, percpu=False):', info) + print("CPU percent (interval=1, percpu=False):", info) print() for _ in range(3): info = psutil.cpu_percent(interval=1, percpu=True) - print('CPU percent (interval=1, percpu=True):', info) + print("CPU percent (interval=1, percpu=True):", info) print() for _ in range(3): info = psutil.cpu_times_percent(interval=1, percpu=False) - print('CPU times percent (interval=1, percpu=False):', info) + print("CPU times percent (interval=1, percpu=False):", info) print() -print('Logical CPUs:', psutil.cpu_count()) -print('Physical CPUs:', psutil.cpu_count(logical=False)) +print("Logical CPUs:", psutil.cpu_count()) +print("Physical CPUs:", psutil.cpu_count(logical=False)) print() -print('CPU stats:', psutil.cpu_stats()) -print('CPU freq:', psutil.cpu_freq()) +print("CPU stats:", psutil.cpu_stats()) +print("CPU freq:", psutil.cpu_freq()) diff --git a/psutil_example/disc_used_DIFF/common.py b/psutil_example/disc_used_DIFF/common.py index 05e254563..2be79686a 100644 --- a/psutil_example/disc_used_DIFF/common.py +++ b/psutil_example/disc_used_DIFF/common.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import json @@ -13,31 +13,31 @@ # Absolute file name -FILE_NAME_SNAPSHOT = str(pathlib.Path(__file__).resolve().parent / 'snapshot.json') +FILE_NAME_SNAPSHOT = str(pathlib.Path(__file__).resolve().parent / "snapshot.json") def sizeof_fmt(num: Union[int, float], with_sign=False) -> str: if with_sign: - sign = '+' if num >= 0 else '-' + sign = "+" if num >= 0 else "-" else: - sign = '' + sign = "" if num == 0: - sign = '' + sign = "" num = abs(num) - for x in ['bytes', 'KB', 'MB', 'GB']: + for x in ["bytes", "KB", "MB", "GB"]: if num < 1024.0: return "%s%3.1f %s" % (sign, num, x) num /= 1024.0 - return "%s%3.1f %s" % (sign, num, 'TB') + return "%s%3.1f %s" % (sign, num, "TB") def get_disc_list() -> List[str]: - return [disk.device for disk in psutil.disk_partitions() if 'fixed' in disk.opts] + return [disk.device for disk in psutil.disk_partitions() if "fixed" in disk.opts] def get_human_sizes(items: List[int], with_sign) -> List[str]: @@ -58,18 +58,18 @@ def get_disc_total_used(disc_list: List[str]) -> str: return sizeof_fmt(sum(psutil.disk_usage(disk).used for disk in disc_list)) -def write_snapshot_raw_disc_used(disc_list: List[str] = None): - with open(FILE_NAME_SNAPSHOT, 'w', encoding='utf-8') as f: +def write_snapshot_raw_disc_used(disc_list: List[str] = None) -> None: + with open(FILE_NAME_SNAPSHOT, "w", encoding="utf-8") as f: items = get_disc_used(disc_list, only_raw=True) - print('Write snapshot with:', items) + print("Write snapshot with:", items) json.dump(items, f) def load_snapshot_raw_disc_used() -> List[int]: - with open(FILE_NAME_SNAPSHOT, encoding='utf-8') as f: + with open(FILE_NAME_SNAPSHOT, encoding="utf-8") as f: items = json.load(f) - print('Read snapshot:', items) + print("Read snapshot:", items) return items diff --git a/psutil_example/disc_used_DIFF/create_disc_snapshot.py b/psutil_example/disc_used_DIFF/create_disc_snapshot.py index 4d662ae9c..c483e1a6b 100644 --- a/psutil_example/disc_used_DIFF/create_disc_snapshot.py +++ b/psutil_example/disc_used_DIFF/create_disc_snapshot.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import write_snapshot_raw_disc_used diff --git a/psutil_example/disc_used_DIFF/main.py b/psutil_example/disc_used_DIFF/main.py index eb7d460fe..9a54e6967 100644 --- a/psutil_example/disc_used_DIFF/main.py +++ b/psutil_example/disc_used_DIFF/main.py @@ -1,26 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install tabulate from tabulate import tabulate -from common import get_disc_list, get_disc_used, get_human_sizes, get_row_for, load_snapshot_raw_disc_used +from common import ( + get_disc_list, + get_disc_used, + get_human_sizes, + get_row_for, + load_snapshot_raw_disc_used, +) disc_list = get_disc_list() disc_list_raw = get_disc_used(disc_list, only_raw=True) disc_list_snapshot_raw = load_snapshot_raw_disc_used() -disc_list_diff_raw = [b-a for a, b in zip(disc_list_snapshot_raw, disc_list_raw)] +disc_list_diff_raw = [b - a for a, b in zip(disc_list_snapshot_raw, disc_list_raw)] -headers = [''] + disc_list + ['TOTAL USED'] +headers = [""] + disc_list + ["TOTAL USED"] rows = [ - get_row_for('', disc_list_snapshot_raw), - get_row_for('', disc_list_raw), - get_row_for('', disc_list_diff_raw, with_sign=True), + get_row_for("", disc_list_snapshot_raw), + get_row_for("", disc_list_raw), + get_row_for("", disc_list_diff_raw, with_sign=True), ] print() diff --git a/psutil_example/disk_info.py b/psutil_example/disk_info.py index ac529aa17..ca824ff52 100644 --- a/psutil_example/disk_info.py +++ b/psutil_example/disk_info.py @@ -1,53 +1,43 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import sys -from pathlib import Path +# pip install humanize +from humanize import naturalsize as sizeof_fmt # pip install psutil import psutil -sys.path.append(str(Path(__file__).resolve().parent.parent)) -from human_byte_size import sizeof_fmt +# pip install tabulate +from tabulate import tabulate -print('Disk partitions:') +print("Disk partitions:") for disk in psutil.disk_partitions(): - print(' {}'.format(disk)) + print(f" {disk}") print() -print('Disk usage:') -for disk in filter(lambda x: 'fixed' in x.opts, psutil.disk_partitions()): +print("Disk usage:") +for disk in filter(lambda x: "fixed" in x.opts, psutil.disk_partitions()): info = psutil.disk_usage(disk.device) - print(' {} {}'.format(disk.device, info)) - print(' {} free of {}'.format(sizeof_fmt(info.free), sizeof_fmt(info.total))) + print(f" {disk.device} {info}") + print(f" {sizeof_fmt(info.free)} free of {sizeof_fmt(info.total)}") print() print() -print('Disk io (input/output) total sum counters:') -print(' {}'.format(psutil.disk_io_counters())) +print("Disk io (input/output) total sum counters:") +print(f" {psutil.disk_io_counters()}") physical_drive_by_info = list(psutil.disk_io_counters(True).items()) print() -print('Physical drive io (input/output) counters ({}):'.format(len(physical_drive_by_info))) -# -# for drive, info in physical_drive_by_info: -# print(' {}: {}'.format(drive, info)) -# # -# # OR: -# # -headers = ('drive',) + physical_drive_by_info[0][1]._fields +print(f"Physical drive io (input/output) counters ({len(physical_drive_by_info)}):") + +headers = ("drive",) + physical_drive_by_info[0][1]._fields headers = [header.upper() for header in headers] rows = [(drive,) + tuple(info) for drive, info in physical_drive_by_info] - -# pip install tabulate -from tabulate import tabulate print(tabulate(rows, headers=headers, tablefmt="grid")) -print() -print() diff --git a/psutil_example/get_battery_status.py b/psutil_example/get_battery_status.py index ea2fd6797..5116944d0 100644 --- a/psutil_example/get_battery_status.py +++ b/psutil_example/get_battery_status.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil + + battery = psutil.sensors_battery() print(battery.percent) diff --git a/psutil_example/get_win_services.py b/psutil_example/get_win_services.py index d27b7343a..26361ecb1 100644 --- a/psutil_example/get_win_services.py +++ b/psutil_example/get_win_services.py @@ -1,30 +1,30 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil from psutil._pswindows import WindowsService -from typing import List - -def get_win_services() -> List[WindowsService]: +def get_win_services() -> list[WindowsService]: return list(psutil.win_service_iter()) -if __name__ == '__main__': +if __name__ == "__main__": win_service_list = get_win_services() - print(f'Win service list ({len(win_service_list)}):') + print(f"Win service list ({len(win_service_list)}):") for service in win_service_list: - title = f'{service.name()!r} ({service.display_name()})' - path = f'Pid={service.pid()}, name={service.name()!r}, display_name={service.display_name()!r}, ' \ - f'status={service.status()!r}, start_type={service.start_type()!r}' - print('Title:', title) - print('Path:', path) - print('Status:', service.status()) - print('binpath:', service.binpath()) + title = f"{service.name()!r} ({service.display_name()})" + path = ( + f"Pid={service.pid()}, name={service.name()!r}, display_name={service.display_name()!r}, " + f"status={service.status()!r}, start_type={service.start_type()!r}" + ) + print("Title:", title) + print("Path:", path) + print("Status:", service.status()) + print("binpath:", service.binpath()) print() diff --git a/psutil_example/hello_world.py b/psutil_example/hello_world.py index 6a1f82e01..01a6e6232 100644 --- a/psutil_example/hello_world.py +++ b/psutil_example/hello_world.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil + + psutil.test() diff --git a/psutil_example/kill_process_by_name.py b/psutil_example/kill_process_by_name.py index e06ffad92..889c8f93a 100644 --- a/psutil_example/kill_process_by_name.py +++ b/psutil_example/kill_process_by_name.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil @@ -9,5 +9,5 @@ for process in psutil.process_iter(): - if process.name() == 'calc.exe': + if process.name() == "calc.exe": process.kill() diff --git a/psutil_example/loop_kill_process_by_name.py b/psutil_example/loop_kill_process_by_name.py index d5e5c27f4..d23f99ea2 100644 --- a/psutil_example/loop_kill_process_by_name.py +++ b/psutil_example/loop_kill_process_by_name.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import time @@ -12,7 +12,7 @@ while True: for process in psutil.process_iter(): - if process.name() == 'calc.exe': + if process.name() == "calc.exe": process.kill() # Wait 5 secs diff --git a/psutil_example/memory_info.py b/psutil_example/memory_info.py index f3579be09..ac1744368 100644 --- a/psutil_example/memory_info.py +++ b/psutil_example/memory_info.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil -print('System memory:', psutil.virtual_memory()) -print('System swap memory:', psutil.swap_memory()) + + +print("System memory:", psutil.virtual_memory()) +print("System swap memory:", psutil.swap_memory()) diff --git a/psutil_example/network_info/net_connections.py b/psutil_example/network_info/net_connections.py index 86e7535fc..bdc25cd6d 100644 --- a/psutil_example/network_info/net_connections.py +++ b/psutil_example/network_info/net_connections.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil +# pip install tabulate +from tabulate import tabulate + + net_connections = psutil.net_connections() -print('Net connections ({}):'.format(len(net_connections))) +print(f"Net connections ({len(net_connections)}):") # Sort by pid net_connections.sort(key=lambda x: x.pid) @@ -23,7 +27,4 @@ headers = [header.upper() for header in headers] rows = [tuple(info) for info in net_connections] - - # pip install tabulate - from tabulate import tabulate print(tabulate(rows, headers=headers, tablefmt="grid", showindex=True)) diff --git a/psutil_example/network_info/net_interface_by_address_list.py b/psutil_example/network_info/net_interface_by_address_list.py index bf797234a..356ae34d2 100644 --- a/psutil_example/network_info/net_interface_by_address_list.py +++ b/psutil_example/network_info/net_interface_by_address_list.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil +# pip install tabulate +from tabulate import tabulate + + net_interface_by_address_list = list(psutil.net_if_addrs().items()) -print('Net interface by address list ({}):'.format(len(net_interface_by_address_list))) +print(f"Net interface by address list ({len(net_interface_by_address_list)}):") # Sort by name interface net_interface_by_address_list.sort(key=lambda x: x[0]) @@ -23,15 +27,13 @@ # # OR: # # if net_interface_by_address_list: - headers = ('name',) + net_interface_by_address_list[0][1][0]._fields + headers = ("name",) + net_interface_by_address_list[0][1][0]._fields headers = [header.upper() for header in headers] rows = [] for name, address_list in net_interface_by_address_list: for i, address in enumerate(address_list): - rows.append((name if i == 0 else '',) + tuple(address)) + rows.append((name if i == 0 else "",) + tuple(address)) - # pip install tabulate - from tabulate import tabulate print(tabulate(rows, headers=headers, tablefmt="grid")) diff --git a/psutil_example/network_info/net_interface_list.py b/psutil_example/network_info/net_interface_list.py index 561c964ac..88f2f7b75 100644 --- a/psutil_example/network_info/net_interface_list.py +++ b/psutil_example/network_info/net_interface_list.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil +# pip install tabulate +from tabulate import tabulate + + net_interface_list = list(psutil.net_io_counters(pernic=True).items()) -print('Net interface list ({}):'.format(len(net_interface_list))) +print(f"Net interface list ({len(net_interface_list)}):") # Sort by name interface net_interface_list.sort(key=lambda x: x[0]) @@ -20,11 +24,8 @@ # # if net_interface_list: info_fields = net_interface_list[0][1]._fields - headers = ('name',) + info_fields + headers = ("name",) + info_fields headers = [header.upper() for header in headers] rows = [(interface,) + tuple(info) for interface, info in net_interface_list] - - # pip install tabulate - from tabulate import tabulate print(tabulate(rows, headers=headers, tablefmt="grid")) diff --git a/psutil_example/network_info/network_interface_card_by_info.py b/psutil_example/network_info/network_interface_card_by_info.py index de795cefd..c4b0b8c73 100644 --- a/psutil_example/network_info/network_interface_card_by_info.py +++ b/psutil_example/network_info/network_interface_card_by_info.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil +# pip install tabulate +from tabulate import tabulate + + network_interface_card_by_info = list(psutil.net_if_stats().items()) -print('Network interface card by info ({}):'.format(len(network_interface_card_by_info))) +print(f"Network interface card by info ({len(network_interface_card_by_info)}):") # Sort by name interface network_interface_card_by_info.sort(key=lambda x: x[0]) @@ -20,11 +24,8 @@ # # if network_interface_card_by_info: - headers = ('name',) + network_interface_card_by_info[0][1]._fields + headers = ("name",) + network_interface_card_by_info[0][1]._fields headers = [header.upper() for header in headers] rows = [(name,) + tuple(info) for name, info in network_interface_card_by_info] - - # pip install tabulate - from tabulate import tabulate print(tabulate(rows, headers=headers, tablefmt="grid")) diff --git a/psutil_example/on_new_process_handler.py b/psutil_example/on_new_process_handler.py index 5d6be6934..b2092c618 100644 --- a/psutil_example/on_new_process_handler.py +++ b/psutil_example/on_new_process_handler.py @@ -1,37 +1,37 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from threading import Thread from time import sleep -from typing import Callable, List +from typing import Callable # pip install psutil import psutil -ListenerFunc = Callable[[List], None] +ListenerFunc = Callable[[list], None] class OnNewProcessHandler(Thread): - def __init__(self, timeout_secs=1.0): + def __init__(self, timeout_secs=1.0) -> None: super().__init__() self._process = dict() - self._listeners: List[ListenerFunc] = [] + self._listeners: list[ListenerFunc] = [] self._timeout_secs = timeout_secs - def add_listener(self, listener: ListenerFunc): + def add_listener(self, listener: ListenerFunc) -> None: if listener not in self._listeners: self._listeners.append(listener) - def remove_listener(self, listener: ListenerFunc): + def remove_listener(self, listener: ListenerFunc) -> None: if listener in self._listeners: self._listeners.remove(listener) - def remove_all_listener(self): + def remove_all_listener(self) -> None: self._listeners.clear() @staticmethod @@ -40,15 +40,15 @@ def get_process() -> dict: for proc in psutil.process_iter(): try: - pinfo = proc.as_dict(attrs=['pid', 'name', 'exe']) - items[pinfo['pid']] = pinfo + pinfo = proc.as_dict(attrs=["pid", "name", "exe"]) + items[pinfo["pid"]] = pinfo except psutil.NoSuchProcess: continue return items - def run(self): + def run(self) -> None: while True: current_process = self.get_process() @@ -69,18 +69,18 @@ def run(self): sleep(self._timeout_secs) -if __name__ == '__main__': - def print_listener(process_list): +if __name__ == "__main__": + + def print_listener(process_list) -> None: result = [] for p in process_list: result.append(f"{p['name']} (pid={p['pid']})") - print('-' * 10 + '\n' + '\n'.join(result) + '\n' + '-' * 10) - + print("-" * 10 + "\n" + "\n".join(result) + "\n" + "-" * 10) handler = OnNewProcessHandler() handler.add_listener(print_listener) handler.start() sleep(2) - handler.add_listener(lambda x: print(f'New process: {len(x)}')) + handler.add_listener(lambda x: print(f"New process: {len(x)}")) diff --git a/psutil_example/other_system_info.py b/psutil_example/other_system_info.py index 6105dc634..0df75294a 100644 --- a/psutil_example/other_system_info.py +++ b/psutil_example/other_system_info.py @@ -1,16 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +from datetime import datetime + # pip install psutil import psutil -print('Users:', psutil.users()) + + +print("Users:", psutil.users()) print() boot_time = psutil.boot_time() -print('System boot time (timestamp):', boot_time) +print("System boot time (timestamp):", boot_time) -from datetime import datetime -print('System boot time:', datetime.fromtimestamp(boot_time)) +print("System boot time:", datetime.fromtimestamp(boot_time)) diff --git a/psutil_example/print_all_webservers/get_info_html.py b/psutil_example/print_all_webservers/get_info_html.py new file mode 100644 index 000000000..476373f37 --- /dev/null +++ b/psutil_example/print_all_webservers/get_info_html.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os +import tempfile + +from dataclasses import dataclass + +# pip install psutil==6.1.0 +import psutil + + +@dataclass +class Process: + pid: int + name: str + path: str + cwd: str + ports: str + + +TITLE = "Список запущенных серверов" + + +def _get_processes() -> list[Process]: + processes = [] + for proc in psutil.process_iter(): + try: + connections = [ + c + for c in proc.net_connections() + if c.status == psutil.CONN_LISTEN and c.laddr + ] + if not connections: + continue + + ports = sorted(set(c.laddr.port for c in connections)) + processes.append( + Process( + pid=proc.pid, + name=proc.name(), + path=" ".join(os.path.normpath(x) for x in proc.cmdline()), + cwd=proc.cwd(), + ports=", ".join(map(str, ports)), + ) + ) + + except psutil.AccessDenied: + pass + + processes.sort(key=lambda p: p.name) + + return processes + + +def get_info_html() -> str: + template = """ + + + + + {{ title }} + + + + + + + + + + + + + + + + + + {{ table_rows }} + +
    {{ title }}
    #PIDНазваниеПорт(ы)ПутьCwd
    + + + """ + + table_rows: list[str] = [ + f""" + + {i} + {p.pid} + {p.name} +
    {p.ports}
    +
    {p.path}
    +
    {p.cwd}
    + + """ + for i, p in enumerate(_get_processes(), 1) + ] + + return template.replace( + "{{ title }}", TITLE + ).replace( + "{{ table_rows }}", "\n".join(table_rows) + ) + + +def open_html_file() -> None: + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + prefix="webservers-", + suffix=".html", + delete=False, + ) as fp: + fp.write(get_info_html()) + path: str = fp.name + + os.startfile(path) + + +if __name__ == "__main__": + open_html_file() diff --git a/psutil_example/print_all_webservers/web.py b/psutil_example/print_all_webservers/web.py index 114811c60..0687ee765 100644 --- a/psutil_example/print_all_webservers/web.py +++ b/psutil_example/print_all_webservers/web.py @@ -1,130 +1,45 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from http.server import BaseHTTPRequestHandler, HTTPServer import os -from urllib.parse import urlsplit - -# pip install psutil -import psutil - - -TITLE = "Список запущенных серверов" -HEADERS = ["#", "PID", "Название", "Порт(ы)", "Путь"] - -def _get_processes() -> list: - processes = [] - for proc in psutil.process_iter(): - try: - connections = [c for c in proc.connections() if c.status == psutil.CONN_LISTEN and c.laddr] - if not connections: - continue - - ports = sorted(set(c.laddr.port for c in connections)) - processes.append({ - 'pid': proc.pid, - 'name': proc.name(), - 'path': ' '.join(os.path.normpath(x) for x in proc.cmdline()), - 'ports': ', '.join(map(str, ports)), - }) - - except psutil.AccessDenied: - pass - - processes.sort(key=lambda p: int(''.join(c for c in p['ports'] if c.isdigit()))) +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import urlsplit - return processes +from get_info_html import get_info_html class HttpProcessor(BaseHTTPRequestHandler): - def do_GET(self): + def do_GET(self) -> None: o = urlsplit(self.path) # Only index - if o.path != '/': + if o.path != "/": self.send_error(404) return - table_rows = [] - for i, p in enumerate(_get_processes(), 1): - table_rows.append(f''' - - {i} - {p['pid']} - {p['name']} - {p['ports']} - {p['path']} - - ''') - - text = """ - - - - - {{ title }} - - - - - - - - - - - - {{ headers }} - - {{ table_rows }} - -
    {{ title }}
    - - - """ \ - .replace('{{ title }}', TITLE) \ - .replace('{{ headers }}', ''.join(f'{x}' for x in HEADERS)) \ - .replace('{{ table_rows }}', ''.join(table_rows)) + text = get_info_html() self.send_response(200) - self.send_header('Content-Type', 'text/html; charset=utf-8') - self.send_header('Connection', 'close') + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Connection", "close") self.end_headers() - self.wfile.write(text.encode('utf-8')) + self.wfile.write(text.encode("utf-8")) -def run(server_class=HTTPServer, handler_class=HttpProcessor, port=8080): - print(f'HTTP server running on http://127.0.0.1:{port}') +def run(server_class=HTTPServer, handler_class=HttpProcessor, port=8080) -> None: + print(f"HTTP server running on http://127.0.0.1:{port}") - server_address = ('', port) + server_address = ("", port) httpd = server_class(server_address, handler_class) httpd.serve_forever() -if __name__ == '__main__': - run(port=10013) +if __name__ == "__main__": + run( + port=int(os.environ.get("PORT", 50012)), + ) diff --git a/psutil_example/print_current_python_webservers/web.py b/psutil_example/print_current_python_webservers/web.py index f9d176cb0..9b36cc0db 100644 --- a/psutil_example/print_current_python_webservers/web.py +++ b/psutil_example/print_current_python_webservers/web.py @@ -1,70 +1,78 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from http.server import BaseHTTPRequestHandler, HTTPServer import os + +from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlsplit -# pip install psutil +# pip install psutil==6.1.0 import psutil -NEED_PROCESS = ['python.exe', 'pythonw.exe'] +NEED_PROCESS = ["python.exe", "pythonw.exe"] TITLE = "Список запущенных серверов на python" HEADERS = ["#", "PID", "Порт(ы)", "Путь"] -def _get_processes() -> list: +def _port_to_tag_a(port: int) -> str: + return f'{port}' + + +def _get_processes() -> list[dict[str, str | int]]: processes = [] for proc in psutil.process_iter(): if not proc.is_running() or proc.name().lower() not in NEED_PROCESS: continue - connections = [c for c in proc.connections() if c.status == psutil.CONN_LISTEN and c.laddr] + connections = [ + c for c in proc.net_connections() if c.status == psutil.CONN_LISTEN and c.laddr + ] if not connections: continue ports = sorted(set(c.laddr.port for c in connections)) - processes.append({ - 'pid': proc.pid, - 'name': proc.name(), - 'path': os.path.normpath(proc.cmdline()[1]), - 'ports': ', '.join(map(_port_to_tag_a, ports)), - }) + processes.append( + { + "pid": proc.pid, + "name": proc.name(), + "path": " ".join(proc.cmdline()[1:]), + "ports": ", ".join(map(_port_to_tag_a, ports)), + } + ) - processes.sort(key=lambda p: int(''.join(c for c in p['ports'] if c.isdigit()))) + processes.sort(key=lambda p: int("".join(c for c in p["ports"] if c.isdigit()))) return processes -def _port_to_tag_a(port: int) -> str: - return f'{port}' - - class HttpProcessor(BaseHTTPRequestHandler): - def do_GET(self): + def do_GET(self) -> None: o = urlsplit(self.path) # Only index - if o.path != '/': + if o.path != "/": self.send_error(404) return table_rows = [] for i, p in enumerate(_get_processes(), 1): - table_rows.append(f''' + table_rows.append( + f""" {i} {p['pid']} {p['ports']} {p['path']} - ''') + """ + ) - text = """ + text = ( + """ @@ -108,26 +116,28 @@ def do_GET(self): - """ \ - .replace('{{ title }}', TITLE) \ - .replace('{{ headers }}', ''.join(f'{x}' for x in HEADERS)) \ - .replace('{{ table_rows }}', ''.join(table_rows)) + """.replace( + "{{ title }}", TITLE + ) + .replace("{{ headers }}", "".join(f"{x}" for x in HEADERS)) + .replace("{{ table_rows }}", "".join(table_rows)) + ) self.send_response(200) - self.send_header('Content-Type', 'text/html; charset=utf-8') - self.send_header('Connection', 'close') + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Connection", "close") self.end_headers() - self.wfile.write(text.encode('utf-8')) + self.wfile.write(text.encode("utf-8")) -def run(server_class=HTTPServer, handler_class=HttpProcessor, port=8080): - print(f'HTTP server running on http://127.0.0.1:{port}') +def run(server_class=HTTPServer, handler_class=HttpProcessor, port=8080) -> None: + print(f"HTTP server running on http://127.0.0.1:{port}") - server_address = ('', port) + server_address = ("", port) httpd = server_class(server_address, handler_class) httpd.serve_forever() -if __name__ == '__main__': +if __name__ == "__main__": run(port=10012) diff --git a/psutil_example/print_process_memory/print_process_by_memory.py b/psutil_example/print_process_memory/print_process_by_memory.py index 57dec5fbc..c859bf762 100644 --- a/psutil_example/print_process_memory/print_process_by_memory.py +++ b/psutil_example/print_process_memory/print_process_by_memory.py @@ -1,19 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import sys -from pathlib import Path - from collections import defaultdict # pip install psutil import psutil -sys.path.append(str(Path(__file__).resolve().parent.parent)) -from human_byte_size import sizeof_fmt +# pip install humanize +from humanize import naturalsize as sizeof_fmt column_width = defaultdict(int) @@ -21,15 +18,15 @@ for p in psutil.process_iter(): memory = p.memory_info().rss - cols = p.name(), str(memory) + ' bytes', sizeof_fmt(memory) + cols = p.name(), str(memory) + " bytes", sizeof_fmt(memory) process_list.append(cols) for i, x in enumerate(cols): column_width[i] = max(column_width[i], len(x)) # Sort by memory size -process_list.sort(key=lambda x: int(x[1].split(' ')[0]), reverse=True) +process_list.sort(key=lambda x: int(x[1].split(" ")[0]), reverse=True) for p in process_list: row = [x.rjust(column_width[i]) for i, x in enumerate(p)] - print(' | '.join(row)) + print(" | ".join(row)) diff --git a/psutil_example/print_process_memory/print_process_by_memory__simple.py b/psutil_example/print_process_memory/print_process_by_memory__simple.py index 188aface3..17c2ae6b8 100644 --- a/psutil_example/print_process_memory/print_process_by_memory__simple.py +++ b/psutil_example/print_process_memory/print_process_by_memory__simple.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil diff --git a/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox.py b/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox.py index 06d8b8444..b4cefbbf1 100644 --- a/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox.py +++ b/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil @@ -10,7 +10,7 @@ for p in psutil.process_iter(): name = p.name() - if name.lower() != 'firefox.exe': + if name.lower() != "firefox.exe": continue print(name, p.memory_info().rss) diff --git a/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox_total.py b/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox_total.py index 2503e9d16..cf47d0986 100644 --- a/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox_total.py +++ b/psutil_example/print_process_memory/print_process_by_memory__simple__only_firefox_total.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil @@ -12,7 +12,7 @@ for p in psutil.process_iter(): name = p.name() - if name.lower() != 'firefox.exe': + if name.lower() != "firefox.exe": continue memory = p.memory_info().rss @@ -20,9 +20,11 @@ print(name, memory) -print(f''' +print( + f""" Total: {total} bytes {total // 1024} KB {total // 1024 // 1024} MB {total // 1024 // 1024 // 1024} GB -''') +""" +) diff --git a/psutil_example/process_management/analog_subprocess_Popen.py b/psutil_example/process_management/analog_subprocess_Popen.py index ffaa915de..8f8f6f77f 100644 --- a/psutil_example/process_management/analog_subprocess_Popen.py +++ b/psutil_example/process_management/analog_subprocess_Popen.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil from subprocess import PIPE -p = psutil.Popen(['python', "-c", "print('hello')\nprint('world!')"], stdout=PIPE) + +p = psutil.Popen(["python", "-c", "print('hello')\nprint('world!')"], stdout=PIPE) print(p.name()) print(p.username()) print(p.communicate()) diff --git a/psutil_example/process_management/current_process_detail_info.py b/psutil_example/process_management/current_process_detail_info.py index 55ff0101e..a7ac6bbb8 100644 --- a/psutil_example/process_management/current_process_detail_info.py +++ b/psutil_example/process_management/current_process_detail_info.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing import current_process diff --git a/psutil_example/process_management/on_terminate__callback.py b/psutil_example/process_management/on_terminate__callback.py index 712109595..36cffe170 100644 --- a/psutil_example/process_management/on_terminate__callback.py +++ b/psutil_example/process_management/on_terminate__callback.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Run 2 process and wait terminate +import threading +import time -def func(time_life=4): - import time - i = 1 - import threading +def func(time_life=4) -> None: + i = 1 while i <= time_life: print(threading.current_thread(), i) @@ -20,8 +20,9 @@ def func(time_life=4): time.sleep(1) -if __name__ == '__main__': +if __name__ == "__main__": from multiprocessing import Process + p1 = Process(target=func, args=(5,)) p1.start() @@ -30,18 +31,21 @@ def func(time_life=4): # pip install psutil import psutil + process1 = psutil.Process(p1.pid) - print('process1 is_running:', process1.is_running()) + print("process1 is_running:", process1.is_running()) process2 = psutil.Process(p2.pid) - print('process2 is_running:', process2.is_running()) + print("process2 is_running:", process2.is_running()) print() - def on_terminate(proc): - print('Process "{}" terminated'.format(proc)) + def on_terminate(proc) -> None: + print(f'Process "{proc}" terminated') # waits for multiple processes to terminate - gone, alive = psutil.wait_procs([process1, process2], timeout=30, callback=on_terminate) + gone, alive = psutil.wait_procs( + [process1, process2], timeout=30, callback=on_terminate + ) print() - print('gone:', gone) - print('alive:', alive) + print("gone:", gone) + print("alive:", alive) diff --git a/psutil_example/process_management/process_control.py b/psutil_example/process_management/process_control.py index eea6d2052..66961ef30 100644 --- a/psutil_example/process_management/process_control.py +++ b/psutil_example/process_management/process_control.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def func(): - import time - import threading +import time +import threading + +def func() -> None: i = 1 while True: @@ -17,37 +18,39 @@ def func(): time.sleep(1) -if __name__ == '__main__': +if __name__ == "__main__": from multiprocessing import Process + p = Process(target=func) p.start() print(p.pid) # pip install psutil import psutil + process = psutil.Process(p.pid) - print('is_running:', process.is_running()) + print("is_running:", process.is_running()) - print('Info:') + print("Info:") from process_detail_info import print_info + print_info(p.pid) - print('\n') + print("\n") - import time time.sleep(4) - print('suspend on 3 seconds') + print("suspend on 3 seconds") process.suspend() time.sleep(3) - print('resume') + print("resume") process.resume() - print('terminate after 5 seconds') + print("terminate after 5 seconds") time.sleep(5) process.terminate() - print('terminate') + print("terminate") result_code = process.wait(timeout=3) - print('result_code:', result_code) + print("result_code:", result_code) diff --git a/psutil_example/process_management/process_detail_info.py b/psutil_example/process_management/process_detail_info.py index aeaf2f7c2..b2ff6649b 100644 --- a/psutil_example/process_management/process_detail_info.py +++ b/psutil_example/process_management/process_detail_info.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # NOTE: Need run as admin @@ -10,52 +10,52 @@ import psutil -def print_info(pid): +def print_info(pid) -> None: process = psutil.Process(pid) - print('Process:', process) - - print('name:', process.name()) - print('as_dict:', process.as_dict()) - print('is_running:', process.is_running()) - - print('exe:', process.exe()) - print('cwd:', process.cwd()) - print('cmdline:', process.cmdline()) - print('pid:', process.pid) - print('ppid:', process.ppid()) - print('parent:', process.parent()) - print('children:', process.children()) - print('status:', process.status()) - print('username:', process.username()) - print('create_time:', process.create_time()) + print("Process:", process) + + print("name:", process.name()) + print("as_dict:", process.as_dict()) + print("is_running:", process.is_running()) + + print("exe:", process.exe()) + print("cwd:", process.cwd()) + print("cmdline:", process.cmdline()) + print("pid:", process.pid) + print("ppid:", process.ppid()) + print("parent:", process.parent()) + print("children:", process.children()) + print("status:", process.status()) + print("username:", process.username()) + print("create_time:", process.create_time()) # If POSIX: # print('uids:', process.uids()) # print('gids:', process.gids()) - print('cpu_times:', process.cpu_times()) - print('cpu_percent:', process.cpu_percent(interval=1.0)) - print('cpu_affinity:', process.cpu_affinity()) - print('cpu_affinity:', process.cpu_affinity([0, 1])) + print("cpu_times:", process.cpu_times()) + print("cpu_percent:", process.cpu_percent(interval=1.0)) + print("cpu_affinity:", process.cpu_affinity()) + print("cpu_affinity:", process.cpu_affinity([0, 1])) # If Linux, FreeBSD, SunOS # print('cpu_num:', process.cpu_num()) - print('memory_info:', process.memory_info()) - print('memory_full_info:', process.memory_full_info()) - print('memory_percent:', process.memory_percent()) - print('memory_maps:', process.memory_maps()) - print('io_counters:', process.io_counters()) - print('open_files:', process.open_files()) - print('connections:', process.connections()) - print('num_threads:', process.num_threads()) + print("memory_info:", process.memory_info()) + print("memory_full_info:", process.memory_full_info()) + print("memory_percent:", process.memory_percent()) + print("memory_maps:", process.memory_maps()) + print("io_counters:", process.io_counters()) + print("open_files:", process.open_files()) + print("connections:", process.connections()) + print("num_threads:", process.num_threads()) # If POSIX # print('num_fds:', process.num_fds()) - print('threads:', process.threads()) - print('num_ctx_switches:', process.num_ctx_switches()) - print('nice/priority get:', process.nice()) + print("threads:", process.threads()) + print("num_ctx_switches:", process.num_ctx_switches()) + print("nice/priority get:", process.nice()) # Error: OSError: [WinError 87] The parameter is incorrect # print('nice/priority set:', process.nice(10)) @@ -64,23 +64,24 @@ def print_info(pid): # Error: AttributeError: module 'psutil' has no attribute 'IOPRIO_CLASS_IDLE' # print('ionice set:', process.ionice(psutil.IOPRIO_CLASS_IDLE)) - print('ionice get:', process.ionice()) + print("ionice get:", process.ionice()) # If Linux # print('rlimit set:', process.rlimit(psutil.RLIMIT_NOFILE, (5, 5))) # print('rlimit get:', process.rlimit(psutil.RLIMIT_NOFILE)) # If Linux, OSX and Windows - print('environ:', process.environ()) + print("environ:", process.environ()) -if __name__ == '__main__': - print('Random process info') +if __name__ == "__main__": + print("Random process info") process_pid_list = psutil.pids() import random + pid = random.choice(process_pid_list) - print('Pid:', pid) + print("Pid:", pid) print_info(pid) diff --git a/psutil_example/process_management/process_list_info.py b/psutil_example/process_management/process_list_info.py index 1b8d0a78e..22b8e3162 100644 --- a/psutil_example/process_management/process_list_info.py +++ b/psutil_example/process_management/process_list_info.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """Получение списка запущенных процессов.""" @@ -13,7 +13,7 @@ for proc in psutil.process_iter(): try: - pinfo = proc.as_dict(attrs=['pid', 'name']) + pinfo = proc.as_dict(attrs=["pid", "name"]) except psutil.NoSuchProcess: continue diff --git a/psutil_example/process_management/process_pid_list.py b/psutil_example/process_management/process_pid_list.py index 13d59670e..fe5b7caf1 100644 --- a/psutil_example/process_management/process_pid_list.py +++ b/psutil_example/process_management/process_pid_list.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install psutil import psutil process_pid_list = psutil.pids() -print('Process pid list ({}): {}'.format(len(process_pid_list), process_pid_list)) +print(f"Process pid list ({len(process_pid_list)}): {process_pid_list}") diff --git a/psutil_example/process_management/random_process_info.py b/psutil_example/process_management/random_process_info.py index c5224eddc..1c4a997dc 100644 --- a/psutil_example/process_management/random_process_info.py +++ b/psutil_example/process_management/random_process_info.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import random @@ -9,11 +9,11 @@ # pip install psutil import psutil -print('Random process info') +print("Random process info") process_pid_list = psutil.pids() pid = random.choice(process_pid_list) -print('Pid:', pid) +print("Pid:", pid) process = psutil.Process(pid) -print('Process:', process) +print("Process:", process) diff --git a/psutil_example/python_processes_started_more_than_an_hour_ago.py b/psutil_example/python_processes_started_more_than_an_hour_ago.py index ef38f2262..e8be19fe4 100644 --- a/psutil_example/python_processes_started_more_than_an_hour_ago.py +++ b/psutil_example/python_processes_started_more_than_an_hour_ago.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt # pip install psutil import psutil for proc in psutil.process_iter(): - if not proc.name().startswith('python'): + if not proc.name().startswith("python"): continue secs = proc.create_time() - started = DT.datetime.fromtimestamp(secs) - if (DT.datetime.now() - started) >= DT.timedelta(hours=1): + started = dt.datetime.fromtimestamp(secs) + if (dt.datetime.now() - started) >= dt.timedelta(hours=1): print(proc) diff --git a/psutil_example/sensors_info.py b/psutil_example/sensors_info.py index d5cce15e7..91fd5e487 100644 --- a/psutil_example/sensors_info.py +++ b/psutil_example/sensors_info.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Only Linux # pip install psutil import psutil + + print(psutil.sensors_temperatures()) print(psutil.sensors_fans()) print(psutil.sensors_battery()) diff --git a/psutil_example/windows_services.py b/psutil_example/windows_services.py index fb6fae644..2d45453a1 100644 --- a/psutil_example/windows_services.py +++ b/psutil_example/windows_services.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +# pip install tabulate +from tabulate import tabulate + # pip install psutil import psutil + win_service_list = list(psutil.win_service_iter()) -print(f'Win service list ({len(win_service_list)}):') +print(f"Win service list ({len(win_service_list)}):") win_service_list.sort(key=lambda x: x.status()) @@ -18,7 +22,16 @@ # # OR: # # if win_service_list: - headers = ['name', 'display_name', 'status', 'start_type', 'username', 'pid', 'binpath', 'description'] + headers = [ + "name", + "display_name", + "status", + "start_type", + "username", + "pid", + "binpath", + "description", + ] rows = [] for win_service in win_service_list: @@ -27,15 +40,14 @@ rows.append(row) headers = [header.upper() for header in headers] - - # pip install tabulate - from tabulate import tabulate print(tabulate(rows, headers=headers, tablefmt="grid", showindex=True)) print() print() + name = win_service_list[0].name() -print(f'Windows service info: {name}') +print(f"Windows service info: {name}") + win_service = psutil.win_service_get(name) -print(' win_service:', win_service) -print(' win_service info:', win_service.as_dict()) +print(" win_service:", win_service) +print(" win_service info:", win_service.as_dict()) diff --git a/public_key_cryptography.py b/public_key_cryptography.py index f39a28fe0..07aa24b82 100644 --- a/public_key_cryptography.py +++ b/public_key_cryptography.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # https://ru.wikipedia.org/wiki/Криптосистема_с_открытым_ключом @@ -9,19 +9,19 @@ # Разбор примера шифрования с помощью справочника: https://ru.wikipedia.org/wiki/Криптосистема_с_открытым_ключом REFERENCE_GUIDE_NAME_NUM = { - 'Королёв': '5643452', - 'Орехов': '3572651', - 'Рузаева': '4673956', - 'Осипов': '3517289', - 'Батурин': '7755628', - 'Кирсанова': '1235267', - 'Арсеньева': '8492746', + "Королёв": "5643452", + "Орехов": "3572651", + "Рузаева": "4673956", + "Осипов": "3517289", + "Батурин": "7755628", + "Кирсанова": "1235267", + "Арсеньева": "8492746", } # Обратный словарь -- ключом будет число, а значением имя REFERENCE_GUIDE_NUM_NAME = {v: k for k, v in REFERENCE_GUIDE_NAME_NUM.items()} -MESS = 'коробка' +MESS = "коробка" def encrypt(mess): @@ -32,19 +32,20 @@ def encrypt(mess): encrypt_key = sorted(filter(lambda x: x[0].lower() == c, keys))[0] crypto_text_list.append(REFERENCE_GUIDE_NAME_NUM[encrypt_key]) - return '@'.join(crypto_text_list) + return "@".join(crypto_text_list) def decrypt(encrypt_mess): - crypto_num_list = encrypt_mess.split('@') - mess = '' + crypto_num_list = encrypt_mess.split("@") + mess = "" for num in crypto_num_list: mess += REFERENCE_GUIDE_NUM_NAME[num][0].lower() return mess + encrypt_mess = encrypt(MESS) -print('Encrypt: {} -> {}.'.format(MESS, encrypt_mess)) -print('Decrypt: {} -> {}'.format(encrypt_mess, decrypt(encrypt_mess))) +print(f"Encrypt: {MESS} -> {encrypt_mess}.") +print(f"Decrypt: {encrypt_mess} -> {decrypt(encrypt_mess)}") diff --git a/pyDes__examples/hello_world.py b/pyDes__examples/hello_world.py index f5c675a27..6d7f42db7 100644 --- a/pyDes__examples/hello_world.py +++ b/pyDes__examples/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/toddw-as/pyDes @@ -10,11 +10,13 @@ # pip install pyDes import pyDes -data = b"Hello World!" +data = b"Hello World!" # def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): -k = pyDes.des(b"DESCRYPT", pyDes.CBC, b"\0\0\0\0\0\0\0\0", pad=None, padmode=pyDes.PAD_PKCS5) +k = pyDes.des( + b"DESCRYPT", pyDes.CBC, b"\0\0\0\0\0\0\0\0", pad=None, padmode=pyDes.PAD_PKCS5 +) d = k.encrypt(data) print("Encrypted: %r" % d) diff --git a/pyautogui__keyboard__examples/10_second_macro.py b/pyautogui__keyboard__examples/10_second_macro.py index c8b0e1278..031ab1fcd 100644 --- a/pyautogui__keyboard__examples/10_second_macro.py +++ b/pyautogui__keyboard__examples/10_second_macro.py @@ -5,9 +5,11 @@ # SOURCE: https://github.com/boppreh/keyboard/blob/master/examples/10_second_macro.py +import time + # pip install keyboard import keyboard -import time + keyboard.start_recording() time.sleep(10) diff --git a/pyautogui__keyboard__examples/Frozen_Throne__auto_select_local_map_and_ai/main.py b/pyautogui__keyboard__examples/Frozen_Throne__auto_select_local_map_and_ai/main.py index abc1b74ce..f595ff49f 100644 --- a/pyautogui__keyboard__examples/Frozen_Throne__auto_select_local_map_and_ai/main.py +++ b/pyautogui__keyboard__examples/Frozen_Throne__auto_select_local_map_and_ai/main.py @@ -1,34 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # NOTE: Frozen Throne, 2560x1440 import time + # import winsound # pip install pyautogui import pyautogui -LOCAL_NETWORK_BUTTON = 'images/local_network_button.png' -NEW_GAME_BUTTON = 'images/new_game_button.png' -NEW_GAME_BUTTON_SELECT_MAP = 'images/new_game_button_select_map.png' +LOCAL_NETWORK_BUTTON = "images/local_network_button.png" +NEW_GAME_BUTTON = "images/new_game_button.png" +NEW_GAME_BUTTON_SELECT_MAP = "images/new_game_button_select_map.png" -OPEN_COMBO_BOX_SENTINEL = 'images/open_combo_box_sentinel.png' -OPEN_COMBO_BOX_SCOURGE = 'images/open_combo_box_scourge.png' +OPEN_COMBO_BOX_SENTINEL = "images/open_combo_box_sentinel.png" +OPEN_COMBO_BOX_SCOURGE = "images/open_combo_box_scourge.png" SELECT_AI_MENU_SENTINEL = "images/select_ai_menu_sentinel.png" SELECT_AI_MENU_SCOURGE = "images/select_ai_menu_scourge.png" AI_EASY = "images/ai_easy.png" -def go_local_network(): +def go_local_network() -> bool: pos = pyautogui.locateCenterOnScreen(LOCAL_NETWORK_BUTTON) - print('LOCAL_NETWORK_BUTTON:', pos) + print("LOCAL_NETWORK_BUTTON:", pos) if pos: pyautogui.click(pos) @@ -40,9 +41,9 @@ def go_local_network(): return False -def go_new_game(): +def go_new_game() -> bool: pos = pyautogui.locateCenterOnScreen(NEW_GAME_BUTTON) - print('NEW_GAME_BUTTON:', pos) + print("NEW_GAME_BUTTON:", pos) if pos: pyautogui.click(pos) @@ -54,9 +55,9 @@ def go_new_game(): return False -def go_select_map(): +def go_select_map() -> bool: pos = pyautogui.locateCenterOnScreen(NEW_GAME_BUTTON_SELECT_MAP) - print('NEW_GAME_BUTTON_SELECT_MAP:', pos) + print("NEW_GAME_BUTTON_SELECT_MAP:", pos) if pos: pyautogui.click(pos) @@ -68,7 +69,7 @@ def go_select_map(): return False -def bot_says(): +def bot_says() -> None: text = """\ Bot say: -aremnpakulsc @@ -77,10 +78,10 @@ def bot_says(): for line in text.splitlines(): pyautogui.typewrite(line) - pyautogui.typewrite(['enter']) + pyautogui.typewrite(["enter"]) -def select_sentinel_and_scourge(coords_sentinel, coords_scourge): +def select_sentinel_and_scourge(coords_sentinel, coords_scourge) -> None: for rect in coords_sentinel + coords_scourge: pos = pyautogui.center(rect) @@ -93,14 +94,16 @@ def select_sentinel_and_scourge(coords_sentinel, coords_scourge): # Ждем появления меню pos_menu_sentinel = pyautogui.locateCenterOnScreen(SELECT_AI_MENU_SENTINEL) pos_menu_scourge = pyautogui.locateCenterOnScreen(SELECT_AI_MENU_SCOURGE) - print(f'SELECT_AI_MENU_SENTINEL: {pos_menu_sentinel}, SELECT_AI_MENU_SCOURGE: {pos_menu_scourge}') + print( + f"SELECT_AI_MENU_SENTINEL: {pos_menu_sentinel}, SELECT_AI_MENU_SCOURGE: {pos_menu_scourge}" + ) if pos_menu_sentinel or pos_menu_scourge: break # Выбираем игрока pos = pyautogui.locateCenterOnScreen(AI_EASY) - print('AI_EASY:', pos) + print("AI_EASY:", pos) if pos: pyautogui.moveTo(pos) or time.sleep(0.3) pyautogui.click(pos) @@ -108,7 +111,7 @@ def select_sentinel_and_scourge(coords_sentinel, coords_scourge): time.sleep(0.3) -if __name__ == '__main__': +if __name__ == "__main__": while True: # Кликаем на Локальную игру while not go_local_network(): @@ -133,7 +136,9 @@ def select_sentinel_and_scourge(coords_sentinel, coords_scourge): while True: coords_sentinel = list(pyautogui.locateAllOnScreen(OPEN_COMBO_BOX_SENTINEL)) coords_scourge = list(pyautogui.locateAllOnScreen(OPEN_COMBO_BOX_SCOURGE)) - print(f'coords_sentinel: {coords_sentinel}, coords_scourge: {coords_scourge}') + print( + f"coords_sentinel: {coords_sentinel}, coords_scourge: {coords_scourge}" + ) if coords_sentinel and coords_scourge: # winsound.Beep(1000, duration=10) diff --git a/pyautogui__keyboard__examples/auto_input_text__repo_example.py b/pyautogui__keyboard__examples/auto_input_text__repo_example.py index 97263406c..5beef36cf 100644 --- a/pyautogui__keyboard__examples/auto_input_text__repo_example.py +++ b/pyautogui__keyboard__examples/auto_input_text__repo_example.py @@ -1,29 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard -keyboard.press_and_release('tab, tab, tab') -keyboard.write('The quick brown fox jumps over the lazy dog.') + +keyboard.press_and_release("tab, tab, tab") + +keyboard.write("The quick brown fox jumps over the lazy dog.") # Press PAGE UP then PAGE DOWN to type "foobar". -keyboard.add_hotkey('page up, page down', lambda: keyboard.write('foobar')) +keyboard.add_hotkey("page up, page down", lambda: keyboard.write("foobar")) # Blocks until you press esc. -keyboard.wait('esc') +keyboard.wait("esc") # Record events until 'esc' is pressed. -recorded = keyboard.record(until='esc') +recorded = keyboard.record(until="esc") # Then replay back at three times the speed. keyboard.play(recorded, speed_factor=3) # Type @@ then press space to replace with abbreviation. -keyboard.add_abbreviation('@@', 'my.long.email@example.com') +keyboard.add_abbreviation("@@", "my.long.email@example.com") # Block forever. keyboard.wait() diff --git a/pyautogui__keyboard__examples/auto_replace_input_text.py b/pyautogui__keyboard__examples/auto_replace_input_text.py index 516b0f4ff..f2aeeaa91 100644 --- a/pyautogui__keyboard__examples/auto_replace_input_text.py +++ b/pyautogui__keyboard__examples/auto_replace_input_text.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard + # Type fuck then press space to replace with abbreviation. -keyboard.add_abbreviation('fuck', 'Furnification under consult of King') +keyboard.add_abbreviation("fuck", "Furnification under consult of King") # Block forever. keyboard.wait() diff --git a/pyautogui__keyboard__examples/autoclick_in_game__pyautoit.py b/pyautogui__keyboard__examples/autoclick_in_game__pyautoit.py new file mode 100644 index 000000000..b618b0b83 --- /dev/null +++ b/pyautogui__keyboard__examples/autoclick_in_game__pyautoit.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import time +import os +import winsound + +from threading import Thread + +# pip install PyAutoIt==0.6.5 +import autoit + +# pip install keyboard==0.13.5 +import keyboard + + +DATA = { + "START": False, +} + +HOTKEY: str = os.environ.get("HOTKEY", "Ctrl+Alt+Space") + + +def change_start() -> None: + DATA["START"] = not DATA["START"] + print("START:", DATA["START"]) + + winsound.Beep(1000, duration=50) + + +def process_auto_click() -> None: + while True: + if not DATA["START"]: + time.sleep(0.100) + continue + + # Симуляция атаки + if DATA["START"]: + autoit.mouse_click() + + time.sleep(0.070) + + +keyboard.add_hotkey(HOTKEY, change_start) + +# Запуск потока для автоатаки +thread_auto_click = Thread(target=process_auto_click) +thread_auto_click.start() + + +print(f"Press {HOTKEY} for starting/stopping") + +while True: + if not DATA["START"]: + time.sleep(0.100) + continue diff --git a/pyautogui__keyboard__examples/customizable_hotkey.py b/pyautogui__keyboard__examples/customizable_hotkey.py index be71e8953..28da7ef28 100644 --- a/pyautogui__keyboard__examples/customizable_hotkey.py +++ b/pyautogui__keyboard__examples/customizable_hotkey.py @@ -8,16 +8,18 @@ # pip install keyboard import keyboard -print('Press and release your desired shortcut: ') + +print("Press and release your desired shortcut: ") shortcut = keyboard.read_shortcut() -print('Shortcut selected:', shortcut) +print("Shortcut selected:", shortcut) -def on_triggered(): +def on_triggered() -> None: print("Triggered!") + keyboard.add_hotkey(shortcut, on_triggered) print("Press ESC to stop.") -keyboard.wait('esc') +keyboard.wait("esc") diff --git a/pyautogui__keyboard__examples/hotkey.py b/pyautogui__keyboard__examples/hotkey.py index d1cdbc4a1..71d22ad30 100644 --- a/pyautogui__keyboard__examples/hotkey.py +++ b/pyautogui__keyboard__examples/hotkey.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard -keyboard.add_hotkey('shift', lambda: keyboard.write('on')) -keyboard.add_hotkey('shift', lambda: keyboard.write('off'), trigger_on_release=True) + + +keyboard.add_hotkey("shift", lambda: keyboard.write("on")) +keyboard.add_hotkey("shift", lambda: keyboard.write("off"), trigger_on_release=True) # Block forever. keyboard.wait() diff --git a/pyautogui__keyboard__examples/hotkey__hello_world.py b/pyautogui__keyboard__examples/hotkey__hello_world.py index 0f43941fe..dd6599df4 100644 --- a/pyautogui__keyboard__examples/hotkey__hello_world.py +++ b/pyautogui__keyboard__examples/hotkey__hello_world.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard -def foo(): - print('World') +def foo() -> None: + print("World") -keyboard.add_hotkey('Ctrl + 1', lambda: print('Hello')) -keyboard.add_hotkey('Ctrl + 2', foo) +keyboard.add_hotkey("Ctrl + 1", lambda: print("Hello")) +keyboard.add_hotkey("Ctrl + 2", foo) -keyboard.wait('Ctrl + Q') +keyboard.wait("Ctrl + Q") diff --git a/pyautogui__keyboard__examples/hotkey__minimize_current_windows__Alt_Space_N.py b/pyautogui__keyboard__examples/hotkey__minimize_current_windows__Alt_Space_N.py index fe6a508e4..210c99773 100644 --- a/pyautogui__keyboard__examples/hotkey__minimize_current_windows__Alt_Space_N.py +++ b/pyautogui__keyboard__examples/hotkey__minimize_current_windows__Alt_Space_N.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard -keyboard.add_hotkey('Win + PageDown', lambda: keyboard.send('Alt + Space + N')) + + +keyboard.add_hotkey("Win + PageDown", lambda: keyboard.send("Alt + Space + N")) # Block forever. keyboard.wait() diff --git a/pyautogui__keyboard__examples/hotkey__mouse___down_up.py b/pyautogui__keyboard__examples/hotkey__mouse___down_up.py index 2e65922eb..8ca58e968 100644 --- a/pyautogui__keyboard__examples/hotkey__mouse___down_up.py +++ b/pyautogui__keyboard__examples/hotkey__mouse___down_up.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' - - -import pyautogui +__author__ = "ipetrash" # pip install keyboard import keyboard -keyboard.add_hotkey('Ctrl + 1', pyautogui.mouseDown) -keyboard.add_hotkey('Ctrl + 2', pyautogui.mouseUp) -keyboard.wait('Ctrl + Q') +import pyautogui + + +keyboard.add_hotkey("Ctrl + 1", pyautogui.mouseDown) +keyboard.add_hotkey("Ctrl + 2", pyautogui.mouseUp) +keyboard.wait("Ctrl + Q") diff --git a/pyautogui__keyboard__examples/hotkey__mouse___down_up__sound_beep.py b/pyautogui__keyboard__examples/hotkey__mouse___down_up__sound_beep.py index 27e3741ef..90f540165 100644 --- a/pyautogui__keyboard__examples/hotkey__mouse___down_up__sound_beep.py +++ b/pyautogui__keyboard__examples/hotkey__mouse___down_up__sound_beep.py @@ -1,15 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import pyautogui import winsound -# pip install keyboard +# pip install keyboard==0.13.5 import keyboard -keyboard.add_hotkey('Ctrl + 1', lambda: pyautogui.mouseDown() or winsound.Beep(1000, duration=50)) -keyboard.add_hotkey('Ctrl + 2', lambda: pyautogui.mouseUp() or winsound.Beep(1000, duration=50)) -keyboard.wait('Ctrl + Q') +# pip install pyautogui==0.9.54 +import pyautogui + + +def beep() -> None: + winsound.Beep(1000, duration=50) + + +keyboard.add_hotkey( + "Ctrl + 1", lambda: pyautogui.mouseDown() or beep() +) +keyboard.add_hotkey( + "Ctrl + 2", lambda: pyautogui.mouseUp() or beep() +) + +keyboard.wait("Ctrl + Q") diff --git a/pyautogui__keyboard__examples/hotkey__start_pause_quit.py b/pyautogui__keyboard__examples/hotkey__start_pause_quit.py index e4ee1d20a..a326f9091 100644 --- a/pyautogui__keyboard__examples/hotkey__start_pause_quit.py +++ b/pyautogui__keyboard__examples/hotkey__start_pause_quit.py @@ -1,51 +1,54 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -RUN_COMBINATION = 'Ctrl+Shift+R' -QUIT_COMBINATION = 'Ctrl+Shift+Q' -AUTO_ATTACK_COMBINATION = 'Ctrl+Shift+Space' +import time +import os + +import keyboard + + +RUN_COMBINATION = "Ctrl+Shift+R" +QUIT_COMBINATION = "Ctrl+Shift+Q" +AUTO_ATTACK_COMBINATION = "Ctrl+Shift+Space" BOT_DATA = { - 'START': False, - 'AUTO_ATTACK': False, + "START": False, + "AUTO_ATTACK": False, } -def change_start(): - BOT_DATA['START'] = not BOT_DATA['START'] - print('START:', BOT_DATA['START']) +def change_start() -> None: + BOT_DATA["START"] = not BOT_DATA["START"] + print("START:", BOT_DATA["START"]) -def change_auto_attack(): - BOT_DATA['AUTO_ATTACK'] = not BOT_DATA['AUTO_ATTACK'] - print('AUTO_ATTACK:', BOT_DATA['AUTO_ATTACK']) +def change_auto_attack() -> None: + BOT_DATA["AUTO_ATTACK"] = not BOT_DATA["AUTO_ATTACK"] + print("AUTO_ATTACK:", BOT_DATA["AUTO_ATTACK"]) -print('Press "{}" for RUN / PAUSE'.format(RUN_COMBINATION)) -print('Press "{}" for QUIT'.format(QUIT_COMBINATION)) -print('Press "{}" for AUTO_ATTACK'.format(AUTO_ATTACK_COMBINATION)) +print(f'Press "{RUN_COMBINATION}" for RUN / PAUSE') +print(f'Press "{QUIT_COMBINATION}" for QUIT') +print(f'Press "{AUTO_ATTACK_COMBINATION}" for AUTO_ATTACK') -import time -import os -import keyboard -keyboard.add_hotkey(QUIT_COMBINATION, lambda: print('Quit by Escape') or os._exit(0)) +keyboard.add_hotkey(QUIT_COMBINATION, lambda: print("Quit by Escape") or os._exit(0)) keyboard.add_hotkey(AUTO_ATTACK_COMBINATION, change_auto_attack) keyboard.add_hotkey(RUN_COMBINATION, change_start) -print('Start') +print("Start") i = 1 while True: - if not BOT_DATA['START']: + if not BOT_DATA["START"]: time.sleep(0.1) continue - print(i, 'AUTO_ATTACK:', BOT_DATA['AUTO_ATTACK']) + print(i, "AUTO_ATTACK:", BOT_DATA["AUTO_ATTACK"]) time.sleep(1) i += 1 diff --git a/pyautogui__keyboard__examples/hotkey_change_state.py b/pyautogui__keyboard__examples/hotkey_change_state.py index 519cb8414..de9ea7729 100644 --- a/pyautogui__keyboard__examples/hotkey_change_state.py +++ b/pyautogui__keyboard__examples/hotkey_change_state.py @@ -1,41 +1,44 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -RUN_COMBINATION = 'Ctrl+Shift+R' -QUIT_COMBINATION = 'Ctrl+Shift+T' -AUTO_ATTACK_COMBINATION = 'Ctrl+Shift+Space' +import time +import os + +import keyboard + + +RUN_COMBINATION = "Ctrl+Shift+R" +QUIT_COMBINATION = "Ctrl+Shift+T" +AUTO_ATTACK_COMBINATION = "Ctrl+Shift+Space" BOT_DATA = { - 'AUTO_ATTACK': False, + "AUTO_ATTACK": False, } -def change_auto_attack(): - BOT_DATA['AUTO_ATTACK'] = not BOT_DATA['AUTO_ATTACK'] - print('AUTO_ATTACK:', BOT_DATA['AUTO_ATTACK']) +def change_auto_attack() -> None: + BOT_DATA["AUTO_ATTACK"] = not BOT_DATA["AUTO_ATTACK"] + print("AUTO_ATTACK:", BOT_DATA["AUTO_ATTACK"]) -print('Press "{}" for RUN'.format(RUN_COMBINATION)) -print('Press "{}" for QUIT'.format(QUIT_COMBINATION)) -print('Press "{}" for AUTO_ATTACK'.format(AUTO_ATTACK_COMBINATION)) +print(f'Press "{RUN_COMBINATION}" for RUN') +print(f'Press "{QUIT_COMBINATION}" for QUIT') +print(f'Press "{AUTO_ATTACK_COMBINATION}" for AUTO_ATTACK') -import time -import os -import keyboard -keyboard.add_hotkey(QUIT_COMBINATION, lambda: print('Quit by Escape') or os._exit(0)) +keyboard.add_hotkey(QUIT_COMBINATION, lambda: print("Quit by Escape") or os._exit(0)) keyboard.add_hotkey(AUTO_ATTACK_COMBINATION, change_auto_attack) keyboard.wait(RUN_COMBINATION) -print('Start') +print("Start") i = 1 while True: - print(i, 'AUTO_ATTACK:', BOT_DATA['AUTO_ATTACK']) + print(i, "AUTO_ATTACK:", BOT_DATA["AUTO_ATTACK"]) time.sleep(1) i += 1 diff --git a/pyautogui__keyboard__examples/hotkey_change_state__multi_thread.py b/pyautogui__keyboard__examples/hotkey_change_state__multi_thread.py index cfb2de25d..bf4264347 100644 --- a/pyautogui__keyboard__examples/hotkey_change_state__multi_thread.py +++ b/pyautogui__keyboard__examples/hotkey_change_state__multi_thread.py @@ -1,50 +1,52 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import time import threading +import os + +import keyboard -RUN_COMBINATION = 'Ctrl+Shift+R' -QUIT_COMBINATION = 'Ctrl+Shift+T' -AUTO_ATTACK_COMBINATION = 'Ctrl+Shift+Space' +RUN_COMBINATION = "Ctrl+Shift+R" +QUIT_COMBINATION = "Ctrl+Shift+T" +AUTO_ATTACK_COMBINATION = "Ctrl+Shift+Space" BOT_DATA = { - 'AUTO_ATTACK': False, + "AUTO_ATTACK": False, } -def change_auto_attack(): - BOT_DATA['AUTO_ATTACK'] = not BOT_DATA['AUTO_ATTACK'] - print('AUTO_ATTACK:', BOT_DATA['AUTO_ATTACK']) +def change_auto_attack() -> None: + BOT_DATA["AUTO_ATTACK"] = not BOT_DATA["AUTO_ATTACK"] + print("AUTO_ATTACK:", BOT_DATA["AUTO_ATTACK"]) -print('Press "{}" for RUN'.format(RUN_COMBINATION)) -print('Press "{}" for QUIT'.format(QUIT_COMBINATION)) -print('Press "{}" for AUTO_ATTACK'.format(AUTO_ATTACK_COMBINATION)) +print(f'Press "{RUN_COMBINATION}" for RUN') +print(f'Press "{QUIT_COMBINATION}" for QUIT') +print(f'Press "{AUTO_ATTACK_COMBINATION}" for AUTO_ATTACK') -import time -import os -import keyboard -keyboard.add_hotkey(QUIT_COMBINATION, lambda: print('Quit by Escape') or os._exit(0)) + +keyboard.add_hotkey(QUIT_COMBINATION, lambda: print("Quit by Escape") or os._exit(0)) keyboard.add_hotkey(AUTO_ATTACK_COMBINATION, change_auto_attack) keyboard.wait(RUN_COMBINATION) -print('Start') +print("Start") -def process_auto_attack(): +def process_auto_attack() -> None: i = 1 while True: - print(i, 'AUTO_ATTACK:', BOT_DATA['AUTO_ATTACK']) + print(i, "AUTO_ATTACK:", BOT_DATA["AUTO_ATTACK"]) time.sleep(1) i += 1 + thread_auto_attack = threading.Thread(target=process_auto_attack) thread_auto_attack.start() diff --git a/pyautogui__keyboard__examples/input__hello_world.py b/pyautogui__keyboard__examples/input__hello_world.py index 1b9caa5fa..b6595328a 100644 --- a/pyautogui__keyboard__examples/input__hello_world.py +++ b/pyautogui__keyboard__examples/input__hello_world.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard -keyboard.write('Hello') -keyboard.write(' world!', delay=0.2) + + +keyboard.write("Hello") +keyboard.write(" world!", delay=0.2) diff --git a/pyautogui__keyboard__examples/input__hello_world__by_hotkey.py b/pyautogui__keyboard__examples/input__hello_world__by_hotkey.py index e0a29d584..8040eb6fa 100644 --- a/pyautogui__keyboard__examples/input__hello_world__by_hotkey.py +++ b/pyautogui__keyboard__examples/input__hello_world__by_hotkey.py @@ -1,12 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard -keyboard.add_hotkey('Shift + Home', lambda: keyboard.write('Hello') or keyboard.write(' world!', delay=0.3)) + + +keyboard.add_hotkey( + "Shift + Home", + lambda: keyboard.write("Hello") or keyboard.write(" world!", delay=0.3), +) # Block forever. keyboard.wait() diff --git a/pyautogui__keyboard__examples/input_and_delete__typewrite_press_keyDown_keyUp.py b/pyautogui__keyboard__examples/input_and_delete__typewrite_press_keyDown_keyUp.py index e7fec5fcc..640268450 100644 --- a/pyautogui__keyboard__examples/input_and_delete__typewrite_press_keyDown_keyUp.py +++ b/pyautogui__keyboard__examples/input_and_delete__typewrite_press_keyDown_keyUp.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://pyautogui.readthedocs.io/en/latest/keyboard.html#the-press-keydown-and-keyup-functions @@ -11,18 +11,18 @@ import pyautogui -TEXT = 'Hello World!' +TEXT = "Hello World!" # Input text pyautogui.typewrite(TEXT) # Select text -pyautogui.keyDown('shift') # Hold down the shift key +pyautogui.keyDown("shift") # Hold down the shift key for _ in TEXT: - pyautogui.press('left') # Press the left arrow key + pyautogui.press("left") # Press the left arrow key -pyautogui.keyUp('shift') # Release the shift key +pyautogui.keyUp("shift") # Release the shift key -pyautogui.press('delete') +pyautogui.press("delete") diff --git a/pyautogui__keyboard__examples/input_cheats_starcraft.py b/pyautogui__keyboard__examples/input_cheats_starcraft.py deleted file mode 100644 index 83101b9da..000000000 --- a/pyautogui__keyboard__examples/input_cheats_starcraft.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# В игре: нажать Enter, ввести комбинацию для чита, нажать Enter - - -# pip install keyboard -import keyboard - - -def write(text: str): - print(text) - - # Удаление символа, который мог попасть при вводе хоткея, например Z, X, C или V - keyboard.send('backspace') - keyboard.write(text) - - -# Пoкaзaть вcю кapтy -keyboard.add_hotkey('Ctrl+Shift+Z', write, args=['BLACK SHEEP WALL']) - -# Пoлyчить пo 10,000 минepaлoв и гaзa -keyboard.add_hotkey('Ctrl+Shift+X', write, args=['SHOW ME THE MONEY']) - -# Быcтpoe cтpoитeльcтвo и пpoвeдeниe улучшений -keyboard.add_hotkey('Ctrl+Shift+C', write, args=['OPERATION CWAL']) - -# Нeyязвимocть -keyboard.add_hotkey('Ctrl+Shift+V', write, args=['POWER OVERWHELMING']) - -keyboard.wait() diff --git a/pyautogui__keyboard__examples/input_cheats_starcraft2.py b/pyautogui__keyboard__examples/input_cheats_starcraft2.py deleted file mode 100644 index 16d7c5d7c..000000000 --- a/pyautogui__keyboard__examples/input_cheats_starcraft2.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# Запустить скрипт от админа. В игре: нажать Enter, ввести комбинацию для чита, нажать Ctrl+V - - -import os - -# pip install keyboard -import keyboard - - -def write(text: str): - print(text) - - os.system(f'echo {text}|clip') - - -# Отключение тумана войны -keyboard.add_hotkey('Ctrl+Shift+Z', write, args=['TookTheRedPill']) - -# +5000 к каждому ресурсу -keyboard.add_hotkey('Ctrl+Shift+X', write, args=['WhoRunBartertown']) - -# Активация быстрой постройки и улучшений -keyboard.add_hotkey('Ctrl+Shift+C', write, args=['CatFoodForPrawnGuns']) - -# Режим «бога» -keyboard.add_hotkey('Ctrl+Shift+V', write, args=['TerribleTerribleDamage']) - -# Мгновенная победа -keyboard.add_hotkey('Ctrl+Shift+B', write, args=['WhatIsBestInLife']) - -keyboard.wait() diff --git a/pyautogui__keyboard__examples/keyboard.send__ctrl+v.py b/pyautogui__keyboard__examples/keyboard.send__ctrl+v.py index 2afb4fc23..04d7784a4 100644 --- a/pyautogui__keyboard__examples/keyboard.send__ctrl+v.py +++ b/pyautogui__keyboard__examples/keyboard.send__ctrl+v.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/boppreh/keyboard#keyboard.send @@ -9,4 +9,5 @@ # pip install keyboard import keyboard + keyboard.send("Ctrl+V") diff --git a/pyautogui__keyboard__examples/keyboard_hook.py b/pyautogui__keyboard__examples/keyboard_hook.py index 9f91aefb4..2bc6174c0 100644 --- a/pyautogui__keyboard__examples/keyboard_hook.py +++ b/pyautogui__keyboard__examples/keyboard_hook.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install keyboard import keyboard -def print_pressed_keys(e): +def print_pressed_keys(e) -> None: print(e, e.event_type, e.name) diff --git a/pyautogui__keyboard__examples/locate_center_on_screen.py b/pyautogui__keyboard__examples/locate_center_on_screen.py index da80a4f9e..0719d62fe 100644 --- a/pyautogui__keyboard__examples/locate_center_on_screen.py +++ b/pyautogui__keyboard__examples/locate_center_on_screen.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pyautogui @@ -16,8 +16,8 @@ def locate_center_on_screen(needle_image, screenshot_image=None): return pyautogui.locateCenterOnScreen(needle_image) -if __name__ == '__main__': - needle_image = '' +if __name__ == "__main__": + needle_image = "" import time diff --git a/pyautogui__keyboard__examples/loop_key_press__quit_by_press_Escape.py b/pyautogui__keyboard__examples/loop_key_press__quit_by_press_Escape.py index 0c4a3b5e9..f3bac70f6 100644 --- a/pyautogui__keyboard__examples/loop_key_press__quit_by_press_Escape.py +++ b/pyautogui__keyboard__examples/loop_key_press__quit_by_press_Escape.py @@ -1,24 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -QUIT_COMBINATION = 'Esc' -print('Press "{}" for QUIT'.format(QUIT_COMBINATION)) - +import time import os -import keyboard -keyboard.add_hotkey(QUIT_COMBINATION, lambda: print('Quit by Escape') or os._exit(0)) -import time +import keyboard import pyautogui + +QUIT_COMBINATION = "Esc" +print(f'Press "{QUIT_COMBINATION}" for QUIT') + + +keyboard.add_hotkey(QUIT_COMBINATION, lambda: print("Quit by Escape") or os._exit(0)) + time.sleep(5) i = 0 while True: - pyautogui.press('pgdn') + pyautogui.press("pgdn") i += 1 print(i) diff --git a/pyautogui__keyboard__examples/message_box/alert.py b/pyautogui__keyboard__examples/message_box/alert.py index d9ace4589..6e9f1cc64 100644 --- a/pyautogui__keyboard__examples/message_box/alert.py +++ b/pyautogui__keyboard__examples/message_box/alert.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pyautogui import alert -button = alert(text='My Text', title='My Title', button='OK') + + +button = alert(text="My Text", title="My Title", button="OK") print(button) diff --git a/pyautogui__keyboard__examples/message_box/confirm.py b/pyautogui__keyboard__examples/message_box/confirm.py index 8f3f88442..1c2c8c9f8 100644 --- a/pyautogui__keyboard__examples/message_box/confirm.py +++ b/pyautogui__keyboard__examples/message_box/confirm.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pyautogui import confirm -button = confirm(text='My Text', title='My Title', buttons=['OK', 'Cancel']) + + +button = confirm(text="My Text", title="My Title", buttons=["OK", "Cancel"]) print(button) diff --git a/pyautogui__keyboard__examples/message_box/password.py b/pyautogui__keyboard__examples/message_box/password.py index 52cfdcaa8..e8794e0d0 100644 --- a/pyautogui__keyboard__examples/message_box/password.py +++ b/pyautogui__keyboard__examples/message_box/password.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pyautogui import password -result = password(text='My Text', title='My Title', default='Default Text', mask='*') + + +result = password(text="My Text", title="My Title", default="Default Text", mask="*") print(result) -result = password(text='My Text', title='My Title', default='Default Text', mask='?') +result = password(text="My Text", title="My Title", default="Default Text", mask="?") print(result) diff --git a/pyautogui__keyboard__examples/message_box/prompt.py b/pyautogui__keyboard__examples/message_box/prompt.py index cfa67c035..026545a88 100644 --- a/pyautogui__keyboard__examples/message_box/prompt.py +++ b/pyautogui__keyboard__examples/message_box/prompt.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from pyautogui import prompt -result = prompt(text='My Text', title='My Title', default='Default Text') + + +result = prompt(text="My Text", title="My Title", default="Default Text") print(result) diff --git a/pyautogui__keyboard__examples/move_mouse_on_center.py b/pyautogui__keyboard__examples/move_mouse_on_center.py index 4e17f3bf7..fa0f598e1 100644 --- a/pyautogui__keyboard__examples/move_mouse_on_center.py +++ b/pyautogui__keyboard__examples/move_mouse_on_center.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pyautogui + + screenWidth, screenHeight = pyautogui.size() pyautogui.moveTo(screenWidth / 2, screenHeight / 2) diff --git a/pyautogui__keyboard__examples/press_key_random_direction__pyautogui.typewrite.py b/pyautogui__keyboard__examples/press_key_random_direction__pyautogui.typewrite.py index 88d30c557..913f9b723 100644 --- a/pyautogui__keyboard__examples/press_key_random_direction__pyautogui.typewrite.py +++ b/pyautogui__keyboard__examples/press_key_random_direction__pyautogui.typewrite.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import random import pyautogui -# left up down right -directions = ['left', 'up', 'down', 'right'] -import random +# left up down right +directions = ["left", "up", "down", "right"] while True: direction = random.choice(directions) diff --git a/pyautogui__keyboard__examples/pressed_keys.py b/pyautogui__keyboard__examples/pressed_keys.py index b2488c6a2..520de3dcb 100644 --- a/pyautogui__keyboard__examples/pressed_keys.py +++ b/pyautogui__keyboard__examples/pressed_keys.py @@ -14,11 +14,11 @@ import keyboard -def print_pressed_keys(e): - line = ', '.join(str(code) for code in keyboard._pressed_events) +def print_pressed_keys(e) -> None: + line = ", ".join(str(code) for code in keyboard._pressed_events) # '\r' and end='' overwrites the previous line. # ' ' * 40 prints 40 spaces at the end to ensure the previous line is cleared. - print('\r' + line + ' ' * 40, end='') + print("\r" + line + " " * 40, end="") keyboard.hook(print_pressed_keys) diff --git a/pyautogui__keyboard__examples/pyautogui__ctrl+v.py b/pyautogui__keyboard__examples/pyautogui__ctrl+v.py index ef1ce5b92..42aae0e97 100644 --- a/pyautogui__keyboard__examples/pyautogui__ctrl+v.py +++ b/pyautogui__keyboard__examples/pyautogui__ctrl+v.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://pyautogui.readthedocs.io/en/latest/cheatsheet.html#keyboard-functions @@ -10,10 +10,11 @@ # pip install pyautogui import pyautogui + # Variant 1 pyautogui.hotkey("Ctrl", "V") # Variant 2 -pyautogui.keyDown('Ctrl') -pyautogui.press('V') -pyautogui.keyUp('Ctrl') +pyautogui.keyDown("Ctrl") +pyautogui.press("V") +pyautogui.keyUp("Ctrl") diff --git a/pyautogui__keyboard__examples/pyautogui_typewrite__hello_world.py b/pyautogui__keyboard__examples/pyautogui_typewrite__hello_world.py index 6e43a4f64..aa4710550 100644 --- a/pyautogui__keyboard__examples/pyautogui_typewrite__hello_world.py +++ b/pyautogui__keyboard__examples/pyautogui_typewrite__hello_world.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pyautogui import pyautogui + # Prints out "Hello world!" instantly -pyautogui.typewrite('Hello world!') +pyautogui.typewrite("Hello world!") -pyautogui.press('enter') +pyautogui.press("enter") # Prints out "Hello world!" with a quarter second delay after each character -pyautogui.typewrite('Hello world!', interval=0.25) +pyautogui.typewrite("Hello world!", interval=0.25) diff --git a/pyautogui__keyboard__examples/quit_by_global_press_Escape.py b/pyautogui__keyboard__examples/quit_by_global_press_Escape.py index 88f79e9b7..ecdad9882 100644 --- a/pyautogui__keyboard__examples/quit_by_global_press_Escape.py +++ b/pyautogui__keyboard__examples/quit_by_global_press_Escape.py @@ -1,17 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -QUIT_COMBINATION = 'Esc' -print('Press "{}" for QUIT'.format(QUIT_COMBINATION)) - import os +import time + import keyboard -keyboard.add_hotkey(QUIT_COMBINATION, lambda: print('Quit by Escape') or os._exit(0)) -import time + +QUIT_COMBINATION = "Esc" +print(f'Press "{QUIT_COMBINATION}" for QUIT') + + +keyboard.add_hotkey(QUIT_COMBINATION, lambda: print("Quit by Escape") or os._exit(0)) i = 0 while True: diff --git a/pyautogui__keyboard__examples/screenshot.py b/pyautogui__keyboard__examples/screenshot.py index 509ca7860..42af1e3dc 100644 --- a/pyautogui__keyboard__examples/screenshot.py +++ b/pyautogui__keyboard__examples/screenshot.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pyautogui -im1 = pyautogui.screenshot() -print(im1) -im2 = pyautogui.screenshot('my_screenshot.png') # save file -print(im2) + +img1 = pyautogui.screenshot() +print(img1) + +img2 = pyautogui.screenshot("my_screenshot.png") # save file +print(img2) diff --git a/pyautogui__keyboard__examples/screenshot__pil_image__to__opencv_image.py b/pyautogui__keyboard__examples/screenshot__pil_image__to__opencv_image.py index 64d91d121..adacc32c8 100644 --- a/pyautogui__keyboard__examples/screenshot__pil_image__to__opencv_image.py +++ b/pyautogui__keyboard__examples/screenshot__pil_image__to__opencv_image.py @@ -1,21 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# from PIL import Image - -import pyautogui import cv2 import numpy +import pyautogui + pil_image = pyautogui.screenshot(region=(200, 200, 200, 200)) print(pil_image.size) -pil_image.show('pil_image') +pil_image.show("pil_image") opencv_image = cv2.cvtColor(numpy.array(pil_image), cv2.COLOR_RGB2BGR) print(opencv_image.shape[:2]) -cv2.imshow('opencv_image', opencv_image) +cv2.imshow("opencv_image", opencv_image) cv2.waitKey() diff --git a/pyautogui__keyboard__examples/screenshots_by_cycle.py b/pyautogui__keyboard__examples/screenshots_by_cycle.py index 9a13ad10d..81d6a4ce0 100644 --- a/pyautogui__keyboard__examples/screenshots_by_cycle.py +++ b/pyautogui__keyboard__examples/screenshots_by_cycle.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt import time + from pathlib import Path import pyautogui @@ -13,12 +14,12 @@ DIR = Path(__file__).resolve().parent -DIR_SCREENSHOTS = DIR / 'screenshots' +DIR_SCREENSHOTS = DIR / "screenshots" DIR_SCREENSHOTS.mkdir(parents=True, exist_ok=True) while True: - file_name = str(DIR_SCREENSHOTS / f'{DT.datetime.now():%Y-%m-%d_%H%M%S.%f}.png') + file_name = str(DIR_SCREENSHOTS / f"{dt.datetime.now():%Y-%m-%d_%H%M%S.%f}.png") print(file_name) pyautogui.screenshot(file_name) # Save file diff --git a/pyautogui__keyboard__examples/telegram_smile_clicker/main.py b/pyautogui__keyboard__examples/telegram_smile_clicker/main.py index 04b131836..7a221651e 100644 --- a/pyautogui__keyboard__examples/telegram_smile_clicker/main.py +++ b/pyautogui__keyboard__examples/telegram_smile_clicker/main.py @@ -1,35 +1,38 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def go(): - OPEN_SMILE_MENU = 'elements/open_smile_menu.png' - GROUP_SMILE = 'elements/group_smile.png' - CLICK_SMILE = 'elements/click_smile.png' - SEND = 'elements/send.png' +import logging +import sys +import time + +# OpenCv -- for performance +# pip install opencv-python +# +# pip install pyautogui +import pyautogui + + +def go() -> None: + OPEN_SMILE_MENU = "elements/open_smile_menu.png" + GROUP_SMILE = "elements/group_smile.png" + CLICK_SMILE = "elements/click_smile.png" + SEND = "elements/send.png" def get_logger(): - import logging - log = logging.getLogger('telegram_smile_clicker') + log = logging.getLogger("telegram_smile_clicker") log.setLevel(logging.DEBUG) - import sys sh = logging.StreamHandler(stream=sys.stdout) - sh.setFormatter(logging.Formatter('[%(asctime)s] %(message)s')) + sh.setFormatter(logging.Formatter("[%(asctime)s] %(message)s")) log.addHandler(sh) return log log = get_logger() - log_it = lambda pos, filename: log.debug('{} [{}]'.format(pos, filename)) - - # OpenCv -- for performance - # pip install opencv-python - - # pip install pyautogui - import pyautogui + log_it = lambda pos, filename: log.debug(f"{pos} [{filename}]") # Ищем меню с смайлами pos = pyautogui.locateCenterOnScreen(OPEN_SMILE_MENU) @@ -41,7 +44,6 @@ def get_logger(): pyautogui.moveTo(pos) # Ожидаем - import time time.sleep(1) # Ищем наш смайл в меню @@ -80,7 +82,7 @@ def get_logger(): pyautogui.click(pos) -if __name__ == '__main__': +if __name__ == "__main__": # import profile # profile.run('go()') diff --git a/pyautogui__keyboard__examples/wait_and_draw__hotkey__dragRel.py b/pyautogui__keyboard__examples/wait_and_draw__hotkey__dragRel.py index db1e27933..daaa48ec3 100644 --- a/pyautogui__keyboard__examples/wait_and_draw__hotkey__dragRel.py +++ b/pyautogui__keyboard__examples/wait_and_draw__hotkey__dragRel.py @@ -1,30 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -RUN_COMBINATION = 'Ctrl+Shift+R' -QUIT_COMBINATION = 'Esc' - -print('Press "{}" for RUN'.format(RUN_COMBINATION)) -print('Press "{}" for QUIT'.format(QUIT_COMBINATION)) - import os -import keyboard -keyboard.add_hotkey(QUIT_COMBINATION, lambda: print('Quit by Escape') or os._exit(0)) -keyboard.wait(RUN_COMBINATION) +import keyboard import pyautogui + +RUN_COMBINATION = "Ctrl+Shift+R" +QUIT_COMBINATION = "Esc" + +print(f'Press "{RUN_COMBINATION}" for RUN') +print(f'Press "{QUIT_COMBINATION}" for QUIT') + + +keyboard.add_hotkey(QUIT_COMBINATION, lambda: print("Quit by Escape") or os._exit(0)) +keyboard.wait(RUN_COMBINATION) + indent = 2 duration = 0.1 distance = 300 while distance > 0: - pyautogui.dragRel(distance, 0, duration) # move right + pyautogui.dragRel(distance, 0, duration) # move right distance -= indent - pyautogui.dragRel(0, distance, duration) # move down + pyautogui.dragRel(0, distance, duration) # move down pyautogui.dragRel(-distance, 0, duration) # move left distance -= indent pyautogui.dragRel(0, -distance, duration) # move up diff --git a/pyautogui__keyboard__examples/win_calc_clicker/main.py b/pyautogui__keyboard__examples/win_calc_clicker/main.py index 80bd5ac7a..812b850f0 100644 --- a/pyautogui__keyboard__examples/win_calc_clicker/main.py +++ b/pyautogui__keyboard__examples/win_calc_clicker/main.py @@ -1,32 +1,36 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +import os +import time + +# OpenCv -- for performance +# pip install opencv-python +# +# pip install pyautogui +import pyautogui BUTTONS = { - '+': 'buttons/add.png', - '-': 'buttons/sub.png', - '/': 'buttons/div.png', - '*': 'buttons/mul.png', - '=': 'buttons/equal.png', + "+": "buttons/add.png", + "-": "buttons/sub.png", + "/": "buttons/div.png", + "*": "buttons/mul.png", + "=": "buttons/equal.png", } for i in range(10): - BUTTONS[str(i)] = 'buttons/{}.png'.format(i) + BUTTONS[str(i)] = f"buttons/{i}.png" CACHE_POS_BUTTON = dict() -def go(expression): - # OpenCv -- for performance - # pip install opencv-python - - # pip install pyautogui - import pyautogui - +def go(expression) -> None: for x in expression: if x not in BUTTONS: - print('Not found: "{}"'.format(x)) + print(f'Not found: "{x}"') continue if x in CACHE_POS_BUTTON: @@ -43,16 +47,13 @@ def go(expression): pyautogui.click(pos) -def show_test_calc(): - import os - os.startfile('calc.exe') - - import time +def show_test_calc() -> None: + os.startfile("calc.exe") time.sleep(1) -if __name__ == '__main__': +if __name__ == "__main__": show_test_calc() - expression = '1234 * 222 + 3214 = ' + expression = "1234 * 222 + 3214 = " go(expression) diff --git a/pyautogui__keyboard__examples/win_calc_clicker/main__PI_clicker_monster.py b/pyautogui__keyboard__examples/win_calc_clicker/main__PI_clicker_monster.py index ccc4ef5a7..407699b56 100644 --- a/pyautogui__keyboard__examples/win_calc_clicker/main__PI_clicker_monster.py +++ b/pyautogui__keyboard__examples/win_calc_clicker/main__PI_clicker_monster.py @@ -1,12 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from main import go, show_test_calc + + show_test_calc() -expression = '3,14159265358979323846264338327950288419716939937510582097494459230781640628620' \ - '8998628034825342117067982148086513282306647093844609550582231725359408128481117' +expression = ( + "3,14159265358979323846264338327950288419716939937510582097494459230781640628620" + "8998628034825342117067982148086513282306647093844609550582231725359408128481117" +) go(expression) diff --git a/pyautogui__keyboard__examples/win_calc_clicker/main__locateAllOnScreen.py b/pyautogui__keyboard__examples/win_calc_clicker/main__locateAllOnScreen.py index 3bd1b841e..9694f9048 100644 --- a/pyautogui__keyboard__examples/win_calc_clicker/main__locateAllOnScreen.py +++ b/pyautogui__keyboard__examples/win_calc_clicker/main__locateAllOnScreen.py @@ -1,43 +1,47 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +# OpenCv -- for performance +# pip install opencv-python +# +# pip install pyautogui +import pyautogui + from main import show_test_calc + + show_test_calc() BUTTONS = { - '+': 'buttons/add.png', - '-': 'buttons/sub.png', - '/': 'buttons/div.png', - '*': 'buttons/mul.png', - '=': 'buttons/equal.png', + "+": "buttons/add.png", + "-": "buttons/sub.png", + "/": "buttons/div.png", + "*": "buttons/mul.png", + "=": "buttons/equal.png", } for i in range(10): - BUTTONS[str(i)] = 'buttons/{}.png'.format(i) + BUTTONS[str(i)] = f"buttons/{i}.png" cache_pos_button = dict() -# OpenCv -- for performance -# pip install opencv-python - -# pip install pyautogui -import pyautogui - -expression = '1234 * 222 + 3214 = ' +expression = "1234 * 222 + 3214 = " for x in expression: if x not in BUTTONS: - print('Not found: "{}"'.format(x)) + print(f'Not found: "{x}"') continue if x in cache_pos_button: pos_list = cache_pos_button[x] else: file_name = BUTTONS[x] - pos_list = [pyautogui.center(pos) for pos in pyautogui.locateAllOnScreen(BUTTONS[x])] + pos_list = [ + pyautogui.center(pos) for pos in pyautogui.locateAllOnScreen(BUTTONS[x]) + ] if not pos_list: continue diff --git a/pycountry__examples/get_ru_country_by_alpha2.py b/pycountry__examples/get_ru_country_by_alpha2.py index 92055786b..2c5b83f5f 100644 --- a/pycountry__examples/get_ru_country_by_alpha2.py +++ b/pycountry__examples/get_ru_country_by_alpha2.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import gettext @@ -18,7 +18,7 @@ def get_country(code): return _(ru.name) -if __name__ == '__main__': +if __name__ == "__main__": print(get_country("RU")) # Российская Федерация diff --git a/pycryptodome__examples__AES_DES_RSA/AES_with_password__verify_key.py b/pycryptodome__examples__AES_DES_RSA/AES_with_password__verify_key.py index d7151cb31..1c3a930a1 100644 --- a/pycryptodome__examples__AES_DES_RSA/AES_with_password__verify_key.py +++ b/pycryptodome__examples__AES_DES_RSA/AES_with_password__verify_key.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import base64 @@ -18,22 +18,26 @@ class AuthenticationError(Exception): class CryptoAES: - def __init__(self, key: str): - self.key = hashlib.sha256(key.encode('utf-8')).digest() + def __init__(self, key: str) -> None: + self.key = hashlib.sha256(key.encode("utf-8")).digest() def encrypt(self, plain_text: str) -> str: - data = plain_text.encode('utf-8') + data = plain_text.encode("utf-8") cipher = AES.new(self.key, AES.MODE_EAX) cipher_text, tag = cipher.encrypt_and_digest(data) encrypted_data = cipher.nonce + tag + cipher_text - return base64.b64encode(encrypted_data).decode('utf-8') + return base64.b64encode(encrypted_data).decode("utf-8") def decrypt(self, encrypted_text: str) -> str: encrypted_data = base64.b64decode(encrypted_text) - nonce, tag, cipher_text = encrypted_data[:16], encrypted_data[16:32], encrypted_data[32:] + nonce, tag, cipher_text = ( + encrypted_data[:16], + encrypted_data[16:32], + encrypted_data[32:], + ) cipher = AES.new(self.key, AES.MODE_EAX, nonce) try: @@ -41,12 +45,12 @@ def decrypt(self, encrypted_text: str) -> str: except ValueError as e: raise AuthenticationError(e) - return data.decode('utf-8') + return data.decode("utf-8") -if __name__ == '__main__': - text = 'Hello World!' - password = '123' +if __name__ == "__main__": + text = "Hello World!" + password = "123" crypto = CryptoAES(password) encrypted_text = crypto.encrypt(text) @@ -57,6 +61,6 @@ def decrypt(self, encrypted_text: str) -> str: # Decrypt with invalid password try: - CryptoAES('abc').decrypt(encrypted_text) + CryptoAES("abc").decrypt(encrypted_text) except AuthenticationError as e: - assert str(e) == 'MAC check failed' + assert str(e) == "MAC check failed" diff --git a/pycryptodome__examples__AES_DES_RSA/RSA__examples/export_import_keys__with__encrypt_decrypt/main.py b/pycryptodome__examples__AES_DES_RSA/RSA__examples/export_import_keys__with__encrypt_decrypt/main.py index 64b0613b4..eddc432a6 100644 --- a/pycryptodome__examples__AES_DES_RSA/RSA__examples/export_import_keys__with__encrypt_decrypt/main.py +++ b/pycryptodome__examples__AES_DES_RSA/RSA__examples/export_import_keys__with__encrypt_decrypt/main.py @@ -1,23 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # Для импорта rsa import sys -sys.path.append('..') + +sys.path.append("..") import rsa -FILE_NAME_PUBLIC_KEY = 'public.pem' -FILE_NAME_PRIVATE_KEY = 'private.pem' +FILE_NAME_PUBLIC_KEY = "public.pem" +FILE_NAME_PRIVATE_KEY = "private.pem" -with open(FILE_NAME_PUBLIC_KEY, 'rb') as f: +with open(FILE_NAME_PUBLIC_KEY, "rb") as f: public_pem = f.read() -with open(FILE_NAME_PRIVATE_KEY, 'rb') as f: +with open(FILE_NAME_PRIVATE_KEY, "rb") as f: private_pem = f.read() if public_pem and private_pem: @@ -25,27 +26,29 @@ private_key = rsa.import_key(private_pem) else: - print(f'The {repr(FILE_NAME_PUBLIC_KEY)} or {repr(FILE_NAME_PRIVATE_KEY)} files are empty! ' - f'Performing key generation!') + print( + f"The {repr(FILE_NAME_PUBLIC_KEY)} or {repr(FILE_NAME_PRIVATE_KEY)} files are empty! " + f"Performing key generation!" + ) public_key, private_key = rsa.new_keys(key_size=2048) - print('Key generation completed successfully!') + print("Key generation completed successfully!") - with open(FILE_NAME_PUBLIC_KEY, 'wb') as f: - f.write(public_key.exportKey('PEM')) + with open(FILE_NAME_PUBLIC_KEY, "wb") as f: + f.write(public_key.exportKey("PEM")) - with open(FILE_NAME_PRIVATE_KEY, 'wb') as f: - f.write(private_key.exportKey('PEM')) + with open(FILE_NAME_PRIVATE_KEY, "wb") as f: + f.write(private_key.exportKey("PEM")) - print('Successfully save generated keys to files!') + print("Successfully save generated keys to files!") print() -print('private:', private_key.exportKey('PEM')) -print('public:', public_key.exportKey('PEM')) +print("private:", private_key.exportKey("PEM")) +print("public:", public_key.exportKey("PEM")) print() -text = 'Hello World!'.encode('utf-8') +text = "Hello World!".encode("utf-8") encrypted = rsa.encrypt(text, public_key) print(encrypted) diff --git "a/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key/main.py" "b/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key/main.py" index 0e364e905..0512d7a76 100644 --- "a/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key/main.py" +++ "b/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key/main.py" @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/Legrandin/pycryptodome/blob/29810b96cefc900138523203f027a179a15ceffe/Doc/src/examples.rst#generate-public-key-and-private-key @@ -10,8 +10,6 @@ # pip install pycryptodome # OR: # pip install pycryptodomex - - from Crypto.PublicKey import RSA diff --git "a/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__passphrase_and_scrypt/main.py" "b/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__passphrase_and_scrypt/main.py" index cc84ee883..b7050dda3 100644 --- "a/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__passphrase_and_scrypt/main.py" +++ "b/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__passphrase_and_scrypt/main.py" @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pycryptodome # OR: # pip install pycryptodomex - - from Crypto.PublicKey import RSA SECRET_CODE = "Unguessable" key = RSA.generate(2048) -encrypted_key = key.export_key(passphrase=SECRET_CODE, pkcs=8, protection="scryptAndAES128-CBC") +encrypted_key = key.export_key( + passphrase=SECRET_CODE, pkcs=8, protection="scryptAndAES128-CBC" +) with open("rsa_key.bin", "wb") as f: f.write(encrypted_key) diff --git "a/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__with__passphrase/main.py" "b/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__with__passphrase/main.py" index fe016325b..20b31880d 100644 --- "a/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__with__passphrase/main.py" +++ "b/pycryptodome__examples__AES_DES_RSA/RSA__examples/ge\320\277enerate_public_key_and_private_key__with__passphrase/main.py" @@ -1,14 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pycryptodome # OR: # pip install pycryptodomex - - from Crypto.PublicKey import RSA diff --git a/pycryptodome__examples__AES_DES_RSA/RSA__examples/main.py b/pycryptodome__examples__AES_DES_RSA/RSA__examples/main.py index 72fb9faf6..197e4dde8 100644 --- a/pycryptodome__examples__AES_DES_RSA/RSA__examples/main.py +++ b/pycryptodome__examples__AES_DES_RSA/RSA__examples/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/42856051/5909792 @@ -12,12 +12,12 @@ import rsa -msg1 = "Hello Tony, I am Jarvis!".encode('utf-8') -msg2 = "Hello Toni, I am Jarvis!".encode('utf-8') +msg1 = "Hello Tony, I am Jarvis!".encode("utf-8") +msg2 = "Hello Toni, I am Jarvis!".encode("utf-8") public, private = rsa.new_keys(key_size=2048) -print('private:', private.exportKey('PEM')) -print('public:', public.exportKey('PEM')) +print("private:", private.exportKey("PEM")) +print("public:", public.exportKey("PEM")) print() # Encrypt-Decrypt: private -> private @@ -39,7 +39,7 @@ print("Verify:", verify) # False # Encrypt-Decrypt: private -> private -print('\n') +print("\n") # Encrypt-Decrypt: public -> private encrypted = b64encode(rsa.encrypt(msg1, public)) diff --git a/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main.py b/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main.py index 78a60d081..88984e6fd 100644 --- a/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main.py +++ b/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main.py @@ -1,14 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pycryptodome # OR: # pip install pycryptodomex - - from Crypto.PublicKey import RSA diff --git a/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main__use_file.py b/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main__use_file.py index a75143b8e..d1ba5eeec 100644 --- a/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main__use_file.py +++ b/pycryptodome__examples__AES_DES_RSA/RSA__examples/read_private_key_and_get_public/main__use_file.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pycryptodome # OR: # pip install pycryptodomex - - from Crypto.PublicKey import RSA SECRET_CODE = "Unguessable" key = RSA.generate(2048) -encrypted_key = key.export_key(passphrase=SECRET_CODE, pkcs=8, protection="scryptAndAES128-CBC") +encrypted_key = key.export_key( + passphrase=SECRET_CODE, pkcs=8, protection="scryptAndAES128-CBC" +) with open("rsa_key.bin", "wb") as f: f.write(encrypted_key) diff --git a/pycryptodome__examples__AES_DES_RSA/hello_world__AES_pkcs7_ECB.py b/pycryptodome__examples__AES_DES_RSA/hello_world__AES_pkcs7_ECB.py index 8e7d1c046..dc6768e73 100644 --- a/pycryptodome__examples__AES_DES_RSA/hello_world__AES_pkcs7_ECB.py +++ b/pycryptodome__examples__AES_DES_RSA/hello_world__AES_pkcs7_ECB.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pycryptodome @@ -12,15 +12,16 @@ # pip install pkcs7 from pkcs7 import PKCS7Encoder -key = '12647863128053333215894456187814' + +key = "12647863128053333215894456187814" mode = AES.MODE_ECB -text = 'Hello World!' +text = "Hello World!" encoder = PKCS7Encoder() pad_text = encoder.encode(text) -print(pad_text) # "Hello World!" -print(bytes(pad_text, 'utf-8')) # b'Hello World!\x04\x04\x04\x04' +print(pad_text) # "Hello World!" +print(bytes(pad_text, "utf-8")) # b'Hello World!\x04\x04\x04\x04' encryptor = AES.new(key, mode) @@ -32,7 +33,7 @@ decrypt_text = encryptor.decrypt(cipher) print(decrypt_text) # b'Hello World!\x04\x04\x04\x04' -decrypt_text = str(decrypt_text, 'utf-8') +decrypt_text = str(decrypt_text, "utf-8") unpad_text = encoder.decode(decrypt_text) print(unpad_text) # Hello World! diff --git a/pycryptodome__examples__AES_DES_RSA/hello_world__DES.py b/pycryptodome__examples__AES_DES_RSA/hello_world__DES.py index 910f3db02..e57c1f305 100644 --- a/pycryptodome__examples__AES_DES_RSA/hello_world__DES.py +++ b/pycryptodome__examples__AES_DES_RSA/hello_world__DES.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/Legrandin/pycryptodome @@ -10,8 +10,6 @@ # pip install pycryptodome # OR: # pip install pycryptodomex - - from Crypto.Cipher import DES @@ -26,11 +24,11 @@ def unpad(s: bytes) -> bytes: return s[:-pad_size] -key = b'abcdefgh' +key = b"abcdefgh" des = DES.new(key, DES.MODE_ECB) -text = b'Hello World!' +text = b"Hello World!" padded_text = pad(text) encrypted_text = des.encrypt(padded_text) diff --git a/pycryptodome__examples__AES_DES_RSA/info_security.py b/pycryptodome__examples__AES_DES_RSA/info_security.py index c6477c143..4af65909b 100644 --- a/pycryptodome__examples__AES_DES_RSA/info_security.py +++ b/pycryptodome__examples__AES_DES_RSA/info_security.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import base64 @@ -14,26 +14,26 @@ from Crypto import Random -AES_key = '' +AES_key = "" # SOURCE: https://github.com/maldevel/gdog/blob/a3a47b17231d0ee3a2d0fa8ac986f7485f3f6b82/gdog.py#L68 class InfoSecurity: - def __init__(self, key: str): + def __init__(self, key: str) -> None: self.bs = 32 self.key = hashlib.sha256(key.encode()).digest() def encrypt(self, plain_text: str) -> str: - raw = self._pad(plain_text.encode('utf-8')) + raw = self._pad(plain_text.encode("utf-8")) iv = Random.new().read(AES.block_size) cipher = AES.new(self.key, AES.MODE_CBC, iv) - return base64.b64encode(iv + cipher.encrypt(raw)).decode('utf-8') + return base64.b64encode(iv + cipher.encrypt(raw)).decode("utf-8") def decrypt(self, cipher_text: str) -> str: enc = base64.b64decode(cipher_text) - iv = enc[:AES.block_size] + iv = enc[: AES.block_size] cipher = AES.new(self.key, AES.MODE_CBC, iv) - return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8') + return self._unpad(cipher.decrypt(enc[AES.block_size :])).decode("utf-8") # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/82810f8397907679316107f135a5b1dd1fcca516/pad__unpad__example.py#L7 def _pad(self, s: bytes) -> bytes: @@ -46,15 +46,15 @@ def _unpad(s: bytes) -> bytes: return s[:-pad_size] -if __name__ == '__main__': +if __name__ == "__main__": info_sec = InfoSecurity(AES_key) - text = 'Hello World!' + text = "Hello World!" cipher_text = info_sec.encrypt(text) print(cipher_text) assert info_sec.decrypt(cipher_text) == text - text = 'Привет Мир!' + text = "Привет Мир!" cipher_text = info_sec.encrypt(text) print(cipher_text) assert info_sec.decrypt(cipher_text) == text diff --git a/pygame__examples/about_joystick.py b/pygame__examples/about_joystick.py index a7f2fe86f..5e9864c45 100644 --- a/pygame__examples/about_joystick.py +++ b/pygame__examples/about_joystick.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://www.pygame.org/docs/ref/joystick.html @@ -9,6 +9,7 @@ import pygame + # Define some colors BLACK = (0, 0, 0) WHITE = (255, 255, 255) @@ -18,24 +19,24 @@ # It has nothing to do with the joysticks, just outputting the # information. class TextPrint: - def __init__(self): + def __init__(self) -> None: self.reset() self.font = pygame.font.Font(None, 20) - def print(self, screen, textString): + def print(self, screen, textString) -> None: textBitmap = self.font.render(textString, True, BLACK) screen.blit(textBitmap, [self.x, self.y]) self.y += self.line_height - def reset(self): + def reset(self) -> None: self.x = 10 self.y = 10 self.line_height = 15 - def indent(self): + def indent(self) -> None: self.x += 10 - def unindent(self): + def unindent(self) -> None: self.x -= 10 @@ -81,7 +82,7 @@ def unindent(self): # Get count of joysticks joystick_count = pygame.joystick.get_count() - textPrint.print(screen, "Number of joysticks: {}".format(joystick_count)) + textPrint.print(screen, f"Number of joysticks: {joystick_count}") textPrint.indent() # For each joystick: @@ -89,42 +90,42 @@ def unindent(self): joystick = pygame.joystick.Joystick(i) joystick.init() - textPrint.print(screen, "Joystick {}".format(i)) + textPrint.print(screen, f"Joystick {i}") textPrint.indent() # Get the name from the OS for the controller/joystick name = joystick.get_name() - textPrint.print(screen, "Joystick name: {}".format(name)) + textPrint.print(screen, f"Joystick name: {name}") # Usually axis run in pairs, up/down for one, and left/right for # the other. axes = joystick.get_numaxes() - textPrint.print(screen, "Number of axes: {}".format(axes)) + textPrint.print(screen, f"Number of axes: {axes}") textPrint.indent() for i in range(axes): axis = joystick.get_axis(i) - textPrint.print(screen, "Axis {} value: {:>6.3f}".format(i, axis)) + textPrint.print(screen, f"Axis {i} value: {axis:>6.3f}") textPrint.unindent() buttons = joystick.get_numbuttons() - textPrint.print(screen, "Number of buttons: {}".format(buttons)) + textPrint.print(screen, f"Number of buttons: {buttons}") textPrint.indent() for i in range(buttons): button = joystick.get_button(i) - textPrint.print(screen, "Button {:>2} value: {}".format(i, button)) + textPrint.print(screen, f"Button {i:>2} value: {button}") textPrint.unindent() # Hat switch. All or nothing for direction, not like joysticks. # Value comes back in an array. hats = joystick.get_numhats() - textPrint.print(screen, "Number of hats: {}".format(hats)) + textPrint.print(screen, f"Number of hats: {hats}") textPrint.indent() for i in range(hats): hat = joystick.get_hat(i) - textPrint.print(screen, "Hat {} value: {}".format(i, str(hat))) + textPrint.print(screen, f"Hat {i} value: {str(hat)}") textPrint.unindent() textPrint.unindent() diff --git a/pygame__examples/buttons__using_pygametext.py b/pygame__examples/buttons__using_pygametext.py index 6452c7012..abe72750f 100644 --- a/pygame__examples/buttons__using_pygametext.py +++ b/pygame__examples/buttons__using_pygametext.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -11,6 +11,7 @@ import pygame import pygametext + running = True pygame.init() @@ -20,17 +21,23 @@ pgt = pygametext.PGT(screen) # Define pygametext object. -pgt.button(10, 10, 100, 50, (255, 0, 0), "Hello!", (0, 0, 0), print, "Hello World!", 0) # Add pgt Button -pgt.button(120, 10, 100, 50, (255, 255, 0), "Bye bye", (0, 0, 0), print, "Goodbye World!", 0) # Add pgt Button +pgt.button( + 10, 10, 100, 50, (255, 0, 0), "Hello!", (0, 0, 0), print, "Hello World!", 0 +) # Add pgt Button +pgt.button( + 120, 10, 100, 50, (255, 255, 0), "Bye bye", (0, 0, 0), print, "Goodbye World!", 0 +) # Add pgt Button pgt.text(10, 70, "Simple pygametext example.", (0, 120, 0), 20, 0) # Add pgt Text -def update(): # Update & Eventd +def update() -> None: # Update & Eventd global running events = pygame.event.get() - pgt.update(events, 0) # Update all pgt elements from layer 0. Takes events arg to process some elements. + pgt.update( + events, 0 + ) # Update all pgt elements from layer 0. Takes events arg to process some elements. for event in events: if event.type == pygame.QUIT: @@ -39,7 +46,7 @@ def update(): # Update & Eventd sys.exit() -def draw(): +def draw() -> None: screen.fill((255, 255, 255)) # Clear screen pgt.draw() # Draw all pgt elements from layer 0 diff --git a/pygame__examples/circle_collision.py b/pygame__examples/circle_collision.py index 436fbca9d..83290096a 100644 --- a/pygame__examples/circle_collision.py +++ b/pygame__examples/circle_collision.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -10,12 +10,12 @@ import pygame -BLACK = ( 0, 0, 0) +BLACK = (0, 0, 0) WHITE = (255, 255, 255) class Ball(pygame.sprite.Sprite): - def __init__(self, size, pos=(0, 0), color=WHITE): + def __init__(self, size, pos=(0, 0), color=WHITE) -> None: super().__init__() self.image = pygame.Surface([size, size], pygame.SRCALPHA) diff --git a/pygame__examples/complex_balls_with_physics.py b/pygame__examples/complex_balls_with_physics.py index 45ae742f9..f6fc2c93a 100644 --- a/pygame__examples/complex_balls_with_physics.py +++ b/pygame__examples/complex_balls_with_physics.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import math import random -from typing import Tuple, Optional # SOURCE: http://archive.petercollingridge.co.uk/book/export/html/6 import pygame @@ -19,7 +18,7 @@ gravity = (math.pi, 0.002) -def add_vectors(angle1, length1, angle2, length2) -> Tuple[float, float]: +def add_vectors(angle1, length1, angle2, length2) -> tuple[float, float]: x = math.sin(angle1) * length1 + math.sin(angle2) * length2 y = math.cos(angle1) * length1 + math.cos(angle2) * length2 @@ -29,7 +28,7 @@ def add_vectors(angle1, length1, angle2, length2) -> Tuple[float, float]: return angle, length -def collide(p1, p2): +def collide(p1, p2) -> None: dx = p1.x - p2.x dy = p1.y - p2.y @@ -39,12 +38,16 @@ def collide(p1, p2): total_mass = p1.mass + p2.mass (p1.angle, p1.speed) = add_vectors( - p1.angle, p1.speed * (p1.mass - p2.mass) / total_mass, - angle, 2 * p2.speed * p2.mass / total_mass + p1.angle, + p1.speed * (p1.mass - p2.mass) / total_mass, + angle, + 2 * p2.speed * p2.mass / total_mass, ) (p2.angle, p2.speed) = add_vectors( - p2.angle, p2.speed * (p2.mass - p1.mass) / total_mass, - angle + math.pi, 2 * p1.speed * p1.mass / total_mass + p2.angle, + p2.speed * (p2.mass - p1.mass) / total_mass, + angle + math.pi, + 2 * p1.speed * p1.mass / total_mass, ) p1.speed *= elasticity p2.speed *= elasticity @@ -57,7 +60,7 @@ def collide(p1, p2): class Particle: - def __init__(self, x, y, size, mass=1): + def __init__(self, x, y, size, mass=1) -> None: self.x = x self.y = y self.size = size @@ -68,27 +71,25 @@ def __init__(self, x, y, size, mass=1): self.mass = mass self.drag = (self.mass / (self.mass + mass_of_air)) ** self.size - def display(self): + def display(self) -> None: pygame.draw.circle( - screen, self.colour, - (int(self.x), int(self.y)), - self.size, self.thickness + screen, self.colour, (int(self.x), int(self.y)), self.size, self.thickness ) - def move(self): + def move(self) -> None: self.x += math.sin(self.angle) * self.speed self.y -= math.cos(self.angle) * self.speed self.speed *= self.drag - def bounce(self): + def bounce(self) -> None: if self.x > width - self.size: self.x = 2 * (width - self.size) - self.x - self.angle = - self.angle + self.angle = -self.angle self.speed *= elasticity elif self.x < self.size: self.x = 2 * self.size - self.x - self.angle = - self.angle + self.angle = -self.angle self.speed *= elasticity if self.y > height - self.size: @@ -102,20 +103,20 @@ def bounce(self): self.speed *= elasticity -def find_particle(particles, x, y) -> Optional[Particle]: +def find_particle(particles, x, y) -> Particle | None: for p in particles: if math.hypot(p.x - x, p.y - y) <= p.size: return p return None -def get_rand_color() -> Tuple[int, int, int]: +def get_rand_color() -> tuple[int, int, int]: return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255) clock = pygame.time.Clock() screen = pygame.display.set_mode((width, height)) -pygame.display.set_caption('Tutorial 9') +pygame.display.set_caption("Tutorial 9") number_of_particles = 5 my_particles = [] @@ -126,7 +127,7 @@ def get_rand_color() -> Tuple[int, int, int]: x = random.randint(size, width - size) y = random.randint(size, height - size) - particle = Particle(x, y, size, density * size ** 2) + particle = Particle(x, y, size, density * size**2) particle.colour = get_rand_color() particle.speed = random.random() particle.angle = random.uniform(0, math.pi * 2) @@ -157,7 +158,7 @@ def get_rand_color() -> Tuple[int, int, int]: for i, particle in enumerate(my_particles): particle.move() particle.bounce() - for particle2 in my_particles[i + 1:]: + for particle2 in my_particles[i + 1 :]: collide(particle, particle2) particle.display() diff --git a/pygame__examples/is_point_in_triangle.py b/pygame__examples/is_point_in_triangle.py index 8adc9f947..911a0aeae 100644 --- a/pygame__examples/is_point_in_triangle.py +++ b/pygame__examples/is_point_in_triangle.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from dataclasses import dataclass @@ -10,7 +10,7 @@ import pygame -BLACK = ( 0, 0, 0) +BLACK = (0, 0, 0) WHITE = (255, 255, 255) @@ -27,8 +27,12 @@ def get_triangle_area(a: Point, b: Point, c: Point) -> float: def is_point_in_triangle(a: Point, b: Point, c: Point, p: Point) -> bool: tr_area = get_triangle_area(a, b, c) # Площадь основного треугольника - tr_area2 = get_triangle_area(a, b, p) # Площади треугольника, образованного из 2 точек основного - tr_area3 = get_triangle_area(a, p, c) # и точки, которая проверяется на принадлежность + tr_area2 = get_triangle_area( + a, b, p + ) # Площади треугольника, образованного из 2 точек основного + tr_area3 = get_triangle_area( + a, p, c + ) # и точки, которая проверяется на принадлежность tr_area4 = get_triangle_area(b, p, c) # к треугольнику # Если площади образованных треугольников равны, то точка в треугольнике diff --git a/pygame__examples/move_circle.py b/pygame__examples/move_circle.py index e9ffd57fa..e79999859 100644 --- a/pygame__examples/move_circle.py +++ b/pygame__examples/move_circle.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pygame @@ -58,6 +58,6 @@ if is_pressed[pygame.K_DOWN]: y += step - pygame.display.set_caption("move_circle [{} fps]".format(int(clock.get_fps()))) + pygame.display.set_caption(f"move_circle [{int(clock.get_fps())} fps]") clock.tick(FPS) diff --git a/pygame__examples/move_rect.py b/pygame__examples/move_rect.py index 48da911de..040bffe9f 100644 --- a/pygame__examples/move_rect.py +++ b/pygame__examples/move_rect.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://www.nerdparadise.com/programming/pygame/part1 # pip install pygame import pygame + + pygame.init() screen = pygame.display.set_mode((400, 300)) @@ -53,7 +55,7 @@ pygame.display.flip() - pygame.display.set_caption("move_rect [{} fps]".format(int(clock.get_fps()))) + pygame.display.set_caption(f"move_rect [{int(clock.get_fps())} fps]") # will block execution until 1/60 seconds have passed # since the previous time clock.tick was called. diff --git a/pygame__examples/move_rect__with_joystick__hat_axis_button.py b/pygame__examples/move_rect__with_joystick__hat_axis_button.py index 89f576cd0..afbe15f66 100644 --- a/pygame__examples/move_rect__with_joystick__hat_axis_button.py +++ b/pygame__examples/move_rect__with_joystick__hat_axis_button.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: http://www.nerdparadise.com/programming/pygame/part1 @@ -85,7 +85,7 @@ pygame.display.flip() - pygame.display.set_caption("move_rect [{} fps]".format(int(clock.get_fps()))) + pygame.display.set_caption(f"move_rect [{int(clock.get_fps())} fps]") # will block execution until 1/60 seconds have passed # since the previous time clock.tick was called. diff --git a/pygame__examples/multiline_text__using_ptext/invisible_text.py b/pygame__examples/multiline_text__using_ptext/invisible_text.py index c0e6c394b..6dab8fb77 100644 --- a/pygame__examples/multiline_text__using_ptext/invisible_text.py +++ b/pygame__examples/multiline_text__using_ptext/invisible_text.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -12,7 +12,7 @@ import ptext -BLACK = ( 0, 0, 0) +BLACK = (0, 0, 0) WHITE = (255, 255, 255) diff --git a/pygame__examples/on_press_lctrl+backspace__event.key+key.get_mods.py b/pygame__examples/on_press_lctrl+backspace__event.key+key.get_mods.py index 02bfaf0dc..790bb8938 100644 --- a/pygame__examples/on_press_lctrl+backspace__event.key+key.get_mods.py +++ b/pygame__examples/on_press_lctrl+backspace__event.key+key.get_mods.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pygame -import pygame.locals + pygame.init() -screen = pygame.display.set_mode((300,200)) +screen = pygame.display.set_mode((300, 200)) running = True @@ -20,7 +20,10 @@ elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: running = False - elif event.key == pygame.K_BACKSPACE and pygame.key.get_mods() & pygame.KMOD_LCTRL: + elif ( + event.key == pygame.K_BACKSPACE + and pygame.key.get_mods() & pygame.KMOD_LCTRL + ): print("pressed: LCTRL + BACKSPACE") pygame.quit() diff --git a/pygame__examples/on_press_lctrl+backspace__key_get_pressed.py b/pygame__examples/on_press_lctrl+backspace__key_get_pressed.py index 2cb988349..bc09d0c56 100644 --- a/pygame__examples/on_press_lctrl+backspace__key_get_pressed.py +++ b/pygame__examples/on_press_lctrl+backspace__key_get_pressed.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pygame -import pygame.locals + pygame.init() diff --git a/pygame__examples/rect_collision.py b/pygame__examples/rect_collision.py index 5710a365f..3d3270ff4 100644 --- a/pygame__examples/rect_collision.py +++ b/pygame__examples/rect_collision.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys diff --git a/pygame__examples/simple_balls.py b/pygame__examples/simple_balls.py index e66f48e91..2295a3d57 100644 --- a/pygame__examples/simple_balls.py +++ b/pygame__examples/simple_balls.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from random import randint @@ -11,7 +11,7 @@ class Ball: - def __init__(self, x, y, r, v_x, v_y, color): + def __init__(self, x, y, r, v_x, v_y, color) -> None: self.x = x self.y = y self.r = r @@ -19,11 +19,11 @@ def __init__(self, x, y, r, v_x, v_y, color): self.v_y = v_y self.color = color - def update(self): + def update(self) -> None: self.x += self.v_x self.y += self.v_y - def draw(self, screen): + def draw(self, screen) -> None: pygame.draw.circle(screen, self.color, self.center, self.r) # Нарисуем поверх первого, прозрачный второй с границей (параметр width) @@ -51,7 +51,14 @@ def right(self): class Game: - def __init__(self, width: int, height: int, caption='Balls!', background_color=(255, 255, 255), frame_rate=60): + def __init__( + self, + width: int, + height: int, + caption="Balls!", + background_color=(255, 255, 255), + frame_rate=60, + ) -> None: self.width = width self.height = height self.frame_rate = frame_rate @@ -70,10 +77,12 @@ def __init__(self, width: int, height: int, caption='Balls!', background_color=( self.update_caption() - def update_caption(self): - pygame.display.set_caption("{} [{} fps]".format(self.caption, int(self.clock.get_fps()))) + def update_caption(self) -> None: + pygame.display.set_caption( + f"{self.caption} [{int(self.clock.get_fps())} fps]" + ) - def handle_events(self): + def handle_events(self) -> None: # Получение всех событий for event in pygame.event.get(): # Проверка события "Выход" @@ -85,7 +94,7 @@ def handle_events(self): if is_pressed[pygame.K_ESCAPE]: self.game_active = False - def run(self): + def run(self) -> None: self.game_active = True while self.game_active: @@ -112,7 +121,7 @@ def run(self): self.clock.tick(self.frame_rate) - def append_random_ball(self): + def append_random_ball(self) -> None: def get_random_vector(): pos = 0, 0 # Если pos равен (0, 0), пересчитываем значения, т.к. шарик должен двигаться @@ -134,8 +143,8 @@ def get_random_color(): self.balls.append(ball) -if __name__ == '__main__': - WIDTH = 600 # ширина экрана +if __name__ == "__main__": + WIDTH = 600 # ширина экрана HEIGHT = 600 # высота экрана BALL_NUMBER = 100 diff --git a/pyglet_example/main.py b/pyglet_example/main.py index b5436f639..648220488 100644 --- a/pyglet_example/main.py +++ b/pyglet_example/main.py @@ -1,34 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pyglet from pyglet.gl import * + # Optional audio outputs (Linux examples): # pyglet.options['audio'] = ('alsa', 'openal', 'silent') key = pyglet.window.key class main(pyglet.window.Window): - def __init__ (self): - super(main, self).__init__(800, 800, fullscreen = False) + def __init__(self) -> None: + super(main, self).__init__(800, 800, fullscreen=False) self.x, self.y = 0, 0 - self.bg = pyglet.sprite.Sprite(pyglet.image.load('background.jpg')) + self.bg = pyglet.sprite.Sprite(pyglet.image.load("background.jpg")) self.sprites = {} self.player = pyglet.media.Player() self.alive = 1 - def on_draw(self): + def on_draw(self) -> None: self.render() - def on_close(self): + def on_close(self) -> None: self.alive = 0 - def on_key_press(self, symbol, modifiers): + def on_key_press(self, symbol, modifiers) -> None: # Do something when a key is pressed? # Pause the audio for instance? # use `if symbol == key.SPACE: ...` @@ -37,13 +38,13 @@ def on_key_press(self, symbol, modifiers): # You could also do a standard input() call and enter a string # on the command line. if symbol == key.ENTER: - self.player.queue(pyglet.media.load('speak.mp3', streaming = False)) + self.player.queue(pyglet.media.load("speak.mp3", streaming=False)) if not self.player.playing: self.player.play() - if symbol == key.ESCAPE: # [ESC] + if symbol == key.ESCAPE: # [ESC] self.alive = 0 - def render(self): + def render(self) -> None: self.clear() self.bg.draw() @@ -54,7 +55,7 @@ def render(self): self.flip() - def run(self): + def run(self) -> None: while self.alive == 1: self.render() @@ -63,7 +64,7 @@ def run(self): # but is required for the GUI to not freeze # event = self.dispatch_events() - self.player.delete() # Free resources. (Not really needed but as an example) + self.player.delete() # Free resources. (Not really needed but as an example) x = main() diff --git a/pyglet_example/using_thread.py b/pyglet_example/using_thread.py index 849763cf6..a5af84548 100644 --- a/pyglet_example/using_thread.py +++ b/pyglet_example/using_thread.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from threading import Thread @@ -10,8 +10,8 @@ import pyglet -def play_song(): - song = pyglet.media.load('speak.mp3') +def play_song() -> None: + song = pyglet.media.load("speak.mp3") song.play() pyglet.app.run() diff --git a/pymem__examples/hello_world.py b/pymem__examples/hello_world.py index c28ba8720..86455892d 100644 --- a/pymem__examples/hello_world.py +++ b/pymem__examples/hello_world.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pymem from pymem import Pymem -pm = Pymem('notepad.exe') -print('Process id: %s' % pm.process_id) +pm = Pymem("notepad.exe") +print("Process id: %s" % pm.process_id) address = pm.allocate(10) -print('Allocated address: %s' % address) +print("Allocated address: %s" % address) pm.write_int(address, 1337) value = pm.read_int(address) -print('Allocated value: %s' % value) +print("Allocated value: %s" % value) pm.free(address) diff --git a/pymem__examples/inject_python_interpreter.py b/pymem__examples/inject_python_interpreter.py index cae468827..2d214f26a 100644 --- a/pymem__examples/inject_python_interpreter.py +++ b/pymem__examples/inject_python_interpreter.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os @@ -43,16 +43,16 @@ # RuntimeError: Could not allocate memory for shellcode -notepad = subprocess.Popen(['notepad.exe']) +notepad = subprocess.Popen(["notepad.exe"]) -pm = Pymem('notepad.exe') +pm = Pymem("notepad.exe") pm.inject_python_interpreter() -filepath = os.path.join(os.path.abspath('.'), 'pymem_injection.txt') +filepath = os.path.join(os.path.abspath("."), "pymem_injection.txt") filepath = filepath.replace("\\", "\\\\") -shellcode = """ -f = open("{}", "w+") +shellcode = f""" +f = open("{filepath}", "w+") f.write("pymem_injection") f.close() -""".format(filepath) +""" pm.inject_python_shellcode(shellcode) notepad.kill() diff --git a/pymem__examples/listing_process_modules.py b/pymem__examples/listing_process_modules.py index 7876d272d..23d089d7e 100644 --- a/pymem__examples/listing_process_modules.py +++ b/pymem__examples/listing_process_modules.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import pymem -pm = pymem.Pymem('python.exe') +pm = pymem.Pymem("python.exe") for module in pm.list_modules(): print(module.name) diff --git "a/pymorphy2__examples/c\320\276\320\263\320\273\320\260\321\201\320\276\320\262\320\260\320\275\320\270\320\265_\321\201\320\273\320\276\320\262_\321\201_\321\207\320\270\321\201\320\273\320\270\321\202\320\265\320\273\321\214\320\275\321\213\320\274\320\270.py" "b/pymorphy2__examples/c\320\276\320\263\320\273\320\260\321\201\320\276\320\262\320\260\320\275\320\270\320\265_\321\201\320\273\320\276\320\262_\321\201_\321\207\320\270\321\201\320\273\320\270\321\202\320\265\320\273\321\214\320\275\321\213\320\274\320\270.py" index 73981ee04..30693d05a 100644 --- "a/pymorphy2__examples/c\320\276\320\263\320\273\320\260\321\201\320\276\320\262\320\260\320\275\320\270\320\265_\321\201\320\273\320\276\320\262_\321\201_\321\207\320\270\321\201\320\273\320\270\321\202\320\265\320\273\321\214\320\275\321\213\320\274\320\270.py" +++ "b/pymorphy2__examples/c\320\276\320\263\320\273\320\260\321\201\320\276\320\262\320\260\320\275\320\270\320\265_\321\201\320\273\320\276\320\262_\321\201_\321\207\320\270\321\201\320\273\320\270\321\202\320\265\320\273\321\214\320\275\321\213\320\274\320\270.py" @@ -1,45 +1,38 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# Для импортирования ascii_table__simple_pretty__format.py -import sys -sys.path.append('..') - -from ascii_table__simple_pretty__format import print_pretty_table - # pip install pymorphy2 import pymorphy2 +# pip install tabulate +from tabulate import tabulate + morph = pymorphy2.MorphAnalyzer() -words = [ - 'программист', - 'ёлка', - 'фрукт', - 'игр', - 'барсук' -] +words = ["программист", "ёлка", "фрукт", "игр", "барсук"] parsed_words = [morph.parse(word)[0] for word in words] -rows = [words] +rows = [] for i in range(5 + 1): row = [] for parsed_word in parsed_words: word = parsed_word.make_agree_with_number(i).word - row.append(f'{i} {word}') + row.append(f"{i} {word}") rows.append(row) -print_pretty_table(rows, align='<') -# программист | ёлка | фрукт | игр | барсук -# ----------------+--------+-----------+--------+----------- -# 0 программистов | 0 ёлок | 0 фруктов | 0 игр | 0 барсуков -# 1 программист | 1 ёлка | 1 фрукт | 1 игры | 1 барсук -# 2 программиста | 2 ёлки | 2 фрукта | 2 игр | 2 барсука -# 3 программиста | 3 ёлки | 3 фрукта | 3 игр | 3 барсука -# 4 программиста | 4 ёлки | 4 фрукта | 4 игр | 4 барсука -# 5 программистов | 5 ёлок | 5 фруктов | 5 игр | 5 барсуков +print(tabulate(rows, headers=words, tablefmt="orgtbl")) +""" +| программист | ёлка | фрукт | игр | барсук | +|-----------------+--------+-----------+--------+------------| +| 0 программистов | 0 ёлок | 0 фруктов | 0 игр | 0 барсуков | +| 1 программист | 1 ёлка | 1 фрукт | 1 игры | 1 барсук | +| 2 программиста | 2 ёлки | 2 фрукта | 2 игр | 2 барсука | +| 3 программиста | 3 ёлки | 3 фрукта | 3 игр | 3 барсука | +| 4 программиста | 4 ёлки | 4 фрукта | 4 игр | 4 барсука | +| 5 программистов | 5 ёлок | 5 фруктов | 5 игр | 5 барсуков | +""" diff --git a/pymorphy2__examples/find_gender_by_first_name.py b/pymorphy2__examples/find_gender_by_first_name.py index 3e88ba131..44d59d310 100644 --- a/pymorphy2__examples/find_gender_by_first_name.py +++ b/pymorphy2__examples/find_gender_by_first_name.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pymorphy2 @@ -9,15 +9,23 @@ name_list = [ - 'Константин', 'Виктор', 'Любовь', 'Дамир', 'Венера', - 'Таисия', 'Алёна', 'Евгений', 'Егор', 'Никита' + "Константин", + "Виктор", + "Любовь", + "Дамир", + "Венера", + "Таисия", + "Алёна", + "Евгений", + "Егор", + "Никита", ] morph = pymorphy2.MorphAnalyzer() for name in name_list: parsed_word = morph.parse(name)[0] - print('{:<15} {}'.format(name, parsed_word.tag.gender)) + print(f"{name:<15} {parsed_word.tag.gender}") # Константин masc # Виктор masc diff --git a/pymorphy2__examples/get_tokens.py b/pymorphy2__examples/get_tokens.py index 746223e2b..518e1ec40 100644 --- a/pymorphy2__examples/get_tokens.py +++ b/pymorphy2__examples/get_tokens.py @@ -1,27 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import List - # pip install pymorphy2 import pymorphy2 from pymorphy2.tokenizers import simple_word_tokenize + morph = pymorphy2.MorphAnalyzer() -def get_tokens(text: str, ignore_punctuations=False) -> List[pymorphy2.analyzer.Parse]: +def get_tokens(text: str, ignore_punctuations=False) -> list[pymorphy2.analyzer.Parse]: tokens = [morph.parse(word)[0] for word in simple_word_tokenize(text)] if ignore_punctuations: - tokens = [token for token in tokens if 'PNCT' not in token.tag] + tokens = [token for token in tokens if "PNCT" not in token.tag] return tokens -if __name__ == '__main__': - text = 'чё за дом! ни конфет, ни печенек, пришлось жрать СОЛЁНЫЙ огурец ((' +if __name__ == "__main__": + text = "чё за дом! ни конфет, ни печенек, пришлось жрать СОЛЁНЫЙ огурец ((" for token in get_tokens(text, ignore_punctuations=True): print(token.word, token.normal_form) diff --git a/pymorphy2__examples/normal_form.py b/pymorphy2__examples/normal_form.py index a0ab96666..ac64bcbca 100644 --- a/pymorphy2__examples/normal_form.py +++ b/pymorphy2__examples/normal_form.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pymorphy2 import pymorphy2 + + morph = pymorphy2.MorphAnalyzer() @@ -13,7 +15,7 @@ def get_normal_form(word: str) -> str: return morph.parse(word)[0].normal_form -if __name__ == '__main__': - print(get_normal_form('ИВАНОВА')) # иванов - print(get_normal_form('Иванова')) # иванов - print(get_normal_form('Иванов')) # иванов +if __name__ == "__main__": + print(get_normal_form("ИВАНОВА")) # иванов + print(get_normal_form("Иванова")) # иванов + print(get_normal_form("Иванов")) # иванов diff --git a/pymorphy2__examples/parse_word.py b/pymorphy2__examples/parse_word.py index d79e7f831..b28bea875 100644 --- a/pymorphy2__examples/parse_word.py +++ b/pymorphy2__examples/parse_word.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install pymorphy2 @@ -10,9 +10,7 @@ morph = pymorphy2.MorphAnalyzer() -words = [ - "Думала", "Подумала", "Согласен" -] +words = ["Думала", "Подумала", "Согласен"] for word in words: print(repr(word)) diff --git "a/pymorphy2__examples/\320\276\320\277\321\200\320\265\320\264\320\265\320\273\320\265\320\275\320\270\320\265_\320\272\320\276\320\274\320\274\320\265\320\275\321\202\320\260\321\200\320\270\320\265\320\262_\320\266\320\265\320\275\321\211\320\270\320\275/main.py" "b/pymorphy2__examples/\320\276\320\277\321\200\320\265\320\264\320\265\320\273\320\265\320\275\320\270\320\265_\320\272\320\276\320\274\320\274\320\265\320\275\321\202\320\260\321\200\320\270\320\265\320\262_\320\266\320\265\320\275\321\211\320\270\320\275/main.py" index 6acb45ef5..682f49b82 100644 --- "a/pymorphy2__examples/\320\276\320\277\321\200\320\265\320\264\320\265\320\273\320\265\320\275\320\270\320\265_\320\272\320\276\320\274\320\274\320\265\320\275\321\202\320\260\321\200\320\270\320\265\320\262_\320\266\320\265\320\275\321\211\320\270\320\275/main.py" +++ "b/pymorphy2__examples/\320\276\320\277\321\200\320\265\320\264\320\265\320\273\320\265\320\275\320\270\320\265_\320\272\320\276\320\274\320\274\320\265\320\275\321\202\320\260\321\200\320\270\320\265\320\262_\320\266\320\265\320\275\321\211\320\270\320\275/main.py" @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # pip install nltk @@ -19,35 +19,35 @@ def is_ADJS_sing_femn(parsed: pymorphy2.analyzer.Parse) -> bool: """ Имя прилагательное (краткое) + единственное число + женский род """ - return {'ADJS', 'sing', 'femn'} in parsed.tag + return {"ADJS", "sing", "femn"} in parsed.tag def is_ADJS_sing_masc(parsed: pymorphy2.analyzer.Parse) -> bool: """ Имя прилагательное (краткое) + единственное число + мужской род """ - return {'ADJS', 'sing', 'masc'} in parsed.tag + return {"ADJS", "sing", "masc"} in parsed.tag def is_VERB_sing_femn(parsed: pymorphy2.analyzer.Parse) -> bool: """ Глагол (личная форма) + единственное число + женский род """ - return {'VERB', 'sing', 'femn'} in parsed.tag + return {"VERB", "sing", "femn"} in parsed.tag def is_VERB_sing_masc(parsed: pymorphy2.analyzer.Parse) -> bool: """ Глагол (личная форма) + единственное число + мужской род """ - return {'VERB', 'sing', 'masc'} in parsed.tag + return {"VERB", "sing", "masc"} in parsed.tag def is_NPRO_1per_sing(parsed: pymorphy2.analyzer.Parse) -> bool: """ Местоимение-существительное + 1 лицо + единственное число """ - return {'NPRO', '1per', 'sing'} in parsed.tag + return {"NPRO", "1per", "sing"} in parsed.tag LOG_DEBUG = False @@ -58,11 +58,11 @@ def is_NPRO_1per_sing(parsed: pymorphy2.analyzer.Parse) -> bool: def is_femn(text: str) -> bool: for line in text.splitlines(): - LOG_DEBUG and print(f'[#] Comment: {line!r}') + LOG_DEBUG and print(f"[#] Comment: {line!r}") # Перебор предложений - for sent in nltk.sent_tokenize(line, language='russian'): - LOG_DEBUG and print(f'[#] Comment part: {sent!r}') + for sent in nltk.sent_tokenize(line, language="russian"): + LOG_DEBUG and print(f"[#] Comment part: {sent!r}") words = nltk.word_tokenize(sent) parsed_words = [morph.parse(word)[0] for word in words] @@ -76,11 +76,11 @@ def is_femn(text: str) -> bool: has_NPRO_1per_sing = False - LOG_DEBUG and print(f'[#] ({len(words)}): {words}') - LOG_DEBUG and print(f'[#] ({len(parsed_words)}):') + LOG_DEBUG and print(f"[#] ({len(words)}): {words}") + LOG_DEBUG and print(f"[#] ({len(parsed_words)}):") for parsed in parsed_words: - LOG_DEBUG and print(f'[#] {parsed.word} - {str(parsed.tag)!r}') + LOG_DEBUG and print(f"[#] {parsed.word} - {str(parsed.tag)!r}") if is_NPRO_1per_sing(parsed): has_NPRO_1per_sing = True @@ -99,26 +99,28 @@ def is_femn(text: str) -> bool: return False -if __name__ == '__main__': +if __name__ == "__main__": import json - with open('comments.json', encoding='utf-8') as f: + with open("comments.json", encoding="utf-8") as f: data = json.load(f) - comments = [(x['text'], x['expected']) for x in data] + comments = [(x["text"], x["expected"]) for x in data] matches = 0 total = len(comments) for i, (text, expected) in enumerate(comments, 1): has_female = is_femn(text) - match = has_female == (expected == 'female') + match = has_female == (expected == "female") matches += match - print(f"{i}. {text!r}" - f"\n [{'+' if match else '-'}] " - f"Expected={expected}, " - f"actual={'female' if has_female else 'male'}") - print('-' * 100) + print( + f"{i}. {text!r}" + f"\n [{'+' if match else '-'}] " + f"Expected={expected}, " + f"actual={'female' if has_female else 'male'}" + ) + print("-" * 100) - print(f'Total: {matches} / {total}') + print(f"Total: {matches} / {total}") diff --git a/pynput__examples/console_board_game.py b/pynput__examples/console_board_game.py new file mode 100644 index 000000000..0bc7ec743 --- /dev/null +++ b/pynput__examples/console_board_game.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from dataclasses import dataclass +from random import randint + +import time +import os + +# pip install pynput-1.8.1 +from pynput import keyboard + + +BOARD_TEMPLATE = """ +############################## +#G # +######## ####### ####### # +# GG # # +# GG # # G G G G G G # # +# GG # ################ # +# GG # # +# # ###### ### #### # +# #GG # # # #GG# # +# #GG # # @ # +############################## +""".strip() + + +def clear() -> None: + os.system("clear" if os.name == "posix" else "cls") + + +@dataclass +class GameObject: + pos_x: int = 0 + pos_y: int = 0 + + icon: str = " " + + +@dataclass +class Wall(GameObject): + icon: str = "#" + + +@dataclass +class Gold(GameObject): + value: int = 0 + icon: str = "G" + + +@dataclass +class Hero(GameObject): + gold: int = 0 + icon: str = "@" + + +class Game: + def __init__(self) -> None: + self.hero: Hero = Hero() + self.board: list[list[GameObject]] = [] + self.is_active: bool = False + + self.listener = keyboard.Listener(on_release=self._on_release) + + def _on_release(self, key) -> None: + self.logic(key) + + def do_step(self) -> bool: + self.draw() + + return self.is_active + + def logic(self, key) -> None: + pos_x, pos_y = self.hero.pos_x, self.hero.pos_y + + if key == keyboard.Key.esc: + self.finish() + return + + elif key == keyboard.KeyCode.from_char("w") or key == keyboard.Key.up: + self.hero.pos_y -= 1 + + elif key == keyboard.KeyCode.from_char("s") or key == keyboard.Key.down: + self.hero.pos_y += 1 + + elif key == keyboard.KeyCode.from_char("d") or key == keyboard.Key.right: + self.hero.pos_x += 1 + + elif key == keyboard.KeyCode.from_char("a") or key == keyboard.Key.left: + self.hero.pos_x -= 1 + + if (pos_x, pos_y) != (self.hero.pos_x, self.hero.pos_y): + game_object: GameObject = self.board[self.hero.pos_y][self.hero.pos_x] + + if isinstance(game_object, Wall): # Если уперлись в стену + self.hero.pos_x, self.hero.pos_y = pos_x, pos_y + return + + if isinstance(game_object, Gold): + self.hero.gold += game_object.value + + self.board[self.hero.pos_y][self.hero.pos_x] = self.hero # Рисуем героя + self.board[pos_y][pos_x] = GameObject( + pos_x, pos_y + ) # Старое место стало пустым + + def draw(self) -> None: + clear() + + self.draw_board() + self.draw_game_info() + + def draw_board(self) -> None: + for row in self.board: + print("".join(game_object.icon for game_object in row)) + + def draw_game_info(self) -> None: + print( + f"Hero: position: {self.hero.pos_x}x{self.hero.pos_y}, gold: {self.hero.gold}" + ) + + def start(self, board_template: str) -> None: + self.is_active = True + self.board.clear() + self.listener.start() + + for y, lines in enumerate(board_template.splitlines()): + self.board.append([]) + + for x, value in enumerate(lines): + match value: + case "@": + self.hero.pos_x = x + self.hero.pos_y = y + game_object = self.hero + + case "G": + game_object = Gold( + pos_x=x, + pos_y=y, + value=randint(1, 100), + ) + + case "#": + game_object = Wall(pos_x=x, pos_y=y) + + case _: + game_object = GameObject( + pos_x=x, + pos_y=y, + ) + + self.board[-1].append(game_object) + + def finish(self) -> None: + self.is_active = False + self.listener.stop() + + +if __name__ == "__main__": + game = Game() + game.start(board_template=BOARD_TEMPLATE) + + while game.do_step(): + time.sleep(0.100) diff --git a/pynput__examples/global_hot_key.py b/pynput__examples/global_hot_key.py new file mode 100644 index 000000000..fe4c26a16 --- /dev/null +++ b/pynput__examples/global_hot_key.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pynput-1.8.1 +from pynput.keyboard import Key, Listener + +# pip install pyscreenshot +import pyscreenshot as ImageGrab + + +def on_release(key) -> None: + if key == Key.f7: + print("screenshot") + + # Fullscreen + im = ImageGrab.grab() + im.save("screenshot.png") + im.show() + + +with Listener(on_release=on_release) as listener: + listener.join() diff --git a/pynput__examples/hello_world.py b/pynput__examples/hello_world.py deleted file mode 100644 index fe6cadc79..000000000 --- a/pynput__examples/hello_world.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# pip install pynput -from pynput.keyboard import Key, Listener - - -def on_press(key): - print('{0} pressed'.format(key)) - - -def on_release(key): - print('{0} release'.format(key)) - - if key == Key.esc: - # Stop listener - return False - -# Collect events until released -with Listener(on_press=on_press, on_release=on_release) as listener: - listener.join() diff --git a/pynput__examples/key_listener.py b/pynput__examples/key_listener.py new file mode 100644 index 000000000..e408ff0fa --- /dev/null +++ b/pynput__examples/key_listener.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pynput-1.8.1 +from pynput.keyboard import Key, Listener + + +def on_press(key) -> None: + print(f"{key} pressed") + + +def on_release(key) -> bool | None: + print(f"{key} release") + + if key == Key.esc: + # Stop listener + return False + + +# Collect events until released +with Listener(on_press=on_press, on_release=on_release) as listener: + listener.join() diff --git a/pynput__examples/media_stop.py b/pynput__examples/media_stop.py new file mode 100644 index 000000000..404ae32fd --- /dev/null +++ b/pynput__examples/media_stop.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pynput-1.8.1 +from pynput.keyboard import Key, Controller + + +keyboard = Controller() + +print("Attempting to stop media playback using pynput...") + +keyboard.press(Key.media_stop) +keyboard.release(Key.media_stop) + +print("Media stop command sent via pynput.") diff --git a/pynput__examples/pynput__global_hot_key.py b/pynput__examples/pynput__global_hot_key.py deleted file mode 100644 index 75b02f25f..000000000 --- a/pynput__examples/pynput__global_hot_key.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# pip install pynput -from pynput.keyboard import Key, Listener - - -def on_release(key): - if key == Key.f7: - print('screenshot') - - # Fullscreen - import pyscreenshot as ImageGrab - im = ImageGrab.grab() - im.save('screenshot.png') - im.show() - - -with Listener(on_release=on_release) as listener: - listener.join() diff --git a/pynput__examples/pynput__key_listener.py b/pynput__examples/pynput__key_listener.py deleted file mode 100644 index 5f9972ea2..000000000 --- a/pynput__examples/pynput__key_listener.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -def on_press(key): - print('on_press', str(key), key, type(key)) - - -def on_release(key): - print('on_release', str(key), key, type(key)) - - -# pip install pynput -from pynput import keyboard -with keyboard.Listener(on_press=on_press, on_release=on_release) as listener: - listener.join() diff --git a/pyqrcode__examples/hello_world.py b/pyqrcode__examples/hello_world.py deleted file mode 100644 index 317736d8e..000000000 --- a/pyqrcode__examples/hello_world.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# SOURCE: https://pythonhosted.org/PyQRCode/ - - -# pip install pyqrcode -# pip install pypng -import pyqrcode - - -qr_code = pyqrcode.create('http://uca.edu') -qr_code.png('img.png') - -qr_code = pyqrcode.create('Hello World!') -qr_code.png('img_hello.png') - -qr_code.png('img_hello__scale=6.png', scale=6) diff --git a/pyscreeze__examples/win_calc_clicker/README.md b/pyscreeze__examples/win_calc_clicker/README.md new file mode 100644 index 000000000..de669a9bf --- /dev/null +++ b/pyscreeze__examples/win_calc_clicker/README.md @@ -0,0 +1,2 @@ +## main.py +![](screenshots/screenshot.gif) diff --git a/pyscreeze__examples/win_calc_clicker/buttons/+.png b/pyscreeze__examples/win_calc_clicker/buttons/+.png new file mode 100644 index 000000000..084c9c285 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/+.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/0.png b/pyscreeze__examples/win_calc_clicker/buttons/0.png new file mode 100644 index 000000000..12e8b4f96 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/0.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/1.png b/pyscreeze__examples/win_calc_clicker/buttons/1.png new file mode 100644 index 000000000..63521ac03 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/1.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/2.png b/pyscreeze__examples/win_calc_clicker/buttons/2.png new file mode 100644 index 000000000..a4b2586f3 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/2.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/3.png b/pyscreeze__examples/win_calc_clicker/buttons/3.png new file mode 100644 index 000000000..ded0748f5 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/3.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/4.png b/pyscreeze__examples/win_calc_clicker/buttons/4.png new file mode 100644 index 000000000..87c7d85b2 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/4.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/5.png b/pyscreeze__examples/win_calc_clicker/buttons/5.png new file mode 100644 index 000000000..fc7991e11 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/5.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/6.png b/pyscreeze__examples/win_calc_clicker/buttons/6.png new file mode 100644 index 000000000..22936758e Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/6.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/7.png b/pyscreeze__examples/win_calc_clicker/buttons/7.png new file mode 100644 index 000000000..483623193 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/7.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/8.png b/pyscreeze__examples/win_calc_clicker/buttons/8.png new file mode 100644 index 000000000..188390ada Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/8.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/9.png b/pyscreeze__examples/win_calc_clicker/buttons/9.png new file mode 100644 index 000000000..ab5ba3774 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/9.png differ diff --git a/pyscreeze__examples/win_calc_clicker/buttons/=.png b/pyscreeze__examples/win_calc_clicker/buttons/=.png new file mode 100644 index 000000000..69088492a Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/buttons/=.png differ diff --git a/pyscreeze__examples/win_calc_clicker/main.py b/pyscreeze__examples/win_calc_clicker/main.py new file mode 100644 index 000000000..a522ead8a --- /dev/null +++ b/pyscreeze__examples/win_calc_clicker/main.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os +import time +import win32api +import win32con + +from pathlib import Path + +# pip install pyscreeze +import pyscreeze + + +DIR = Path(__file__).parent.resolve() +DIR_BUTTONS = DIR / "buttons" + + +def click(x, y, sleep_secs: float = 0.01) -> None: + win32api.SetCursorPos((x, y)) + win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0) + win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x, y, 0, 0) + + time.sleep(sleep_secs) + + +BUTTON_BY_POSITION = dict() + + +def init_button_positions(grayscale: bool = True) -> None: + BUTTON_BY_POSITION.clear() + + for path in DIR_BUTTONS.glob("*.png"): + button = path.stem + + position = pyscreeze.locateCenterOnScreen(str(path), grayscale=grayscale) + if not position: + print(f"Position of button {button!r} not found!") + continue + + BUTTON_BY_POSITION[button] = position + + +def go(expression: str) -> None: + init_button_positions() + + for button in expression: + if not button.strip(): + continue + + if button not in BUTTON_BY_POSITION: + print(f"Unknown button {button!r}!") + continue + + x, y = BUTTON_BY_POSITION[button] + print(f"Click on {button!r} at {x}x{y}") + + click(x, y) + + +def show_test_calc() -> None: + os.startfile("calc.exe") + time.sleep(1) + + +if __name__ == "__main__": + show_test_calc() + + expression = "1234 + 222 + 3214 + 1111 + 1234567890 + 222 = " + go(expression) diff --git a/pyscreeze__examples/win_calc_clicker/screenshots/screenshot.gif b/pyscreeze__examples/win_calc_clicker/screenshots/screenshot.gif new file mode 100644 index 000000000..a7c1b0f92 Binary files /dev/null and b/pyscreeze__examples/win_calc_clicker/screenshots/screenshot.gif differ diff --git a/pysimplegui__examples/generation_random.py b/pysimplegui__examples/generation_random.py index 79cd59736..414e2973f 100644 --- a/pysimplegui__examples/generation_random.py +++ b/pysimplegui__examples/generation_random.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from random import randint @@ -10,20 +10,20 @@ import PySimpleGUI as sg -sg.theme('SystemDefault') +sg.theme("SystemDefault") layout = [ - [sg.Text('Number of digits'), sg.Input(default_text='5', key='ND')], - [sg.Text('Minimum'), sg.Input(default_text='1', key='Min')], - [sg.Text('Maximum'), sg.Input(default_text='9', key='Max')], - [sg.Text(' ', key='OUT')], - [sg.Button('Generate'), sg.Button('Clear')] + [sg.Text("Number of digits"), sg.Input(default_text="5", key="ND")], + [sg.Text("Minimum"), sg.Input(default_text="1", key="Min")], + [sg.Text("Maximum"), sg.Input(default_text="9", key="Max")], + [sg.Text(" ", key="OUT")], + [sg.Button("Generate", key="button_generate"), sg.Button("Clear")], ] -window = sg.Window('Generator', layout) +window = sg.Window("Generator", layout) def generate(number: int, min_number: int, max_number: int) -> str: - return ''.join(str(randint(min_number, max_number)) for _ in range(number)) + return "".join(str(randint(min_number, max_number)) for _ in range(number)) while True: @@ -31,14 +31,18 @@ def generate(number: int, min_number: int, max_number: int) -> str: if event is None or event == sg.WIN_CLOSED: break - if event == 'Generate': - number, min_number, max_number = int(values['ND']), int(values['Min']), int(values['Max']) + if event == "button_generate": + number, min_number, max_number = ( + int(values["ND"]), + int(values["Min"]), + int(values["Max"]), + ) text = generate(number, min_number, max_number) print(text) - window['OUT'].update(f'Result: {text}') + window["OUT"].update(f"Result: {text}") - if event == 'Clear': - window.find_element('ND').update('') - window.find_element('Min').update('') - window.find_element('Max').update('') - window.find_element('OUT').update('') + if event == "Clear": + window.find_element("ND").update("") + window.find_element("Min").update("") + window.find_element("Max").update("") + window.find_element("OUT").update("") diff --git a/pytesseract__examples__image_to_text/GTA/main.py b/pytesseract__examples__image_to_text/GTA/main.py index 6868c0d18..54791cd2b 100644 --- a/pytesseract__examples__image_to_text/GTA/main.py +++ b/pytesseract__examples__image_to_text/GTA/main.py @@ -1,22 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -# pip install pillow -from PIL import Image - # pip install pytesseract # tesseract.exe from https://github.com/UB-Mannheim/tesseract/wiki import pytesseract -pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' + +# pip install pillow +from PIL import Image + + +pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" -file_name = 'RuFjPBg.png' +file_name = "RuFjPBg.png" img = Image.open(file_name) -text = pytesseract.image_to_string(img, lang='eng') +text = pytesseract.image_to_string(img, lang="eng") print(repr(text)) # '.\nCheat activated' diff --git a/pytesseract__examples__image_to_text/hello_world.py b/pytesseract__examples__image_to_text/hello_world.py index 4cfec19f7..722288cc7 100644 --- a/pytesseract__examples__image_to_text/hello_world.py +++ b/pytesseract__examples__image_to_text/hello_world.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/madmaze/pytesseract @@ -9,20 +9,22 @@ import re -# pip install pillow -from PIL import Image - # pip install pytesseract # Tesseract.exe from https://github.com/UB-Mannheim/tesseract/wiki import pytesseract -pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' + +# pip install pillow +from PIL import Image + + +pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" # Simple image to string -img = Image.open('test.jpg') -text = pytesseract.image_to_string(img, lang='eng') +img = Image.open("test.jpg") +text = pytesseract.image_to_string(img, lang="eng") -text = re.sub(r'(\s){2,}', '\1', text) +text = re.sub(r"(\s){2,}", "\1", text) print(text) # At this Time. two Great Empires struggled # for Dominion over Ivalice:Archadia in the East. Rozarria. the West. diff --git a/pytesseract__examples__image_to_text/hello_world__with__googletrans.py b/pytesseract__examples__image_to_text/hello_world__with__googletrans.py index 2dbbb9d36..8e2085af1 100644 --- a/pytesseract__examples__image_to_text/hello_world__with__googletrans.py +++ b/pytesseract__examples__image_to_text/hello_world__with__googletrans.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/madmaze/pytesseract @@ -9,23 +9,25 @@ import re +# pip install pytesseract +# Tesseract.exe from https://github.com/UB-Mannheim/tesseract/wiki +import pytesseract + # pip install googletrans from googletrans import Translator # pip install pillow from PIL import Image -# pip install pytesseract -# Tesseract.exe from https://github.com/UB-Mannheim/tesseract/wiki -import pytesseract -pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' + +pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" # Simple image to string -img = Image.open('test.jpg') -text = pytesseract.image_to_string(img, lang='eng') +img = Image.open("test.jpg") +text = pytesseract.image_to_string(img, lang="eng") -text = re.sub(r'(\s){2,}', '\1', text) +text = re.sub(r"(\s){2,}", "\1", text) print(text) # At this Time. two Great Empires struggled # for Dominion over Ivalice:Archadia in the East. Rozarria. the West. @@ -35,17 +37,13 @@ # tolled the Destruction of the greater # part of Dalmasca’'s f orces. -print('', '-' * 100, '', sep='\n') +print("", "-" * 100, "", sep="\n") -from_lang = 'en' -to_lang = 'ru' +from_lang = "en" +to_lang = "ru" translator = Translator() -translation = translator.translate( - text, - src=from_lang, - dest=to_lang -).text +translation = translator.translate(text, src=from_lang, dest=to_lang).text print(translation) # В это время. два Великие Империи боролись # для Dominion над Ивалисом: Archadia на Востоке. Rozarria. Запад. diff --git a/python_implementation.py b/python_implementation.py index dfa994cba..659662411 100644 --- a/python_implementation.py +++ b/python_implementation.py @@ -1,8 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import platform + + print(platform.python_implementation()) +# CPython diff --git a/python_interpreter_info.py b/python_interpreter_info.py index 29363df55..8a2406dd9 100644 --- a/python_interpreter_info.py +++ b/python_interpreter_info.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import platform -print('Implementation:', platform.python_implementation()) -print('Version:', platform.python_version()) -print('Compiler:', platform.python_compiler()) - import sys -print('Executable:', sys.executable) + + +print("Implementation:", platform.python_implementation()) +print("Version:", platform.python_version()) +print("Compiler:", platform.python_compiler()) + +print("Executable:", sys.executable) diff --git a/python_object_to_json.py b/python_object_to_json.py index b0b7a9bfe..339d4b0dc 100644 --- a/python_object_to_json.py +++ b/python_object_to_json.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" class SubField: - def __init__(self): + def __init__(self) -> None: self.flag = True class Field: - def __init__(self, tag1, tag2, sub_field_flag=True): + def __init__(self, tag1, tag2, sub_field_flag=True) -> None: self.tag1 = tag1 self.tag2 = tag2 @@ -20,17 +20,17 @@ def __init__(self, tag1, tag2, sub_field_flag=True): class Object: a = 0 - b = '123' + b = "123" - def __init__(self): + def __init__(self) -> None: self.c = 3 self.items = [1, 2, 3, 4] self.maps = { - 'is': True, - 'not': 0, + "is": True, + "not": 0, } - self.field = Field('abc', 'tag2') + self.field = Field("abc", "tag2") self.field_2 = Field(777, False, sub_field_flag=False) @@ -39,11 +39,11 @@ def object_to_dict(object): fields.update(object.__class__.__dict__) fields.update(object.__dict__) - fields = dict(filter(lambda x: not x[0].startswith('_'), fields.items())) + fields = dict(filter(lambda x: not x[0].startswith("_"), fields.items())) new_fields = dict() for k, v in fields.items(): - if hasattr(v, '__dict__'): + if hasattr(v, "__dict__"): v = object_to_dict(v) new_fields[k] = v @@ -51,11 +51,12 @@ def object_to_dict(object): return new_fields -if __name__ == '__main__': +if __name__ == "__main__": obj = Object() fields = object_to_dict(obj) print(fields) print() import json + print(json.dumps(fields, ensure_ascii=False, indent=4, sort_keys=True)) diff --git a/pythonnet__examples/OpenHardwareMonitorLib__Sensor_Temperature/main.py b/pythonnet__examples/OpenHardwareMonitorLib__Sensor_Temperature/main.py index e67b33ea0..c30092c25 100644 --- a/pythonnet__examples/OpenHardwareMonitorLib__Sensor_Temperature/main.py +++ b/pythonnet__examples/OpenHardwareMonitorLib__Sensor_Temperature/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # NOTE: Run as administrator @@ -12,22 +12,41 @@ # pip install pythonnet import clr +from OpenHardwareMonitor import Hardware + openhardwaremonitor_hwtypes = [ - 'Mainboard', 'SuperIO', 'CPU', 'RAM', 'GpuNvidia', 'GpuAti', 'TBalancer', 'Heatmaster', 'HDD' + "Mainboard", + "SuperIO", + "CPU", + "RAM", + "GpuNvidia", + "GpuAti", + "TBalancer", + "Heatmaster", + "HDD", ] openhardwaremonitor_sensortypes = [ - 'Voltage', 'Clock', 'Temperature', 'Load', 'Fan', 'Flow', 'Control', 'Level', - 'Factor', 'Power', 'Data', 'SmallData' + "Voltage", + "Clock", + "Temperature", + "Load", + "Fan", + "Flow", + "Control", + "Level", + "Factor", + "Power", + "Data", + "SmallData", ] def initialize_openhardwaremonitor(): DIR = os.path.abspath(os.path.dirname(__file__)) - dll_file_name = DIR + R'\OpenHardwareMonitorLib.dll' + dll_file_name = DIR + R"\OpenHardwareMonitorLib.dll" clr.AddReference(dll_file_name) - from OpenHardwareMonitor import Hardware handle = Hardware.Computer() handle.MainboardEnabled = True handle.CPUEnabled = True @@ -38,7 +57,7 @@ def initialize_openhardwaremonitor(): return handle -def fetch_stats(handle): +def fetch_stats(handle) -> None: for i in handle.Hardware: i.Update() @@ -51,15 +70,17 @@ def fetch_stats(handle): parse_sensor(subsensor) -def parse_sensor(sensor): +def parse_sensor(sensor) -> None: if sensor.Value is None: return # If SensorType is Temperature - if sensor.SensorType == openhardwaremonitor_sensortypes.index('Temperature'): + if sensor.SensorType == openhardwaremonitor_sensortypes.index("Temperature"): type_name = openhardwaremonitor_hwtypes[sensor.Hardware.HardwareType] - print(f" {type_name}. {sensor.Hardware.Name!r} " - f"Temperature Sensor #{sensor.Index} {sensor.Name} - {sensor.Value}°C") + print( + f" {type_name}. {sensor.Hardware.Name!r} " + f"Temperature Sensor #{sensor.Index} {sensor.Name} - {sensor.Value}°C" + ) if __name__ == "__main__": diff --git a/pytube__examples/main.py b/pytube__examples/main.py index a71e2b421..5edc52193 100644 --- a/pytube__examples/main.py +++ b/pytube__examples/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -10,20 +10,20 @@ from pytube import YouTube -url = 'https://www.youtube.com/watch?v=90KZnwrVMgY' +url = "https://www.youtube.com/watch?v=90KZnwrVMgY" yt = YouTube(url) -print(f'Download video {yt.title!r}: {url}') +print(f"Download video {yt.title!r}: {url}") # Атрибут progressive=True нужен, чтобы получить поток с видео и аудио в одном файле -streams = yt.streams\ - .filter(progressive=True, file_extension='mp4', resolution='720p')\ - .order_by('resolution') +streams = yt.streams.filter( + progressive=True, file_extension="mp4", resolution="720p" +).order_by("resolution") if not streams: - print('Not found mp4 with 720p!') + print("Not found mp4 with 720p!") sys.exit() video = streams[-1] -print('Stream url:', video.url) +print("Stream url:", video.url) video.download() diff --git a/pytz_vs_zoneinfo__examples/dt_with_tz_to_utc__pytz.py b/pytz_vs_zoneinfo__examples/dt_with_tz_to_utc__pytz.py new file mode 100644 index 000000000..c10204f9a --- /dev/null +++ b/pytz_vs_zoneinfo__examples/dt_with_tz_to_utc__pytz.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime + +# pip install pytz==2025.2 +import pytz + + +tz_moscow = pytz.timezone("Europe/Moscow") +tz_etc_0300 = pytz.timezone("Etc/GMT-3") +tz_offset_0300 = pytz.FixedOffset(offset=3 * 60) +tz_offset_neg0300 = pytz.FixedOffset(offset=-3 * 60) +tz_utc = pytz.timezone("UTC") + +dt = datetime(year=2025, month=1, day=1, hour=12, minute=0, second=0) +print(dt) +# 2025-01-01 12:00:00 + +print() + +dt_moscow = dt.replace(tzinfo=tz_moscow) +print(dt_moscow) +print(dt_moscow.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+02:30 +# 2025-01-01 09:30:00 + +print() + +dt_etc_0300 = dt.replace(tzinfo=tz_etc_0300) +print(dt_etc_0300) +print(dt_etc_0300.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+03:00 +# 2025-01-01 09:00:00 + +print() + +dt_offset_0300 = dt.replace(tzinfo=tz_offset_0300) +print(dt_offset_0300) +print(dt_offset_0300.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+03:00 +# 2025-01-01 09:00:00 + +print() + +dt_offset_neg0300 = dt.replace(tzinfo=tz_offset_neg0300) +print(dt_offset_neg0300) +print(dt_offset_neg0300.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00-03:00 +# 2025-01-01 15:00:00 + +print() + +dt_utc = dt.replace(tzinfo=tz_utc) +print(dt_utc) +print(dt_utc.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+00:00 +# 2025-01-01 12:00:00 diff --git a/pytz_vs_zoneinfo__examples/dt_with_tz_to_utc__zoneinfo.py b/pytz_vs_zoneinfo__examples/dt_with_tz_to_utc__zoneinfo.py new file mode 100644 index 000000000..e5e761c72 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/dt_with_tz_to_utc__zoneinfo.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import zoneinfo +from datetime import datetime, timedelta, timezone + + +tz_moscow = zoneinfo.ZoneInfo("Europe/Moscow") +tz_etc_0300 = zoneinfo.ZoneInfo("Etc/GMT-3") +tz_offset_0300 = timezone(offset=timedelta(minutes=3 * 60)) +tz_offset_neg0300 = timezone(offset=timedelta(minutes=-3 * 60)) +tz_utc = zoneinfo.ZoneInfo("UTC") + +dt = datetime(year=2025, month=1, day=1, hour=12, minute=0, second=0) +print(dt) +# 2025-01-01 12:00:00 + +print() + +dt_moscow = dt.replace(tzinfo=tz_moscow) +print(dt_moscow) +print(dt_moscow.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+03:00 +# 2025-01-01 09:00:00 + +print() + +dt_etc_0300 = dt.replace(tzinfo=tz_etc_0300) +print(dt_etc_0300) +print(dt_etc_0300.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+03:00 +# 2025-01-01 09:00:00 + +print() + +dt_offset_0300 = dt.replace(tzinfo=tz_offset_0300) +print(dt_offset_0300) +print(dt_offset_0300.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+03:00 +# 2025-01-01 09:00:00 + +print() + +dt_offset_neg0300 = dt.replace(tzinfo=tz_offset_neg0300) +print(dt_offset_neg0300) +print(dt_offset_neg0300.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00-03:00 +# 2025-01-01 15:00:00 + +print() + +dt_utc = dt.replace(tzinfo=tz_utc) +print(dt_utc) +print(dt_utc.astimezone(tz_utc).replace(tzinfo=None)) +# 2025-01-01 12:00:00+00:00 +# 2025-01-01 12:00:00 diff --git a/pytz_vs_zoneinfo__examples/get_offset__pytz.py b/pytz_vs_zoneinfo__examples/get_offset__pytz.py new file mode 100644 index 000000000..2bee60209 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_offset__pytz.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime + +# pip install pytz==2025.2 +import pytz + + +tz_moscow = pytz.timezone("Europe/Moscow") +print(tz_moscow) +# Europe/Moscow + +tz_etc_0300 = pytz.timezone("Etc/GMT-3") +print(tz_etc_0300) +# Etc/GMT-3 + +tz_offset_0300 = pytz.FixedOffset(offset=3 * 60) +print(tz_offset_0300) +# pytz.FixedOffset(180) + +tz_utc = pytz.timezone("UTC") +print(tz_utc) +# UTC + +print() + +dt = datetime(year=2025, month=1, day=1, hour=12, minute=0, second=0) + +print(dt.astimezone(tz_moscow).strftime("%z")) +print(dt.astimezone(tz_etc_0300).strftime("%z")) +print(dt.astimezone(tz_offset_0300).strftime("%z")) +print(dt.astimezone(tz_utc).strftime("%z")) +# +0300 +# +0300 +# +0300 +# +0000 diff --git a/pytz_vs_zoneinfo__examples/get_offset__zoneinfo.py b/pytz_vs_zoneinfo__examples/get_offset__zoneinfo.py new file mode 100644 index 000000000..7c4f3c6f3 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_offset__zoneinfo.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import zoneinfo +from datetime import datetime, timedelta, timezone + + +tz_moscow = zoneinfo.ZoneInfo("Europe/Moscow") +print(tz_moscow) +# Europe/Moscow + +tz_etc_0300 = zoneinfo.ZoneInfo("Etc/GMT-3") +print(tz_etc_0300) +# Etc/GMT-3 + +tz_offset_0300 = timezone(offset=timedelta(minutes=3 * 60)) +print(tz_offset_0300) +# pytz.FixedOffset(180) + +tz_utc = zoneinfo.ZoneInfo("UTC") +print(tz_utc) +# UTC + +print() + +dt = datetime(year=2025, month=1, day=1, hour=12, minute=0, second=0) + +print(dt.astimezone(tz_moscow).strftime("%z")) +print(dt.astimezone(tz_etc_0300).strftime("%z")) +print(dt.astimezone(tz_offset_0300).strftime("%z")) +print(dt.astimezone(tz_utc).strftime("%z")) +# +0300 +# +0300 +# +0300 +# +0000 diff --git a/pytz_vs_zoneinfo__examples/get_timezones__pytz.py b/pytz_vs_zoneinfo__examples/get_timezones__pytz.py new file mode 100644 index 000000000..4cc985eac --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_timezones__pytz.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pytz==2025.2 +import pytz + + +timezones: list[str] = pytz.all_timezones +print(len(timezones), timezones) +# 594 ['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', ..., 'W-SU', 'WET', 'Zulu'] + +timezones: list[str] = pytz.common_timezones +print(len(timezones), timezones) +# 439 ['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', ..., 'US/Mountain', 'US/Pacific', 'UTC'] diff --git a/pytz_vs_zoneinfo__examples/get_timezones__zoneinfo.py b/pytz_vs_zoneinfo__examples/get_timezones__zoneinfo.py new file mode 100644 index 000000000..a7cc79dc6 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_timezones__zoneinfo.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import zoneinfo + + +timezones: list[str] = sorted(zoneinfo.available_timezones()) +print(len(timezones), timezones) +# 597 ['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', ..., 'W-SU', 'WET', 'Zulu'] diff --git a/pytz_vs_zoneinfo__examples/get_tz__pytz.py b/pytz_vs_zoneinfo__examples/get_tz__pytz.py new file mode 100644 index 000000000..18165a422 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_tz__pytz.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pytz==2025.2 +import pytz + + +tz_moscow = pytz.timezone("Europe/Moscow") +print(tz_moscow) +# Europe/Moscow + +tz_utc = pytz.timezone("UTC") +print(tz_utc) +# UTC + +try: + tz = pytz.timezone("INVALID") + raise Exception() +except pytz.UnknownTimeZoneError as e: + print(e) +# 'INVALID' + +try: + tz = pytz.timezone(None) + raise Exception() +except pytz.UnknownTimeZoneError as e: + print(e) +# None diff --git a/pytz_vs_zoneinfo__examples/get_tz__zoneinfo.py b/pytz_vs_zoneinfo__examples/get_tz__zoneinfo.py new file mode 100644 index 000000000..c6fe4b826 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_tz__zoneinfo.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import zoneinfo +from datetime import datetime, timedelta, timezone + + +tz_moscow = zoneinfo.ZoneInfo("Europe/Moscow") +print(tz_moscow) +# Europe/Moscow + +tz_utc = zoneinfo.ZoneInfo("UTC") +print(tz_utc) +# UTC + +try: + tz = zoneinfo.ZoneInfo("INVALID") + raise Exception() +except zoneinfo.ZoneInfoNotFoundError as e: + print(e) +# 'No time zone found with key INVALID' + +try: + tz = zoneinfo.ZoneInfo(None) + raise Exception() +except TypeError as e: + print(e) +# expected str, bytes or os.PathLike object, not NoneType diff --git a/pytz_vs_zoneinfo__examples/get_tz_by_country__pytz.py b/pytz_vs_zoneinfo__examples/get_tz_by_country__pytz.py new file mode 100644 index 000000000..6937b5366 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_tz_by_country__pytz.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# pip install pytz==2025.2 +import pytz + + +ru_timezones: list[str] = pytz.country_timezones["ru"] +print(len(ru_timezones)) +# 26 + +for tz in ru_timezones: + print(tz) +""" +Europe/Kaliningrad +Europe/Moscow +Europe/Kirov +Europe/Volgograd +Europe/Astrakhan +Europe/Saratov +Europe/Ulyanovsk +Europe/Samara +Asia/Yekaterinburg +Asia/Omsk +Asia/Novosibirsk +Asia/Barnaul +Asia/Tomsk +Asia/Novokuznetsk +Asia/Krasnoyarsk +Asia/Irkutsk +Asia/Chita +Asia/Yakutsk +Asia/Khandyga +Asia/Vladivostok +Asia/Ust-Nera +Asia/Magadan +Asia/Sakhalin +Asia/Srednekolymsk +Asia/Kamchatka +Asia/Anadyr +""" diff --git a/pytz_vs_zoneinfo__examples/get_tz_from_offset__pytz.py b/pytz_vs_zoneinfo__examples/get_tz_from_offset__pytz.py new file mode 100644 index 000000000..f673cb92b --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_tz_from_offset__pytz.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from datetime import datetime, tzinfo, timezone + +# pip install pytz==2025.2 +import pytz + + +PATTERN_TZ_OFFSET = re.compile(r"(?P[+-])(?P\d{2}):?(?P\d{2})") + + +def get_tz(value: str) -> tzinfo: + m = PATTERN_TZ_OFFSET.search(value) + if not m: + raise Exception(f"Invalid time zone: {value!r}") + + sign: str = m.group("sign") + hour: int = int(m.group("hour")) + minute: int = int(m.group("minute")) + + total_minutes = hour * 60 + minute + if sign == "-": + total_minutes = -total_minutes + + return pytz.FixedOffset(offset=total_minutes) + + +if __name__ == "__main__": + now_utc = datetime.now(timezone.utc) + + test_cases = [ + (pytz.timezone("Etc/GMT-3"), "+0300"), + (pytz.FixedOffset(offset=3 * 60), "+0300"), + (get_tz("+0300"), "+0300"), + (get_tz("+03:00"), "+0300"), + (get_tz("-0300"), "-0300") + ] + + for tz, expected in test_cases: + result = now_utc.astimezone(tz).strftime("%z") + print(result) + assert result == expected diff --git a/pytz_vs_zoneinfo__examples/get_tz_from_offset__zoneinfo.py b/pytz_vs_zoneinfo__examples/get_tz_from_offset__zoneinfo.py new file mode 100644 index 000000000..d213e598f --- /dev/null +++ b/pytz_vs_zoneinfo__examples/get_tz_from_offset__zoneinfo.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import re +from datetime import datetime, timedelta, timezone, tzinfo + + +PATTERN_TZ_OFFSET = re.compile(r"(?P[+-])(?P\d{2}):?(?P\d{2})") + + +def get_tz(value: str) -> tzinfo: + m = PATTERN_TZ_OFFSET.search(value) + if not m: + raise Exception(f"Invalid time zone: {value!r}") + + sign: str = m.group("sign") + hour: int = int(m.group("hour")) + minute: int = int(m.group("minute")) + + total_minutes = hour * 60 + minute + if sign == "-": + total_minutes = -total_minutes + + return timezone(offset=timedelta(minutes=total_minutes)) + + +if __name__ == "__main__": + import zoneinfo + + now_utc = datetime.now(timezone.utc) + + test_cases = [ + (zoneinfo.ZoneInfo("Etc/GMT-3"), "+0300"), + (timezone(offset=timedelta(minutes=3 * 60)), "+0300"), + (get_tz("+0300"), "+0300"), + (get_tz("+03:00"), "+0300"), + (get_tz("-0300"), "-0300") + ] + + for tz, expected in test_cases: + result = now_utc.astimezone(tz).strftime("%z") + print(result) + assert result == expected diff --git a/pytz_vs_zoneinfo__examples/utc_plus_tz__pytz.py b/pytz_vs_zoneinfo__examples/utc_plus_tz__pytz.py new file mode 100644 index 000000000..b62df7409 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/utc_plus_tz__pytz.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime + +# pip install pytz==2025.2 +import pytz + + +tz_moscow = pytz.timezone("Europe/Moscow") +tz_etc_0300 = pytz.timezone("Etc/GMT-3") +tz_offset_0300 = pytz.FixedOffset(offset=3 * 60) +tz_utc = pytz.timezone("UTC") + +dt = datetime(year=2025, month=1, day=1, hour=12, minute=0, second=0) +print(dt) +# 2025-01-01 12:00:00 + +print() + +print(tz_moscow.fromutc(dt)) +print(tz_etc_0300.fromutc(dt)) +print(tz_offset_0300.fromutc(tz_offset_0300.localize(dt))) +print(tz_utc.fromutc(tz_utc.localize(dt))) +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 12:00:00+00:00 diff --git a/pytz_vs_zoneinfo__examples/utc_plus_tz__zoneinfo.py b/pytz_vs_zoneinfo__examples/utc_plus_tz__zoneinfo.py new file mode 100644 index 000000000..73ad0f6d1 --- /dev/null +++ b/pytz_vs_zoneinfo__examples/utc_plus_tz__zoneinfo.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import zoneinfo +from datetime import datetime, timedelta, timezone + + +tz_moscow = zoneinfo.ZoneInfo("Europe/Moscow") +tz_etc_0300 = zoneinfo.ZoneInfo("Etc/GMT-3") +tz_offset_0300 = timezone(offset=timedelta(minutes=3 * 60)) +tz_utc = zoneinfo.ZoneInfo("UTC") + +dt_utc = datetime(year=2025, month=1, day=1, hour=12, minute=0, second=0, tzinfo=tz_utc) +print(dt_utc) +# 2025-01-01 12:00:00+00:00 + +assert dt_utc == datetime.fromisoformat("2025-01-01 12:00:00").replace(tzinfo=dt_utc.tzinfo) +assert dt_utc == datetime.fromisoformat("2025-01-01 12:00:00").replace(tzinfo=timezone.utc) + +print() + +print(dt_utc.astimezone(tz_moscow)) +print(dt_utc.astimezone(tz_etc_0300)) +print(dt_utc.astimezone(tz_offset_0300)) +print(dt_utc.astimezone(tz_utc)) +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 12:00:00+00:00 + +print("\n" + "-" * 10 + "\n") + +dt = datetime(year=2025, month=1, day=1, hour=12, minute=0, second=0) +print(dt) +# 2025-01-01 12:00:00 + +print() + +print(tz_moscow.fromutc(dt.replace(tzinfo=tz_moscow))) +print(tz_etc_0300.fromutc(dt.replace(tzinfo=tz_etc_0300))) +print(tz_offset_0300.fromutc(dt.replace(tzinfo=tz_offset_0300))) +print(tz_utc.fromutc(dt.replace(tzinfo=tz_utc))) +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 15:00:00+03:00 +# 2025-01-01 12:00:00+00:00 diff --git a/qbittorrent_examples/about_version.py b/qbittorrent_examples/about_version.py index 2fe3c6757..466279d99 100644 --- a/qbittorrent_examples/about_version.py +++ b/qbittorrent_examples/about_version.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_client + qb = get_client() -print('Get qBittorrent version:', qb.qbittorrent_version) -print('Get WEB API version:', qb.api_version) +print("Get qBittorrent version:", qb.qbittorrent_version) +print("Get WEB API version:", qb.api_version) diff --git a/qbittorrent_examples/check_changes_in_torrent.py b/qbittorrent_examples/check_changes_in_torrent.py index a62124426..d6b80645e 100644 --- a/qbittorrent_examples/check_changes_in_torrent.py +++ b/qbittorrent_examples/check_changes_in_torrent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -10,6 +10,21 @@ """ +import time +import traceback +import re + +from collections import defaultdict +from datetime import datetime + +import requests +import effbot_bencode + +from bs4 import BeautifulSoup + +# pip install simple-wait +from simple_wait import wait + def get_rutor_torrent_download_info(torrent_url): """ @@ -17,27 +32,23 @@ def get_rutor_torrent_download_info(torrent_url): """ - import requests rs = requests.get(torrent_url) + root = BeautifulSoup(rs.content, "lxml") - from bs4 import BeautifulSoup - root = BeautifulSoup(rs.content, 'lxml') - - magnet_url = root.select_one('#download > a[href^="magnet"]')['href'] + magnet_url = root.select_one('#download > a[href^="magnet"]')["href"] # For get info hash from magnet url - import re - match = re.compile(r'btih:([abcdef\d]+?)&', flags=re.IGNORECASE).search(magnet_url) + match = re.compile(r"btih:([abcdef\d]+?)&", flags=re.IGNORECASE).search(magnet_url) if match: info_hash = match.group(1) - return torrent_url.replace('/torrent/', '/download/'), magnet_url, info_hash - + return torrent_url.replace("/torrent/", "/download/"), magnet_url, info_hash -def remove_previous_torrent_from_qbittorrent(qb, new_info_hash): - info_hash_by_name_dict = {torrent['hash']: torrent['name'] for torrent in qb.torrents()} - from collections import defaultdict +def remove_previous_torrent_from_qbittorrent(qb, new_info_hash) -> None: + info_hash_by_name_dict = { + torrent["hash"]: torrent["name"] for torrent in qb.torrents() + } name_by_info_hash_list_dict = defaultdict(list) for info_hash, name in info_hash_by_name_dict.items(): @@ -56,60 +67,61 @@ def remove_previous_torrent_from_qbittorrent(qb, new_info_hash): # Remove previous torrents if info_hash_list: - print('Удаление предыдущих раздач этого торрента: {}'.format(info_hash_list)) + print( + f"Удаление предыдущих раздач этого торрента: {info_hash_list}" + ) qb.delete(info_hash_list) else: print("Предыдущие закачки не найдены") - -# Import https://github.com/gil9red/SimplePyScripts/blob/8fa9b9c23d10b5ee7ff0161da997b463f7a861bf/wait/wait.py -import sys -sys.path.append('../wait') - -from wait import wait - -if __name__ == '__main__': +if __name__ == "__main__": from config import API_ID, TO from common import get_client qb = get_client() - torrent_url = 'http://anti-tor.org/torrent/544942' + torrent_url = "http://anti-tor.org/torrent/544942" last_info_hash = None last_torrent_files = list() while True: try: - from datetime import datetime today = datetime.today() - torrent_file_url, _, info_hash = get_rutor_torrent_download_info(torrent_url) - print('{}: Проверка {}: {} / {}'.format(today, torrent_url, torrent_file_url, info_hash)) + torrent_file_url, _, info_hash = get_rutor_torrent_download_info( + torrent_url + ) + print( + f"{today}: Проверка {torrent_url}: {torrent_file_url} / {info_hash}" + ) if qb.get_torrent(info_hash): - print('Торрент {} уже есть в списке раздачи'.format(info_hash)) + print(f"Торрент {info_hash} уже есть в списке раздачи") else: if info_hash != last_info_hash: - import requests - data = requests.get(torrent_file_url).content.decode('latin1') - - import effbot_bencode + data = requests.get(torrent_file_url).content.decode("latin1") torrent = effbot_bencode.decode(data) - files = ["/".join(file["path"]) for file in torrent["info"]["files"]] + files = [ + "/".join(file["path"]) for file in torrent["info"]["files"] + ] # Использование множеств, чтобы узнать разницу списков, т.е. какие файлы были добавлены # А чтобы узнать какие были удалены: list(set(last_torrent_files) - set(files)) new_files = list(set(files) - set(last_torrent_files)) if last_info_hash is None: - print("Добавление торрента с {} файлами: {}".format(len(new_files), new_files)) + print( + f"Добавление торрента с {len(new_files)} файлами: {new_files}" + ) else: - print('Торрент изменился, пора его перекачивать') - print("Добавлено {} файлов: {}".format(len(new_files), new_files)) + print("Торрент изменился, пора его перекачивать") + print( + f"Добавлено {len(new_files)} файлов: {new_files}" + ) last_info_hash = info_hash last_torrent_files = files @@ -119,22 +131,21 @@ def remove_previous_torrent_from_qbittorrent(qb, new_info_hash): qb.download_from_link(torrent_file_url) # Отправляю смс на номер - url = 'https://sms.ru/sms/send?api_id={api_id}&to={to}&text={text}'.format( + url = "https://sms.ru/sms/send?api_id={api_id}&to={to}&text={text}".format( api_id=API_ID, to=TO, - text="Вышла новая серия '{}'".format(torrent['info']['name']) + text=f"Вышла новая серия '{torrent['info']['name']}'", ) rs = requests.get(url) print(rs.text) # Даем 5 секунд на добавление торрента в клиент - import time time.sleep(5) remove_previous_torrent_from_qbittorrent(qb, info_hash) else: - print('Изменений нет') + print("Изменений нет") print() @@ -142,12 +153,10 @@ def remove_previous_torrent_from_qbittorrent(qb, new_info_hash): wait(hours=3) except Exception: - import traceback - print('Ошибка:') + print("Ошибка:") print(traceback.format_exc()) - print('Через 1 минуту попробую снова...') + print("Через 1 минуту попробую снова...") - # Wait 1 minute before next attempt - import time - time.sleep(60) + # Wait before next attempt + wait(minutes=1) diff --git a/qbittorrent_examples/common.py b/qbittorrent_examples/common.py index ddc95e8e8..6691e6592 100644 --- a/qbittorrent_examples/common.py +++ b/qbittorrent_examples/common.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import sys -from typing import List, Dict -from pathlib import Path +# pip install humanize +from humanize import naturalsize as sizeof_fmt # pip install tabulate from tabulate import tabulate @@ -16,11 +15,8 @@ from config import IP_HOST, USER, PASSWORD -sys.path.append(str(Path(__file__).resolve().parent.parent)) -from human_byte_size import sizeof_fmt - -def print_table(rows: List[List[str]], headers: List[str], show_index=True): +def print_table(rows: list[list[str]], headers: list[str], show_index=True) -> None: if show_index: show_index = range(1, len(rows) + 1) @@ -28,23 +24,28 @@ def print_table(rows: List[List[str]], headers: List[str], show_index=True): print(text) -def print_files_table(files: List[Dict]): - rows = [(file['name'], sizeof_fmt(file['size'])) for file in sorted(files, key=lambda x: x['name'])] - headers = ['#', 'File Name', 'Size'] +def print_files_table(files: list[dict]) -> None: + rows = [ + (file["name"], sizeof_fmt(file["size"])) + for file in sorted(files, key=lambda x: x["name"]) + ] + headers = ["#", "File Name", "Size"] print_table(rows, headers) -def print_torrents(torrents: List[Dict]): +def print_torrents(torrents: list[dict]) -> None: total_size = 0 for i, torrent in enumerate(torrents, 1): - torrent_size = torrent['total_size'] + torrent_size = torrent["total_size"] total_size += torrent_size print(f"{i:3}. {torrent['name']} ({sizeof_fmt(torrent_size)})") print() - print(f'Total torrents: {len(torrents)}, total size: {sizeof_fmt(total_size)} ({total_size} bytes)') + print( + f"Total torrents: {len(torrents)}, total size: {sizeof_fmt(total_size)} ({total_size} bytes)" + ) def get_client() -> Client: diff --git a/qbittorrent_examples/config.py b/qbittorrent_examples/config.py index 9d2ee95d9..657fe9bde 100644 --- a/qbittorrent_examples/config.py +++ b/qbittorrent_examples/config.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -IP_HOST = 'http://127.0.0.1:8080/' -USER = 'admin' -PASSWORD = '' +IP_HOST = "http://127.0.0.1:8080/" +USER = "admin" +PASSWORD = "" # Для отправки смс -API_ID = '' -TO = '' +API_ID = "" +TO = "" diff --git a/qbittorrent_examples/effbot_bencode.py b/qbittorrent_examples/effbot_bencode.py index b6cea7858..3777e710b 100644 --- a/qbittorrent_examples/effbot_bencode.py +++ b/qbittorrent_examples/effbot_bencode.py @@ -17,7 +17,7 @@ def tokenize(text, match=re.compile("([idel])|(\d+):|(-?\d+)").match): i = m.end() if m.lastindex == 2: yield "s" - yield text[i:i+int(s)] + yield text[i : i + int(s)] i = i + int(s) else: yield s @@ -50,7 +50,7 @@ def decode(text): try: src = tokenize(text) data = decode_item(src.__next__, next(src)) - for token in src: # look for more tokens + for token in src: # look for more tokens raise SyntaxError("trailing junk") except (AttributeError, ValueError, StopIteration): raise SyntaxError("syntax error") diff --git a/qbittorrent_examples/get_info_random_torrent.py b/qbittorrent_examples/get_info_random_torrent.py index 5a43b603b..295f6b9f0 100644 --- a/qbittorrent_examples/get_info_random_torrent.py +++ b/qbittorrent_examples/get_info_random_torrent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import random @@ -13,7 +13,7 @@ print(f"{torrent['name']} ({sizeof_fmt(torrent['total_size'])})") print() -files = qb.get_torrent_files(torrent['hash']) -print(f'Files ({len(files)}):') +files = qb.get_torrent_files(torrent["hash"]) +print(f"Files ({len(files)}):") print_files_table(files) diff --git a/qbittorrent_examples/get_top5_torrent_by_size.py b/qbittorrent_examples/get_top5_torrent_by_size.py index a4468e863..5b89fb8d7 100644 --- a/qbittorrent_examples/get_top5_torrent_by_size.py +++ b/qbittorrent_examples/get_top5_torrent_by_size.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import sizeof_fmt, get_client @@ -11,15 +11,19 @@ torrents = qb.torrents() -torrents_max_top5 = sorted(torrents, key=lambda x: x['total_size'], reverse=True)[:5] -torrents_min_top5 = sorted(torrents, key=lambda x: x['total_size'])[:5] +torrents_max_top5 = sorted(torrents, key=lambda x: x["total_size"], reverse=True)[:5] +torrents_min_top5 = sorted(torrents, key=lambda x: x["total_size"])[:5] -print('Max top5:') +print("Max top5:") for i, torrent in enumerate(torrents_max_top5, 1): - print(' {}. {} ({})'.format(i, torrent['name'], sizeof_fmt(torrent['total_size']))) + print( + f" {i}. {torrent['name']} ({sizeof_fmt(torrent['total_size'])})" + ) print() -print('Min top5:') +print("Min top5:") for i, torrent in enumerate(torrents_min_top5, 1): - print(' {}. {} ({})'.format(i, torrent['name'], sizeof_fmt(torrent['total_size']))) + print( + f" {i}. {torrent['name']} ({sizeof_fmt(torrent['total_size'])})" + ) diff --git a/qbittorrent_examples/get_top5_torrent_by_size__use_api_sort_limit.py b/qbittorrent_examples/get_top5_torrent_by_size__use_api_sort_limit.py index 44fef4851..c47a2f3aa 100644 --- a/qbittorrent_examples/get_top5_torrent_by_size__use_api_sort_limit.py +++ b/qbittorrent_examples/get_top5_torrent_by_size__use_api_sort_limit.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import sizeof_fmt, get_client @@ -9,15 +9,19 @@ qb = get_client() -torrents_max_top5 = qb.torrents(sort='size', reverse='true', limit='5') -torrents_min_top5 = qb.torrents(sort='size', limit='5') +torrents_max_top5 = qb.torrents(sort="size", reverse="true", limit="5") +torrents_min_top5 = qb.torrents(sort="size", limit="5") -print('Max top5:') +print("Max top5:") for i, torrent in enumerate(torrents_max_top5, 1): - print(' {}. {} ({})'.format(i, torrent['name'], sizeof_fmt(torrent['total_size']))) + print( + f" {i}. {torrent['name']} ({sizeof_fmt(torrent['total_size'])})" + ) print() -print('Min top5:') +print("Min top5:") for i, torrent in enumerate(torrents_min_top5, 1): - print(' {}. {} ({})'.format(i, torrent['name'], sizeof_fmt(torrent['total_size']))) + print( + f" {i}. {torrent['name']} ({sizeof_fmt(torrent['total_size'])})" + ) diff --git a/qbittorrent_examples/get_top_torrent_by_files_number.py b/qbittorrent_examples/get_top_torrent_by_files_number.py index 93481442e..6b7963568 100644 --- a/qbittorrent_examples/get_top_torrent_by_files_number.py +++ b/qbittorrent_examples/get_top_torrent_by_files_number.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import sizeof_fmt, get_client, print_files_table @@ -10,12 +10,12 @@ qb = get_client() torrents = qb.torrents() -torrent = max(torrents, key=lambda x: len(qb.get_torrent_files(x['hash']))) +torrent = max(torrents, key=lambda x: len(qb.get_torrent_files(x["hash"]))) -print('{} ({})'.format(torrent['name'], sizeof_fmt(torrent['total_size']))) +print(f"{torrent['name']} ({sizeof_fmt(torrent['total_size'])})") print() -files = qb.get_torrent_files(torrent['hash']) -print('Files ({}):'.format(len(files))) +files = qb.get_torrent_files(torrent["hash"]) +print(f"Files ({len(files)}):") print_files_table(files) diff --git a/qbittorrent_examples/print_current__save_path__of_torrents.py b/qbittorrent_examples/print_current__save_path__of_torrents.py index 08103cda9..cb134c5f3 100644 --- a/qbittorrent_examples/print_current__save_path__of_torrents.py +++ b/qbittorrent_examples/print_current__save_path__of_torrents.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from collections import Counter @@ -10,10 +10,10 @@ qb = get_client() -items = [torrent['save_path'] for torrent in qb.torrents()] +items = [torrent["save_path"] for torrent in qb.torrents()] counter = Counter(items) -print('Save path:', sorted(counter.keys())) +print("Save path:", sorted(counter.keys())) for name, number in counter.items(): - print(f'{name!r}: {number}') + print(f"{name!r}: {number}") diff --git a/qbittorrent_examples/print_not_torrent_content_on_saved_paths.py b/qbittorrent_examples/print_not_torrent_content_on_saved_paths.py index e12257a8e..79b7fc0ca 100644 --- a/qbittorrent_examples/print_not_torrent_content_on_saved_paths.py +++ b/qbittorrent_examples/print_not_torrent_content_on_saved_paths.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/qbittorrent/qBittorrent/blob/ccd8f3e0f1447c0f88fedb975b52e2691722d6e9/src/base/bittorrent/torrenthandle.cpp#L302 @@ -10,6 +10,7 @@ from glob import glob, escape as glob_escape from os.path import join, normpath, getsize, isfile from os import listdir + from common import get_client, sizeof_fmt @@ -17,9 +18,9 @@ save_path_current_paths = [] -for save_path in set(torrent['save_path'] for torrent in qb.torrents()): +for save_path in set(torrent["save_path"] for torrent in qb.torrents()): for file_name in listdir(save_path): - if file_name == '.unwanted': + if file_name == ".unwanted": continue path = normpath(join(save_path, file_name)) @@ -29,20 +30,20 @@ torrent_content_path_list = [] -for torrent in qb.torrents(sort='name'): +for torrent in qb.torrents(sort="name"): # Нужны только те торренты, что скачаны (хоть частично) - if torrent['downloaded'] == 0: + if torrent["downloaded"] == 0: continue - files = qb.get_torrent_files(torrent['hash']) + files = qb.get_torrent_files(torrent["hash"]) - save_path = torrent['save_path'] - first_file_path = files[0]['name'] + save_path = torrent["save_path"] + first_file_path = files[0]["name"] # Найдем первый разделите папки - index = first_file_path.find('/') + index = first_file_path.find("/") if index == -1: - index = first_file_path.find('\\') + index = first_file_path.find("\\") if index != -1: # Путь до папки торрента @@ -67,12 +68,12 @@ size = getsize(path) else: # Найдем все файлы в папке и подпапках и подсчитаем их суммарный размер - sub_files = filter(isfile, glob(glob_escape(path) + '/**', recursive=True)) + sub_files = filter(isfile, glob(glob_escape(path) + "/**", recursive=True)) size = sum(getsize(file_name) for file_name in sub_files) total_size += size - print(f'{path} ({size} / {sizeof_fmt(size)})') + print(f"{path} ({size} / {sizeof_fmt(size)})") print() -print(f'Total size: {total_size} / {sizeof_fmt(total_size)}') +print(f"Total size: {total_size} / {sizeof_fmt(total_size)}") diff --git a/qbittorrent_examples/print_torrent_comment.py b/qbittorrent_examples/print_torrent_comment.py index ffa968c26..2bae74324 100644 --- a/qbittorrent_examples/print_torrent_comment.py +++ b/qbittorrent_examples/print_torrent_comment.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_client def get_comment(torrent): - return qb.get_torrent(torrent['hash'])['comment'] + return qb.get_torrent(torrent["hash"])["comment"] qb = get_client() @@ -21,4 +21,4 @@ def get_comment(torrent): comment_list.append(comment) comment_list.sort() -print('\n'.join(comment_list)) +print("\n".join(comment_list)) diff --git a/qbittorrent_examples/print_torrent_created_by.py b/qbittorrent_examples/print_torrent_created_by.py index db6b0ab77..8a724f973 100644 --- a/qbittorrent_examples/print_torrent_created_by.py +++ b/qbittorrent_examples/print_torrent_created_by.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_client @@ -12,8 +12,8 @@ created_by_list = set() for torrent in qb.torrents(): - created_by = qb.get_torrent(torrent['hash'])['created_by'] + created_by = qb.get_torrent(torrent["hash"])["created_by"] if created_by: created_by_list.add(created_by) -print('\n'.join(created_by_list)) +print("\n".join(created_by_list)) diff --git a/qbittorrent_examples/print_torrents_by_specific_Disc.py b/qbittorrent_examples/print_torrents_by_specific_Disc.py index 7a9a75d46..3f687bb13 100644 --- a/qbittorrent_examples/print_torrents_by_specific_Disc.py +++ b/qbittorrent_examples/print_torrents_by_specific_Disc.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_client, print_torrents @@ -9,5 +9,5 @@ qb = get_client() need_disc = "E:" -torrents = [x for x in qb.torrents() if x['save_path'].startswith(need_disc)] +torrents = [x for x in qb.torrents() if x["save_path"].startswith(need_disc)] print_torrents(torrents) diff --git a/qbittorrent_examples/print_total_size.py b/qbittorrent_examples/print_total_size.py index db0d9e616..e92d04f41 100644 --- a/qbittorrent_examples/print_total_size.py +++ b/qbittorrent_examples/print_total_size.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_client, sizeof_fmt @@ -10,7 +10,7 @@ qb = get_client() torrents = qb.torrents() -total_size = sum(torrent['total_size'] for torrent in torrents) +total_size = sum(torrent["total_size"] for torrent in torrents) -print(f'Total torrents: {len(torrents)}') -print(f'Total size: {sizeof_fmt(total_size)} ({total_size} bytes)') +print(f"Total torrents: {len(torrents)}") +print(f"Total size: {sizeof_fmt(total_size)} ({total_size} bytes)") diff --git a/qbittorrent_examples/send_api.py b/qbittorrent_examples/send_api.py index e41ab8017..43f354e4d 100644 --- a/qbittorrent_examples/send_api.py +++ b/qbittorrent_examples/send_api.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import requests -rs = requests.get('http://127.0.0.1:8080/api/v2/torrents/info') + + +rs = requests.get("http://127.0.0.1:8080/api/v2/torrents/info") print(rs) print(rs.text) diff --git a/qbittorrent_examples/shutdown.py b/qbittorrent_examples/shutdown.py index b7019406b..ff659ad6d 100644 --- a/qbittorrent_examples/shutdown.py +++ b/qbittorrent_examples/shutdown.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_client + qb = get_client() qb.shutdown() diff --git a/qbittorrent_examples/torrent_list.py b/qbittorrent_examples/torrent_list.py index f2eb9f9a5..4f9b1b8a9 100644 --- a/qbittorrent_examples/torrent_list.py +++ b/qbittorrent_examples/torrent_list.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/qbittorrent/qbittorrent/wiki/WebUI-API-Documentation#get-torrent-list diff --git a/qbittorrent_examples/torrent_list__sort_by_added_on.py b/qbittorrent_examples/torrent_list__sort_by_added_on.py index 842c6e810..d6ffe6d7b 100644 --- a/qbittorrent_examples/torrent_list__sort_by_added_on.py +++ b/qbittorrent_examples/torrent_list__sort_by_added_on.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/qbittorrent/qbittorrent/wiki/WebUI-API-Documentation#get-torrent-list @@ -11,5 +11,5 @@ qb = get_client() -torrents = qb.torrents(sort='added_on', reverse='true') +torrents = qb.torrents(sort="added_on", reverse="true") print_torrents(torrents) diff --git a/qbittorrent_examples/torrent_list__sort_by_name.py b/qbittorrent_examples/torrent_list__sort_by_name.py index f3cb95e0a..34aeeabaa 100644 --- a/qbittorrent_examples/torrent_list__sort_by_name.py +++ b/qbittorrent_examples/torrent_list__sort_by_name.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/qbittorrent/qbittorrent/wiki/WebUI-API-Documentation#get-torrent-list @@ -12,7 +12,7 @@ qb = get_client() -torrents = qb.torrents(sort='name') +torrents = qb.torrents(sort="name") # Desc / Reverse: # torrents = qb.torrents(sort='name', reverse='true') diff --git a/qbittorrent_examples/torrent_list__table_gui_pyqt.py b/qbittorrent_examples/torrent_list__table_gui_pyqt.py index 0e6c3b35a..9b0c01f7b 100644 --- a/qbittorrent_examples/torrent_list__table_gui_pyqt.py +++ b/qbittorrent_examples/torrent_list__table_gui_pyqt.py @@ -1,33 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/qbittorrent/qbittorrent/wiki/WebUI-API-Documentation#get-torrent-list -from common import get_client - from PyQt5.Qt import * +from common import get_client + class TorrentInfoWidget(QTableWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setEditTriggers(QTableWidget.NoEditTriggers) self.setSelectionBehavior(QTableWidget.SelectRows) self.setSelectionMode(QTableWidget.SingleSelection) - headers = ['KEY', 'VALUE'] + headers = ["KEY", "VALUE"] self.setColumnCount(len(headers)) self.setHorizontalHeaderLabels(headers) self.horizontalHeader().setStretchLastSection(True) self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) - def fill(self, torrent_details: dict): + def fill(self, torrent_details: dict) -> None: while self.rowCount(): self.removeRow(0) @@ -40,7 +40,7 @@ def fill(self, torrent_details: dict): class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.table_torrent = QTableWidget() @@ -57,7 +57,7 @@ def __init__(self): self.setCentralWidget(splitter) - def fill_table(self): + def fill_table(self) -> None: qb = get_client() torrents = qb.torrents() if not torrents: @@ -80,7 +80,7 @@ def fill_table(self): self.table_torrent.setItem(row, column, item) - def fill_torrent_info(self): + def fill_torrent_info(self) -> None: items = self.table_torrent.selectedItems() if not items: return @@ -88,12 +88,12 @@ def fill_torrent_info(self): torrent = items[0].data(Qt.UserRole) qb = get_client() - torrent_details = qb.get_torrent(torrent['hash']) + torrent_details = qb.get_torrent(torrent["hash"]) self.torrent_info_widget.fill(torrent_details) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qbittorrent_examples/torrent_list__with_filter.py b/qbittorrent_examples/torrent_list__with_filter.py index b11e888ea..09bc98b9d 100644 --- a/qbittorrent_examples/torrent_list__with_filter.py +++ b/qbittorrent_examples/torrent_list__with_filter.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/qbittorrent/qbittorrent/wiki/WebUI-API-Documentation#get-torrent-list @@ -11,5 +11,5 @@ qb = get_client() -torrents = qb.torrents(filter='downloading') +torrents = qb.torrents(filter="downloading") print_torrents(torrents) diff --git a/qbittorrent_examples/torrent_list__with_filter_by_name.py b/qbittorrent_examples/torrent_list__with_filter_by_name.py index 6d527f511..2ac0e4b4c 100644 --- a/qbittorrent_examples/torrent_list__with_filter_by_name.py +++ b/qbittorrent_examples/torrent_list__with_filter_by_name.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from common import get_client, print_torrents -def get_torrents(qb, search_name='', **filters) -> list: +def get_torrents(qb, search_name="", **filters) -> list: def match(name: str) -> bool: return search_name.lower() in name.lower() - return [torrent for torrent in qb.torrents(**filters) if match(torrent['name'])] + return [torrent for torrent in qb.torrents(**filters) if match(torrent["name"])] qb = get_client() -torrents = get_torrents(qb, search_name='.mkv') +torrents = get_torrents(qb, search_name=".mkv") print_torrents(torrents) diff --git a/qbittorrent_examples/torrent_list__with_filter_plus_sort_by_added_on.py b/qbittorrent_examples/torrent_list__with_filter_plus_sort_by_added_on.py index ef7e2b087..f8c036311 100644 --- a/qbittorrent_examples/torrent_list__with_filter_plus_sort_by_added_on.py +++ b/qbittorrent_examples/torrent_list__with_filter_plus_sort_by_added_on.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://github.com/qbittorrent/qbittorrent/wiki/WebUI-API-Documentation#get-torrent-list @@ -11,5 +11,5 @@ qb = get_client() -torrents = qb.torrents(filter='downloading', sort='added_on', reverse='true') +torrents = qb.torrents(filter="downloading", sort="added_on", reverse="true") print_torrents(torrents) diff --git a/pyqrcode__examples/db_sqlite3.py b/qrcode/pyqrcode__examples/db_sqlite3.py similarity index 80% rename from pyqrcode__examples/db_sqlite3.py rename to qrcode/pyqrcode__examples/db_sqlite3.py index 3a714ca4c..806d1bf7e 100644 --- a/pyqrcode__examples/db_sqlite3.py +++ b/qrcode/pyqrcode__examples/db_sqlite3.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sqlite3 @@ -16,7 +16,7 @@ print(items) # ('Version: 3.29.0', '2020-07-20') -text = '\n'.join(items) +text = "\n".join(items) qr_code = pyqrcode.create(text) -qr_code.png('sqlite3.png', scale=6) +qr_code.png("sqlite3.png", scale=6) diff --git a/qrcode/pyqrcode__examples/hello_world.py b/qrcode/pyqrcode__examples/hello_world.py new file mode 100644 index 000000000..d2e5aee67 --- /dev/null +++ b/qrcode/pyqrcode__examples/hello_world.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://pythonhosted.org/PyQRCode/ + + +# pip install pyqrcode +# pip install pypng +import pyqrcode + + +qr_code = pyqrcode.create("http://uca.edu") +qr_code.png("img.png") + +qr_code = pyqrcode.create("Hello World!") +qr_code.png("img_hello.png") + +qr_code.png("img_hello__scale=6.png", scale=6) diff --git a/qrcode__generate/README.md b/qrcode/qrcode__generate/README.md similarity index 100% rename from qrcode__generate/README.md rename to qrcode/qrcode__generate/README.md diff --git a/qrcode/qrcode__generate/main.py b/qrcode/qrcode__generate/main.py new file mode 100644 index 000000000..1d98b762e --- /dev/null +++ b/qrcode/qrcode__generate/main.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/lincolnloop/python-qrcode + + +# In memory +import io + +# pip install qrcode +import qrcode + + +text = "Hello World!" + +img = qrcode.make(text) +io_data = io.BytesIO() +img.save(io_data, "png") +data = io_data.getvalue() +print(data) + +# In file +img.save("qr_code_1.png") +# +# In file, version 2: +with open("qr_code_2.png", "wb") as f: + f.write(data) diff --git a/qrcode__generate/qr_code_1.png b/qrcode/qrcode__generate/qr_code_1.png similarity index 100% rename from qrcode__generate/qr_code_1.png rename to qrcode/qrcode__generate/qr_code_1.png diff --git a/qrcode__generate/qr_code_2.png b/qrcode/qrcode__generate/qr_code_2.png similarity index 100% rename from qrcode__generate/qr_code_2.png rename to qrcode/qrcode__generate/qr_code_2.png diff --git a/qrcode/qrcode__reader/main.py b/qrcode/qrcode__reader/main.py new file mode 100644 index 000000000..dc67f7e7c --- /dev/null +++ b/qrcode/qrcode__reader/main.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import io +from urllib.request import urlopen + +import requests + +# SOURCE: https://github.com/tomerfiliba/reedsolomon/blob/master/reedsolo.py +# pip install reedsolo +# +# pip install git+https://github.com/ewino/qreader.git +import qreader + + +# FROM FILE +data = qreader.read("../qrcode__generate/qr_code_1.png") +print(data) # Hello World! + + +# FROM URL +url = "https://upload.wikimedia.org/wikipedia/commons/8/8f/Qr-2.png" +data = qreader.read(urlopen(url)) +print(data) # "Version 2" + +url = "https://upload.wikimedia.org/wikipedia/commons/e/eb/QR-%D0%BA%D0%BE%D0%B4.png" +data = qreader.read(urlopen(url)) +print(data) # http://ru.wikipedia.org/wiki/QR_Code + + +# FROM URL as bytes in file-like object +rs = requests.get(url) +image_data = rs.content + +bytes_io = io.BytesIO(image_data) + +data = qreader.read(bytes_io) +print(data) # http://ru.wikipedia.org/wiki/QR_Code diff --git a/qrcode__reader/reedsolo.py b/qrcode/qrcode__reader/reedsolo.py similarity index 100% rename from qrcode__reader/reedsolo.py rename to qrcode/qrcode__reader/reedsolo.py diff --git a/qrcode__generate/main.py b/qrcode__generate/main.py deleted file mode 100644 index bb224e762..000000000 --- a/qrcode__generate/main.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# SOURCE: https://github.com/lincolnloop/python-qrcode - - -text = 'Hello World!' - -# pip install qrcode -import qrcode -img = qrcode.make(text) - -# In memory -import io -io_data = io.BytesIO() -img.save(io_data, 'png') -data = io_data.getvalue() -print(data) - -# In file -img.save('qr_code_1.png') -# -# In file, version 2: -with open('qr_code_2.png', 'wb') as f: - f.write(data) diff --git a/qrcode__reader/main.py b/qrcode__reader/main.py deleted file mode 100644 index f02951d38..000000000 --- a/qrcode__reader/main.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# pip install git+https://github.com/ewino/qreader.git - -# SOURCE: https://github.com/tomerfiliba/reedsolomon/blob/master/reedsolo.py -# pip install reedsolo -import qreader - - -# FROM FILE -data = qreader.read('../qrcode__generate/qr_code_1.png') -print(data) # Hello World! - - -# FROM URL -url = 'https://upload.wikimedia.org/wikipedia/commons/8/8f/Qr-2.png' -from urllib.request import urlopen -data = qreader.read(urlopen(url)) -print(data) # "Version 2" - -url = 'https://upload.wikimedia.org/wikipedia/commons/e/eb/QR-%D0%BA%D0%BE%D0%B4.png' -data = qreader.read(urlopen(url)) -print(data) # http://ru.wikipedia.org/wiki/QR_Code - - -# FROM URL as bytes in file-like object -import requests -rs = requests.get(url) -image_data = rs.content - -import io -bytes_io = io.BytesIO(image_data) - -data = qreader.read(bytes_io) -print(data) # http://ru.wikipedia.org/wiki/QR_Code diff --git a/qt__pyqt__pyside__pyqode/Auto_widget_placement__using_FlowLayout_QThread.py b/qt__pyqt__pyside__pyqode/Auto_widget_placement__using_FlowLayout_QThread.py index cd54ca99c..ccb4e9403 100644 --- a/qt__pyqt__pyside__pyqode/Auto_widget_placement__using_FlowLayout_QThread.py +++ b/qt__pyqt__pyside__pyqode/Auto_widget_placement__using_FlowLayout_QThread.py @@ -2,14 +2,23 @@ # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import time import random +import sys +import time +import traceback from PyQt5.QtWidgets import ( - QWidget, QMessageBox, QVBoxLayout, QApplication, QLabel, QPushButton, QScrollArea, QSizePolicy + QWidget, + QMessageBox, + QVBoxLayout, + QApplication, + QLabel, + QPushButton, + QScrollArea, + QSizePolicy, ) from PyQt5.QtGui import QPainter, QImage, QPixmap from PyQt5.QtCore import Qt, QThread, pyqtSignal @@ -18,18 +27,15 @@ # Для отлова всех исключений, которые в слотах Qt могут "затеряться" и привести к тихому падению -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) - import traceback - text += ''.join(traceback.format_tb(tb)) - - print('Error: ', text) - QMessageBox.critical(None, 'Error', text) + print("Error: ", text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions @@ -42,12 +48,12 @@ def seconds_to_str(seconds: int) -> str: class ImgThread(QThread): new_img = pyqtSignal(QImage) - def __init__(self, life_time_seconds=60): + def __init__(self, life_time_seconds=60) -> None: super().__init__() self.life_time_seconds = life_time_seconds - def run(self): + def run(self) -> None: painter = QPainter() painter.setRenderHint(QPainter.Antialiasing) @@ -71,7 +77,9 @@ def run(self): try: # Алгоритм изменения размера текста взят из http://stackoverflow.com/a/2204501 # Для текущего пришлось немного адаптировать - factor = img.width() / painter.fontMetrics().width(text) / 1.5 # Ширина текста 2/3 от ширины + factor = ( + img.width() / painter.fontMetrics().width(text) / 1.5 + ) # Ширина текста 2/3 от ширины if factor < 1 or factor > 1.25: f = painter.font() point_size = f.pointSizeF() * factor @@ -92,7 +100,7 @@ def run(self): class ImgWidget(QWidget): closed = pyqtSignal() - def __init__(self, life_time_seconds=60): + def __init__(self, life_time_seconds=60) -> None: super().__init__() self._thread = ImgThread(life_time_seconds) @@ -108,26 +116,26 @@ def __init__(self, life_time_seconds=60): self.setLayout(layout) - def _on_new_img(self, img: QImage): + def _on_new_img(self, img: QImage) -> None: if not img: return self.setFixedSize(img.size()) self._label.setPixmap(QPixmap.fromImage(img)) - def closeEvent(self, event): + def closeEvent(self, event) -> None: self.closed.emit() super().closeEvent(event) class Window(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle(__file__.split('/')[-1]) + self.setWindowTitle(__file__.split("/")[-1]) - button = QPushButton('Add') + button = QPushButton("Add") button.clicked.connect(self._on_push_add) self.items_layout = FlowLayout() @@ -144,14 +152,14 @@ def __init__(self): self.setLayout(main_layout) - def _on_push_add(self): + def _on_push_add(self) -> None: w = ImgWidget(life_time_seconds=random.randint(10, 100)) w.closed.connect(lambda w=w: self.items_layout.removeWidget(w)) self.items_layout.addWidget(w) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = Window() diff --git a/qt__pyqt__pyside__pyqode/CompletingTextEdit__QTextEdit_QCompleter.py b/qt__pyqt__pyside__pyqode/CompletingTextEdit__QTextEdit_QCompleter.py index e53204811..17fd1fad0 100644 --- a/qt__pyqt__pyside__pyqode/CompletingTextEdit__QTextEdit_QCompleter.py +++ b/qt__pyqt__pyside__pyqode/CompletingTextEdit__QTextEdit_QCompleter.py @@ -1,30 +1,37 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import sys +import traceback + from PyQt5.QtCore import QFile, QStringListModel, Qt from PyQt5.QtGui import QCursor, QKeySequence, QTextCursor -from PyQt5.QtWidgets import QApplication, QCompleter, QMainWindow, QTextEdit, QMessageBox +from PyQt5.QtWidgets import ( + QApplication, + QCompleter, + QMainWindow, + QTextEdit, + QMessageBox, +) -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions class CompletingTextEdit(QTextEdit): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self.completerSequence = QKeySequence("Ctrl+Space") @@ -33,7 +40,7 @@ def __init__(self, parent=None): self._completer.setWidget(self) self._completer.activated.connect(self.insertCompletion) - def insertCompletion(self, completion): + def insertCompletion(self, completion) -> None: if self._completer.widget() is not self: return @@ -50,7 +57,7 @@ def textUnderCursor(self): return tc.selectedText() - def loadFromFile(self, fileName): + def loadFromFile(self, fileName) -> None: f = QFile(fileName) if not f.open(QFile.ReadOnly): model = QStringListModel() @@ -63,7 +70,7 @@ def loadFromFile(self, fileName): line = f.readLine().trimmed() if line.length() != 0: try: - line = str(line, encoding='utf-8') + line = str(line, encoding="utf-8") except TypeError: line = str(line) @@ -74,14 +81,20 @@ def loadFromFile(self, fileName): model = QStringListModel(words) self._completer.setModel(model) - def loadFromList(self, words): + def loadFromList(self, words) -> None: model = QStringListModel(words) self._completer.setModel(model) - def keyPressEvent(self, e): + def keyPressEvent(self, e) -> None: if self._completer.popup().isVisible(): # The following keys are forwarded by the completer to the widget. - if e.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab, Qt.Key_Backtab): + if e.key() in ( + Qt.Key_Enter, + Qt.Key_Return, + Qt.Key_Escape, + Qt.Key_Tab, + Qt.Key_Backtab, + ): e.ignore() # Let the completer do default behavior. return @@ -102,7 +115,12 @@ def keyPressEvent(self, e): hasModifier = (e.modifiers() != Qt.NoModifier) and not ctrlOrShift completionPrefix = self.textUnderCursor() - if not isShortcut and (hasModifier or not e.text() or len(completionPrefix) < 3 or e.text()[-1] in eow): + if not isShortcut and ( + hasModifier + or not e.text() + or len(completionPrefix) < 3 + or e.text()[-1] in eow + ): self._completer.popup().hide() return @@ -113,33 +131,38 @@ def keyPressEvent(self, e): ) cr = self.cursorRect() - cr.setWidth(self._completer.popup().sizeHintForColumn(0) + - self._completer.popup().verticalScrollBar().sizeHint().width()) + cr.setWidth( + self._completer.popup().sizeHintForColumn(0) + + self._completer.popup().verticalScrollBar().sizeHint().width() + ) self._completer.complete(cr) class MainWindow(QMainWindow): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self.setWindowTitle("Completer") self.textEdit = CompletingTextEdit() - self.textEdit.loadFromList(['dog', 'cat', 'carry', 'python', 'собака', 'foobar']) + self.textEdit.loadFromList( + ["dog", "cat", "carry", "python", "собака", "foobar", "editor", "egg", "foo", "bar", "tester"] + ) # OR: # self.textEdit.loadFromFile('wordlist.txt') self.textEdit.setPlainText( "This TextEdit provides autocompletions for words that have " "more than 3 characters. You can trigger autocompletion " - "using %s\n\n" % self.textEdit.completerSequence.toString(QKeySequence.NativeText)) + "using %s\n\n" + % self.textEdit.completerSequence.toString(QKeySequence.NativeText) + ) self.setCentralWidget(self.textEdit) -if __name__ == '__main__': - import sys +if __name__ == "__main__": app = QApplication(sys.argv) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/EgoDialog.py b/qt__pyqt__pyside__pyqode/EgoDialog.py index 41aedb943..d4bbf7b9a 100644 --- a/qt__pyqt__pyside__pyqode/EgoDialog.py +++ b/qt__pyqt__pyside__pyqode/EgoDialog.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QMessageBox, QWidget @@ -10,7 +10,7 @@ class EgoDialog(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowFlag(Qt.FramelessWindowHint) @@ -29,19 +29,19 @@ def exec(title: str, text: str) -> bool: return ok == QMessageBox.Yes - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setBrush(QColor(0, 0, 0, 127)) painter.drawRect(self.rect()) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) - ok = EgoDialog.exec('Question', 'Question?') + ok = EgoDialog.exec("Question", "Question?") if ok: - print('User select Ok') + print("User select Ok") else: - print('User select No') + print("User select No") # app.exec() diff --git a/qt__pyqt__pyside__pyqode/ElidedLabel.py b/qt__pyqt__pyside__pyqode/ElidedLabel.py index 277df1ceb..5b1bdf5ae 100644 --- a/qt__pyqt__pyside__pyqode/ElidedLabel.py +++ b/qt__pyqt__pyside__pyqode/ElidedLabel.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QLabel @@ -10,7 +10,12 @@ class ElidedLabel(QLabel): - def paintEvent(self, event): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + self.setMinimumWidth(50) + + def paintEvent(self, event) -> None: painter = QPainter(self) metrics = QFontMetrics(self.font()) @@ -19,14 +24,14 @@ def paintEvent(self, event): painter.drawText(self.rect(), self.alignment(), elided_text) -if __name__ == '__main__': +if __name__ == "__main__": from PyQt5.QtWidgets import QApplication from PyQt5.QtGui import QFont app = QApplication([]) - mw = ElidedLabel('abc123' * 100) - mw.setFont(QFont('Arial', 20)) + mw = ElidedLabel("abc123" * 100) + mw.setFont(QFont("Arial", 20)) mw.resize(100, 100) mw.show() diff --git a/qt__pyqt__pyside__pyqode/FlowLayout.py b/qt__pyqt__pyside__pyqode/FlowLayout.py index 990c2bae3..c7cf4eb52 100644 --- a/qt__pyqt__pyside__pyqode/FlowLayout.py +++ b/qt__pyqt__pyside__pyqode/FlowLayout.py @@ -76,8 +76,12 @@ def doLayout(self, rect, testOnly): for item in self.itemList: wid = item.widget() - spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal) - spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical) + spaceX = self.spacing() + wid.style().layoutSpacing( + QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal + ) + spaceY = self.spacing() + wid.style().layoutSpacing( + QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical + ) nextX = x + item.sizeHint().width() + spaceX if nextX - spaceX > rect.right() and lineHeight > 0: x = rect.x() @@ -94,13 +98,12 @@ def doLayout(self, rect, testOnly): return y + lineHeight - rect.y() -if __name__ == '__main__': +if __name__ == "__main__": import sys - from PyQt5.QtWidgets import QWidget, QPushButton, QApplication class Window(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() flowLayout = FlowLayout() diff --git a/qt__pyqt__pyside__pyqode/GIPHY__gif/config.py b/qt__pyqt__pyside__pyqode/GIPHY__gif/config.py index bd0ee287e..ff9c1f653 100644 --- a/qt__pyqt__pyside__pyqode/GIPHY__gif/config.py +++ b/qt__pyqt__pyside__pyqode/GIPHY__gif/config.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os @@ -9,10 +9,10 @@ DIR = Path(__file__).resolve().parent -TOKEN_FILE_NAME = DIR / 'TOKEN.txt' +TOKEN_FILE_NAME = DIR / "TOKEN.txt" # SOURCE: https://developers.giphy.com/dashboard/ -GIPHY_API_KEY = os.environ.get('TOKEN') or TOKEN_FILE_NAME.read_text('utf-8').strip() +GIPHY_API_KEY = os.environ.get("TOKEN") or TOKEN_FILE_NAME.read_text("utf-8").strip() -TEMP_DIR = DIR / 'temp' +TEMP_DIR = DIR / "temp" TEMP_DIR.mkdir(exist_ok=True) diff --git a/qt__pyqt__pyside__pyqode/GIPHY__gif/main.py b/qt__pyqt__pyside__pyqode/GIPHY__gif/main.py index 49a5ed9ef..114f44686 100644 --- a/qt__pyqt__pyside__pyqode/GIPHY__gif/main.py +++ b/qt__pyqt__pyside__pyqode/GIPHY__gif/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/q/1134473/201445 @@ -14,8 +14,18 @@ import requests from PyQt5.QtWidgets import ( - QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QFrame, QMessageBox, - QLineEdit, QPushButton, QLabel, QScrollArea, QWidget, QGridLayout + QApplication, + QMainWindow, + QVBoxLayout, + QHBoxLayout, + QFrame, + QMessageBox, + QLineEdit, + QPushButton, + QLabel, + QScrollArea, + QWidget, + QGridLayout, ) from PyQt5.QtCore import QThread, pyqtSignal, Qt from PyQt5.QtGui import QMovie @@ -23,12 +33,12 @@ from config import GIPHY_API_KEY, TEMP_DIR -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) @@ -36,23 +46,23 @@ def log_uncaught_exceptions(ex_cls, ex, tb): class SearchGifThread(QThread): - SITE_URL = 'https://api.giphy.com/v1' + SITE_URL = "https://api.giphy.com/v1" about_add_gif = pyqtSignal(dict, int, int) - def __init__(self, name_gif=None): + def __init__(self, name_gif=None) -> None: super().__init__() self.name_gif = name_gif def get_gif(self) -> dict: - url = f'{self.SITE_URL}/gifs/search?api_key={GIPHY_API_KEY}&q={self.name_gif}' + url = f"{self.SITE_URL}/gifs/search?api_key={GIPHY_API_KEY}&q={self.name_gif}" try: rs = requests.get(url) rs.raise_for_status() - data = rs.json()['data'] + data = rs.json()["data"] if data: return data @@ -61,38 +71,43 @@ def get_gif(self) -> dict: print(e) # TODO: emit 'not found' to MainWindow - return {'error': True} + return {"error": True} def _process_gif(self, img: dict, index: int) -> dict: - url_gif = img['images']['fixed_width']['url'] + url_gif = img["images"]["fixed_width"]["url"] image_rs = requests.get(url_gif) image_rs.raise_for_status() file_name = TEMP_DIR / f"img{index}.gif" - with open(file_name, 'wb') as f: + with open(file_name, "wb") as f: f.write(image_rs.content) - data = {'row': index // 3, 'col': index % 3, 'error': None, 'file_name': str(file_name)} + data = { + "row": index // 3, + "col": index % 3, + "error": None, + "file_name": str(file_name), + } return data - def run(self): + def run(self) -> None: data = self.get_gif() - if 'error' in data: + if "error" in data: # TODO: emit error to MainWindow return for i, img in enumerate(data): data_gif = self._process_gif(img, i) - self.about_add_gif.emit(data_gif, i+1, len(data)) + self.about_add_gif.emit(data_gif, i + 1, len(data)) time.sleep(1) class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.title = 'Gif Manager' + self.title = "Gif Manager" self.setWindowTitle(self.title) self.search_gif_thread = SearchGifThread() @@ -102,12 +117,12 @@ def __init__(self): self.init_ui() - def init_ui(self): + def init_ui(self) -> None: self.gif_edit = QLineEdit() self.gif_edit.returnPressed.connect(self.search_gif) - self.gif_edit.setPlaceholderText('Enter name gif') + self.gif_edit.setPlaceholderText("Enter name gif") - gif_search_button = QPushButton('Search gif') + gif_search_button = QPushButton("Search gif") gif_search_button.clicked.connect(self.search_gif) gif_data_layout = QHBoxLayout() @@ -140,15 +155,15 @@ def init_ui(self): root_layout.addWidget(self.info_label) root_layout.addWidget(self.scroll) - def on_finish(self): + def on_finish(self) -> None: self.gif_data_frame.setEnabled(True) - def on_start(self): + def on_start(self) -> None: self.info_label.hide() self.scroll.show() self.gif_data_frame.setEnabled(False) - def search_gif(self): + def search_gif(self) -> None: # TODO: replace on QListWidget for i in range(self.gifs_layout.count()): self.gifs_layout.itemAt(i).widget().deleteLater() @@ -156,10 +171,10 @@ def search_gif(self): self.search_gif_thread.name_gif = self.gif_edit.text() self.search_gif_thread.start() - def add_gif(self, data: dict, num: int, total: int): - self.setWindowTitle(f'{self.title}. {num} / {total}') + def add_gif(self, data: dict, num: int, total: int) -> None: + self.setWindowTitle(f"{self.title}. {num} / {total}") - if data['error']: + if data["error"]: self.gif_data_frame.setEnabled(True) self.scroll.hide() self.info_label.show() @@ -183,6 +198,6 @@ def add_gif(self, data: dict, num: int, total: int): mw.resize(800, 600) mw.show() - mw.gif_edit.setText('cat') + mw.gif_edit.setText("cat") sys.exit(app.exec_()) diff --git a/qt__pyqt__pyside__pyqode/Good_text_foreground_color_for_a_given_background_color.py b/qt__pyqt__pyside__pyqode/Good_text_foreground_color_for_a_given_background_color.py index 2af77002d..56467789b 100644 --- a/qt__pyqt__pyside__pyqode/Good_text_foreground_color_for_a_given_background_color.py +++ b/qt__pyqt__pyside__pyqode/Good_text_foreground_color_for_a_given_background_color.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtGui import QColor diff --git a/qt__pyqt__pyside__pyqode/ImageLabel__QGraphicsView/main.py b/qt__pyqt__pyside__pyqode/ImageLabel__QGraphicsView/main.py index eb4e208f8..db0860632 100644 --- a/qt__pyqt__pyside__pyqode/ImageLabel__QGraphicsView/main.py +++ b/qt__pyqt__pyside__pyqode/ImageLabel__QGraphicsView/main.py @@ -1,38 +1,43 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem +from PyQt5.QtWidgets import ( + QApplication, + QGraphicsView, + QGraphicsScene, + QGraphicsPixmapItem, +) from PyQt5.QtGui import QPixmap from PyQt5.QtCore import Qt class ImageLabel(QGraphicsView): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.setScene(QGraphicsScene()) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - def setImage(self, filename: str): + def setImage(self, filename: str) -> None: self.setPixmap(QPixmap(filename)) - def setPixmap(self, pixmap: QPixmap): + def setPixmap(self, pixmap: QPixmap) -> None: item = QGraphicsPixmapItem(pixmap) item.setTransformationMode(Qt.SmoothTransformation) self.scene().addItem(item) - def resizeEvent(self, event): + def resizeEvent(self, event) -> None: super().resizeEvent(event) rect = self.scene().itemsBoundingRect() self.fitInView(rect, Qt.KeepAspectRatio) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) screenshot = app.primaryScreen().grabWindow(QApplication.desktop().winId()) diff --git a/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/IP_Validator.py b/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/IP_Validator.py index f999c1b98..702592e7d 100644 --- a/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/IP_Validator.py +++ b/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/IP_Validator.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtGui import QRegularExpressionValidator diff --git a/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/is_ip.py b/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/is_ip.py index 21b9ebbda..a2f49e2fc 100644 --- a/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/is_ip.py +++ b/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/is_ip.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import re @@ -9,17 +9,21 @@ # SOURCE: https://stackoverflow.com/a/23166561/5909792 IP_RANGE = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])" -IP_REGEXP = f'^{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}$' +IP_REGEXP = rf"^{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}\.{IP_RANGE}$" def is_ip(ip: str) -> bool: return bool(re.match(IP_REGEXP, ip)) -if __name__ == '__main__': +if __name__ == "__main__": tests = [ - ('127.0.0.1', True), ('100.100.100.100', True), ('0.0.0.0', True), ('300.300.300.300', False), - ('300.0.0.1', False), ('255.255.255.255', True) + ("127.0.0.1", True), + ("100.100.100.100", True), + ("0.0.0.0", True), + ("300.300.300.300", False), + ("300.0.0.1", False), + ("255.255.255.255", True), ] for ip, expected in tests: diff --git a/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/main.py b/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/main.py index b7c01b9a0..e8c03e338 100644 --- a/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/main.py +++ b/qt__pyqt__pyside__pyqode/Is_IP__QRegExpValidator/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QLineEdit, QApplication @@ -9,7 +9,7 @@ from IP_Validator import get_ip_validator -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = QLineEdit() diff --git a/qt__pyqt__pyside__pyqode/OID__QValidator.py b/qt__pyqt__pyside__pyqode/OID__QValidator.py index ad1eff961..cfdd10e50 100644 --- a/qt__pyqt__pyside__pyqode/OID__QValidator.py +++ b/qt__pyqt__pyside__pyqode/OID__QValidator.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QLineEdit @@ -12,8 +12,8 @@ app = QApplication([]) mw = QLineEdit() -mw.setText('1.3.6.1.2.1.25.1.1.0') -mw.setValidator(QRegExpValidator(QRegExp(r'\d+(\.\d+)+'))) +mw.setText("1.3.6.1.2.1.25.1.1.0") +mw.setValidator(QRegExpValidator(QRegExp(r"\d+(\.\d+)+"))) mw.show() app.exec() diff --git a/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QPainter/main.py b/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QPainter/main.py index b2c0036fe..1b5cf743b 100644 --- a/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QPainter/main.py +++ b/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QPainter/main.py @@ -1,32 +1,59 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/a/970882/201445 # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/d50de0f237d778f7d69ee75e328503f8c8344743/qt__pyqt__pyside__pyqode/QWebEngineView__one_ball__in_outer_circle_area__collision_with_mouse/index.html +import traceback +import sys + +from dataclasses import dataclass from pathlib import Path from timeit import default_timer -from PyQt5.Qt import QApplication, QWidget, QPainter, QTimer, Qt, QPoint, QPen +from PyQt5.QtCore import QTimer, Qt, QPointF +from PyQt5.QtGui import QPainter, QPen +from PyQt5.QtWidgets import QApplication, QMessageBox, QWidget + + +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}\n" + text += "".join(traceback.format_tb(tb)) + print(text) + + if QApplication.instance(): + msg_box = QMessageBox( + QMessageBox.Critical, + "Error", + f"Error: {ex}", + parent=None, + ) + msg_box.setDetailedText(text) + msg_box.setStandardButtons(QMessageBox.Ok) + msg_box.exec() + + +sys.excepthook = log_uncaught_exceptions +@dataclass class Ball: - r = 50 # Радиус шарика - x = 0 # Координата по х центра шарика - y = 0 # Координата по y центра шарика - speed = 0 # Скорость движения - dir_x = 0 # Компонент x вектора движения шарика - dir_y = 0 # Компонент y вектора движения шарика - damp = 10 # Скорость уменьшения скорости движения (сопротивление) - collision = False # Признак коллизии с внешним кругом - speed_after_collision = 300 # Скорость движения шарика после столкновения + r: float = 50 # Радиус шарика + x: float = 0 # Координата по х центра шарика + y: float = 0 # Координата по y центра шарика + speed: float = 0 # Скорость движения + dir_x: float = 0 # Компонент x вектора движения шарика + dir_y: float = 0 # Компонент y вектора движения шарика + damp: float = 10 # Скорость уменьшения скорости движения (сопротивление) + collision: bool = False # Признак коллизии с внешним кругом + speed_after_collision: float = 300 # Скорость движения шарика после столкновения # Функция, которая проверяет наличие коллизии шарика с внешним кругом - def hit_outer_circle_check(self, outer_circle: int): + def hit_outer_circle_check(self, outer_circle: int) -> None: dr = outer_circle - self.r # Разница радиусов # По теореме пифагора проверяем выход за пределы круга (коллизию) @@ -69,7 +96,7 @@ def hit_outer_circle_check(self, outer_circle: int): self.collision = False # Функция проверки коллизии шарика и мышки - def hit_mouse_check(self, x, y): + def hit_mouse_check(self, x, y) -> None: # Если есть коллизия с внешним кругом игнорируем мышку if self.collision: return @@ -93,7 +120,7 @@ def hit_mouse_check(self, x, y): # Тут осуществляется передвижение # dt - кол-во секунд с прошлого обсчета - def do_move(self, dt): + def do_move(self, dt: float) -> None: # К текущей координате прибавляем вектор скорости помноженный # на значение скорости помноженные на прошедшее время self.x += self.dir_x * self.speed * dt @@ -104,17 +131,15 @@ def do_move(self, dt): class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - title = Path(__file__).parent.name - self.setWindowTitle(title) - - timeout = 1000 // 60 + timeout: int = 1000 // 60 # Таймер обновления движения и обработки столкновения шариков self.timer = QTimer() self.timer.timeout.connect(self.tick) + self.timer.timeout.connect(self.update_window_title) self.timer.start(timeout) # NOTE: Перерисовку помести в tick, но этот вариант с отдельным таймером тоже @@ -124,19 +149,26 @@ def __init__(self): # self.timer_render.timeout.connect(self.update) # self.timer_render.start(timeout) - self.outer_circle = 195 + self.outer_circle: int = 195 self.ball = Ball() - self.mouse_center_x = 0 - self.mouse_center_y = 0 + self.mouse_center_x: int = 0 + self.mouse_center_y: int = 0 # Используется, чтобы в независимости от количества вызовов # tick скорость шарика была одинаковая - self.t = 0 + self.t: float = 0 self.setMouseTracking(True) - def tick(self): + self.update_window_title() + + def update_window_title(self) -> None: + name = Path(__file__).parent.name + title = f"{name}. Speed: {self.ball.speed}" + self.setWindowTitle(title) + + def tick(self) -> None: # Считаем сколько времени прошло с прошлого обсчета dt = default_timer() - self.t @@ -148,11 +180,11 @@ def tick(self): self.update() - def mouseMoveEvent(self, event): + def mouseMoveEvent(self, event) -> None: self.mouse_center_x = event.pos().x() - (self.width() / 2) self.mouse_center_y = event.pos().y() - (self.height() / 2) - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setRenderHint(QPainter.HighQualityAntialiasing) @@ -161,11 +193,11 @@ def paintEvent(self, event): painter.translate(self.width() / 2, self.height() / 2) - painter.drawEllipse(QPoint(0, 0), self.outer_circle, self.outer_circle) - painter.drawEllipse(QPoint(self.ball.x, self.ball.y), self.ball.r, self.ball.r) + painter.drawEllipse(QPointF(0, 0), self.outer_circle, self.outer_circle) + painter.drawEllipse(QPointF(self.ball.x, self.ball.y), self.ball.r, self.ball.r) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QWebEngineView/main.py b/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QWebEngineView/main.py index 8abe15c2c..e3ef79230 100644 --- a/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QWebEngineView/main.py +++ b/qt__pyqt__pyside__pyqode/One_ball__in_outer_circle_area__collision_with_mouse__QWebEngineView/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/a/970882/201445 @@ -13,8 +13,8 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView -if __name__ == '__main__': - with open('index.html', encoding='utf-8') as f: +if __name__ == "__main__": + with open("index.html", encoding="utf-8") as f: html = f.read() title = Path(__file__).parent.name @@ -27,4 +27,3 @@ view.show() app.exec() - diff --git a/qt__pyqt__pyside__pyqode/PyQt6_screens_size.py b/qt__pyqt__pyside__pyqode/PyQt6_screens_size.py new file mode 100644 index 000000000..ec7232233 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/PyQt6_screens_size.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt6.QtWidgets import QApplication +from PyQt6.QtGui import QGuiApplication + + +app = QApplication([]) +for screen in QApplication.screens(): + print(screen.geometry(), screen.size()) +""" +PyQt6.QtCore.QRect(0, 0, 1920, 1080) PyQt6.QtCore.QSize(1920, 1080) +PyQt6.QtCore.QRect(-3840, 0, 1536, 864) PyQt6.QtCore.QSize(1536, 864) +PyQt6.QtCore.QRect(-1920, 0, 1920, 1080) PyQt6.QtCore.QSize(1920, 1080) +""" + +app = None +print() + +app = QGuiApplication([]) +for screen in QGuiApplication.screens(): + print(screen.geometry(), screen.size()) +""" +PyQt6.QtCore.QRect(0, 0, 1920, 1080) PyQt6.QtCore.QSize(1920, 1080) +PyQt6.QtCore.QRect(-3840, 0, 1536, 864) PyQt6.QtCore.QSize(1536, 864) +PyQt6.QtCore.QRect(-1920, 0, 1920, 1080) PyQt6.QtCore.QSize(1920, 1080) +""" diff --git a/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/draw_table.py b/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/draw_table.py index a8621542b..03c97bf5c 100644 --- a/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/draw_table.py +++ b/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/draw_table.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from random import randint import sys - +from random import randint from PyQt5.QtWidgets import * from PyQt5.QtGui import * @@ -14,9 +13,9 @@ class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('Widget') + self.setWindowTitle("Widget") self.cell_size = 20 self.matrix_size = 9 @@ -35,7 +34,7 @@ def __init__(self): self.setMouseTracking(True) - def resizeEvent(self, event): + def resizeEvent(self, event) -> None: super().resizeEvent(event) w, h = event.size().width(), event.size().height() @@ -43,7 +42,7 @@ def resizeEvent(self, event): self.cell_size = min_size // self.matrix_size - def mouseMoveEvent(self, event): + def mouseMoveEvent(self, event) -> None: super().mouseMoveEvent(event) pos = event.pos() @@ -53,13 +52,16 @@ def mouseMoveEvent(self, event): self.update() - def paintEvent(self, event): + def paintEvent(self, event) -> None: super().paintEvent(event) painter = QPainter(self) # Если индекс ячейки под курсором валидный - if 0 <= self.x_highlight_cell < self.matrix_size and 0 <= self.y_highlight_cell < self.matrix_size: + if ( + 0 <= self.x_highlight_cell < self.matrix_size + and 0 <= self.y_highlight_cell < self.matrix_size + ): # Выделение всего столбца и строки пересекающих ячейку под курсором painter.save() painter.setBrush(Qt.lightGray) @@ -70,7 +72,7 @@ def paintEvent(self, event): i * self.cell_size, self.y_highlight_cell * self.cell_size, self.cell_size, - self.cell_size + self.cell_size, ) # Выделение столбца @@ -79,7 +81,7 @@ def paintEvent(self, event): self.x_highlight_cell * self.cell_size, j * self.cell_size, self.cell_size, - self.cell_size + self.cell_size, ) painter.restore() @@ -91,7 +93,7 @@ def paintEvent(self, event): self.x_highlight_cell * self.cell_size, self.y_highlight_cell * self.cell_size, self.cell_size, - self.cell_size + self.cell_size, ) painter.restore() @@ -131,7 +133,7 @@ def paintEvent(self, event): x2 += self.cell_size -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/setup.py b/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/setup.py index 882963664..7f6db329a 100644 --- a/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/setup.py +++ b/qt__pyqt__pyside__pyqode/PySide_examples/draw_table/setup.py @@ -9,12 +9,8 @@ from cx_Freeze import setup, Executable -executables = [ - Executable('draw_table.py') -] +executables = [Executable("draw_table.py")] -setup(name='draw_table', - version='0.1', - description='draw_table', - executables=executables - ) +setup( + name="draw_table", version="0.1", description="draw_table", executables=executables +) diff --git a/qt__pyqt__pyside__pyqode/PySide_examples/e1.py b/qt__pyqt__pyside__pyqode/PySide_examples/e1.py index 600ecf9c9..6d43f7ab5 100644 --- a/qt__pyqt__pyside__pyqode/PySide_examples/e1.py +++ b/qt__pyqt__pyside__pyqode/PySide_examples/e1.py @@ -1,17 +1,18 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Простой пример использования модуля PySide from PySide.QtGui import * -# from PySide.QtCore import * -import sys -if __name__ == '__main__': + +if __name__ == "__main__": + import sys + app = QApplication(sys.argv) - def says(): + def says() -> None: print("Hello, Python!") label = QLabel("Hello, PySize!") @@ -33,4 +34,4 @@ def says(): window.setLayout(layout) window.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/qt__pyqt__pyside__pyqode/PySide_examples/e2.py b/qt__pyqt__pyside__pyqode/PySide_examples/e2.py index bdf84542b..5d60a5f05 100644 --- a/qt__pyqt__pyside__pyqode/PySide_examples/e2.py +++ b/qt__pyqt__pyside__pyqode/PySide_examples/e2.py @@ -1,4 +1,4 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" # Пример показывает как можно создать простое окно с небольшим функционалом, @@ -11,7 +11,7 @@ class SimpleWindow(QtGui.QWidget): """Просто окно""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.setWindowTitle("Простое окно") @@ -30,18 +30,18 @@ def __init__(self, *args, **kwargs): self.setLayout(layout_main) - def slot_add_log(self): + def slot_add_log(self) -> None: text = self.le_mess.text() if text: self.lw_log.addItem(text) -import sys +if __name__ == "__main__": + import sys -if __name__ == '__main__': app = QtGui.QApplication(sys.argv) w = SimpleWindow() w.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/qt__pyqt__pyside__pyqode/PySide_examples/e3.py b/qt__pyqt__pyside__pyqode/PySide_examples/e3.py index d756ac811..5bdfbe286 100644 --- a/qt__pyqt__pyside__pyqode/PySide_examples/e3.py +++ b/qt__pyqt__pyside__pyqode/PySide_examples/e3.py @@ -1,11 +1,11 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" from PySide.QtGui import * class MainWindow(QMainWindow): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.editor = QTextEdit() self.setCentralWidget(self.editor) @@ -19,25 +19,25 @@ def __init__(self, *args, **kwargs): self.edit_menu = self.menuBar().addMenu("Правка") self.edit_menu.aboutToShow.connect(self.update_edit_menu) - def update_edit_menu(self): + def update_edit_menu(self) -> None: self.edit_menu.clear() actions = self.editor.createStandardContextMenu().actions() self.edit_menu.addActions(actions) - def slot_save_as(self): + def slot_save_as(self) -> None: file_name = QFileDialog.getSaveFileName()[0] if file_name: - with open(file_name, 'w') as f: - f.write(self.editor.toPlainText()) + with open(file_name, "w") as f: + f.write(self.editor.toPlainText()) -if __name__ == '__main__': +if __name__ == "__main__": import sys app = QApplication(sys.argv) - app.setApplicationName('myWindow') + app.setApplicationName("myWindow") w = MainWindow() w.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/qt__pyqt__pyside__pyqode/PySide_examples/e4.py b/qt__pyqt__pyside__pyqode/PySide_examples/e4.py index 17b03bb5c..736d1b344 100644 --- a/qt__pyqt__pyside__pyqode/PySide_examples/e4.py +++ b/qt__pyqt__pyside__pyqode/PySide_examples/e4.py @@ -1,51 +1,55 @@ -__author__ = 'ipetrash' +__author__ = "ipetrash" +import time +from threading import Thread + from grab import Grab -import sys from PySide.QtGui import * -from threading import Thread -import time def get_confucius_quotes(): g = Grab() # http://ru.wikiquote.org/wiki/Конфуций - url = "http://ru.wikiquote.org/wiki/%D0%9A%D0%BE%D0%BD%D1%84%D1%83%D1%86%D0%B8%D0%B9" + url = ( + "http://ru.wikiquote.org/wiki/%D0%9A%D0%BE%D0%BD%D1%84%D1%83%D1%86%D0%B8%D0%B9" + ) g.go(url) quotes = list() - for el in g.doc.select('//h2/following-sibling::ul/li'): + for el in g.doc.select("//h2/following-sibling::ul/li"): quotes.append(el.text()) return quotes class MyThread(Thread): - def __init__(self, log): + def __init__(self, log) -> None: super().__init__() self.log = log - def run(self): + def run(self) -> None: for i, quote in enumerate(get_confucius_quotes(), 1): - self.log.append('{}. "{}"\n'.format(i, quote)) + self.log.append(f'{i}. "{quote}"\n') time.sleep(0.1) # задержка каждые 100 миллисекунд class Window(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.te_quotes = QTextEdit() self.te_quotes.setReadOnly(True) self.setCentralWidget(self.te_quotes) - def slot_refresh(self): + def slot_refresh(self) -> None: self.te_quotes.clear() thread = MyThread(self.te_quotes) thread.start() -if __name__ == '__main__': +if __name__ == "__main__": + import sys + app = QApplication(sys.argv) w = Window() diff --git a/qt__pyqt__pyside__pyqode/PySide_examples/infinite_input.py b/qt__pyqt__pyside__pyqode/PySide_examples/infinite_input.py index ecb58f731..8390f5d6b 100644 --- a/qt__pyqt__pyside__pyqode/PySide_examples/infinite_input.py +++ b/qt__pyqt__pyside__pyqode/PySide_examples/infinite_input.py @@ -1,17 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import sys - from PySide.QtGui import * from PySide.QtCore import * class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.layout = QVBoxLayout() @@ -21,14 +19,14 @@ def __init__(self): self._add_line_edit() - def _add_line_edit(self): + def _add_line_edit(self) -> None: line_edit = QLineEdit() line_edit.textEdited.connect(self._text_edited) self.layout.addWidget(line_edit) self.line_edit_list.append(line_edit) - def _text_edited(self, text): + def _text_edited(self, text) -> None: if not self.line_edit_list: return @@ -36,7 +34,9 @@ def _text_edited(self, text): self._add_line_edit() -if __name__ == '__main__': +if __name__ == "__main__": + import sys + app = QApplication(sys.argv) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/QApplication_keyboardModifiers.py b/qt__pyqt__pyside__pyqode/QApplication_keyboardModifiers.py index ef79d74f1..c10d47b94 100644 --- a/qt__pyqt__pyside__pyqode/QApplication_keyboardModifiers.py +++ b/qt__pyqt__pyside__pyqode/QApplication_keyboardModifiers.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QPushButton @@ -9,10 +9,10 @@ class Window(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.button = QPushButton('Test') + self.button = QPushButton("Test") self.button.clicked.connect(self.on_clicked) main_layout = QVBoxLayout() @@ -20,13 +20,13 @@ def __init__(self): self.setLayout(main_layout) - def foo(self): - print('foo') + def foo(self) -> None: + print("foo") - def bar(self): - print('bar') + def bar(self) -> None: + print("bar") - def on_clicked(self): + def on_clicked(self) -> None: modifiers = QApplication.keyboardModifiers() if modifiers == Qt.AltModifier: @@ -35,7 +35,7 @@ def on_clicked(self): self.foo() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = Window() diff --git a/qt__pyqt__pyside__pyqode/QApplication_show_screens.py b/qt__pyqt__pyside__pyqode/QApplication_show_screens.py index b8be5b4ac..0c21d0bf2 100644 --- a/qt__pyqt__pyside__pyqode/QApplication_show_screens.py +++ b/qt__pyqt__pyside__pyqode/QApplication_show_screens.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication @@ -10,8 +10,7 @@ app = QApplication([]) screens = app.screens() -print(f'Screens ({len(screens)}):') +print(f"Screens ({len(screens)}):") for screen in screens: - print(f' {screen.availableGeometry()} {screen.availableSize()}') - + print(f" {screen.availableGeometry()} {screen.availableSize()}") diff --git a/qt__pyqt__pyside__pyqode/QAxContainer_Word_Application.py b/qt__pyqt__pyside__pyqode/QAxContainer_Word_Application.py index 2236a70c6..69357fd5a 100644 --- a/qt__pyqt__pyside__pyqode/QAxContainer_Word_Application.py +++ b/qt__pyqt__pyside__pyqode/QAxContainer_Word_Application.py @@ -1,25 +1,30 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys from PyQt5.QAxContainer import QAxWidget from PyQt5.QtWidgets import ( - QWidget, QGridLayout, QPushButton, QFileDialog, QMessageBox, QApplication + QWidget, + QGridLayout, + QPushButton, + QFileDialog, + QMessageBox, + QApplication, ) class Window(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('Document Word.Application') + self.setWindowTitle("Document Word.Application") self.axWidget = QAxWidget(self) - self.buttonOpen = QPushButton('Open') + self.buttonOpen = QPushButton("Open") self.buttonOpen.clicked.connect(self.handleOpen) layout = QGridLayout(self) @@ -28,26 +33,24 @@ def __init__(self): def handleOpen(self): path, _ = QFileDialog.getOpenFileName( - self, 'Выберите файл word', '', 'word(*.docx *.doc)' + self, "Выберите файл word", "", "word(*.docx *.doc)" ) if not path: return - return self.openOffice(path, 'Word.Application') + return self.openOffice(path, "Word.Application") def openOffice(self, path, app): self.axWidget.clear() if not self.axWidget.setControl(app): - return QMessageBox.critical(self, 'Ошибка', f'Нет установки {app!r}') + return QMessageBox.critical(self, "Ошибка", f"Нет установки {app!r}") - self.axWidget.dynamicCall( - 'SetVisible (bool Visible)', 'false' - ) - self.axWidget.setProperty('DisplayAlerts', False) + self.axWidget.dynamicCall("SetVisible (bool Visible)", "false") + self.axWidget.setProperty("DisplayAlerts", False) self.axWidget.setControl(path) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) mw = Window() diff --git a/qt__pyqt__pyside__pyqode/QFileSystemWatcher__examples/main.py b/qt__pyqt__pyside__pyqode/QFileSystemWatcher__examples/main.py index b2d06df0d..2c0192947 100644 --- a/qt__pyqt__pyside__pyqode/QFileSystemWatcher__examples/main.py +++ b/qt__pyqt__pyside__pyqode/QFileSystemWatcher__examples/main.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt from pathlib import Path from PyQt5.QtCore import QCoreApplication, QFileSystemWatcher @@ -17,13 +17,11 @@ watcher = QFileSystemWatcher([current_dir]) watcher.directoryChanged.connect( lambda directory: print( - f'[{DT.datetime.now():%d/%m/%Y %H:%M:%S}] Directory: {directory!r}' + f"[{dt.datetime.now():%d/%m/%Y %H:%M:%S}] Directory: {directory!r}" ) ) watcher.fileChanged.connect( - lambda file: print( - f'[{DT.datetime.now():%d/%m/%Y %H:%M:%S}] File: {file!r}' - ) + lambda file: print(f"[{dt.datetime.now():%d/%m/%Y %H:%M:%S}] File: {file!r}") ) app.exec_() diff --git a/qt__pyqt__pyside__pyqode/QImageReader.supportedImageFormats.py b/qt__pyqt__pyside__pyqode/QImageReader.supportedImageFormats.py index cc5d7e465..fa1511990 100644 --- a/qt__pyqt__pyside__pyqode/QImageReader.supportedImageFormats.py +++ b/qt__pyqt__pyside__pyqode/QImageReader.supportedImageFormats.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtGui import QImageReader -print( - list(map(lambda x: x.data().decode(), QImageReader.supportedImageFormats())) -) + +print(list(map(lambda x: x.data().decode(), QImageReader.supportedImageFormats()))) # ['bmp', 'cur', 'gif', 'icns', 'ico', 'jpeg', 'jpg', 'pbm', 'pgm', 'png', 'ppm', 'svg', 'svgz', 'tga', 'tif', 'tiff', 'wbmp', 'webp', 'xbm', 'xpm'] diff --git a/qt__pyqt__pyside__pyqode/QLabel__enter_leave__with_pixmap/main.py b/qt__pyqt__pyside__pyqode/QLabel__enter_leave__with_pixmap/main.py index 503552ec1..24b062c28 100644 --- a/qt__pyqt__pyside__pyqode/QLabel__enter_leave__with_pixmap/main.py +++ b/qt__pyqt__pyside__pyqode/QLabel__enter_leave__with_pixmap/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QLabel @@ -9,23 +9,23 @@ class Label(QLabel): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.pixmap = QPixmap('52rQ3.png') + self.pixmap = QPixmap("52rQ3.png") self.pixmap_leave = self.pixmap.copy(0, 0, 26, 26) self.pixmap_enter = self.pixmap.copy(26, 0, 52, 26) self.setPixmap(self.pixmap_leave) - def enterEvent(self, event): + def enterEvent(self, event) -> None: self.setPixmap(self.pixmap_enter) - def leaveEvent(self, event): + def leaveEvent(self, event) -> None: self.setPixmap(self.pixmap_leave) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = Label() diff --git a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__QFont.py b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__QFont.py index 3ec556b50..d2b09cff9 100644 --- a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__QFont.py +++ b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__QFont.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QLabel, QVBoxLayout, QWidget + app = QApplication([]) label_1 = QLabel() -label_1.setText('Hello World!!!') +label_1.setText("Hello World!!!") font = label_1.font() font.setItalic(True) @@ -17,7 +18,7 @@ label_1.setFont(font) label_2 = QLabel() -label_2.setText('nothing...') +label_2.setText("nothing...") layout = QVBoxLayout() layout.addWidget(label_1) diff --git a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__html.py b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__html.py index 2f214642b..897b864f6 100644 --- a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__html.py +++ b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__html.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QLabel, QVBoxLayout, QWidget + app = QApplication([]) label_1 = QLabel() -label_1.setText('Hello World!!!') +label_1.setText("Hello World!!!") label_2 = QLabel() -label_2.setText('nothing...') +label_2.setText("nothing...") layout = QVBoxLayout() layout.addWidget(label_1) diff --git a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__by_object_name.py b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__by_object_name.py index 2856d3e7e..749dbe34a 100644 --- a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__by_object_name.py +++ b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__by_object_name.py @@ -1,30 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QLabel, QVBoxLayout, QWidget + app = QApplication([]) -app.setStyleSheet(''' +app.setStyleSheet( + """ #welcome { font: bold italic } #label_foo_bar { font: italic; color: green; } -''') +""" +) label_1 = QLabel() -label_1.setObjectName('welcome') -label_1.setText('Hello World!!!') +label_1.setObjectName("welcome") +label_1.setText("Hello World!!!") label_2 = QLabel() -label_2.setText('nothing...') +label_2.setText("nothing...") label_3 = QLabel() -label_3.setObjectName('label_foo_bar') -label_3.setText('FooBar') +label_3.setObjectName("label_foo_bar") +label_3.setText("FooBar") layout = QVBoxLayout() layout.addWidget(label_1) diff --git a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__global.py b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__global.py index ddbf73edb..096fd41ca 100644 --- a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__global.py +++ b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__global.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QLabel, QVBoxLayout, QWidget + app = QApplication([]) -app.setStyleSheet('QLabel { font: bold italic }') +app.setStyleSheet("QLabel { font: bold italic }") label_1 = QLabel() -label_1.setText('Hello World!!!') +label_1.setText("Hello World!!!") label_2 = QLabel() -label_2.setText('nothing...') +label_2.setText("nothing...") layout = QVBoxLayout() layout.addWidget(label_1) diff --git a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__local.py b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__local.py index c8beffea6..4ff050f43 100644 --- a/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__local.py +++ b/qt__pyqt__pyside__pyqode/QLabel_bold_italic_text/use__qss__qt_style_sheet__local.py @@ -1,23 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QLabel, QVBoxLayout, QWidget + app = QApplication([]) label_1 = QLabel() -label_1.setText('Hello World!!!') -label_1.setStyleSheet(""" +label_1.setText("Hello World!!!") +label_1.setStyleSheet( + """ font: bold italic; color: green; background-color: black; -""") +""" +) label_2 = QLabel() -label_2.setText('nothing...') +label_2.setText("nothing...") layout = QVBoxLayout() layout.addWidget(label_1) diff --git a/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__QThread.py b/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__QThread.py index 45766a589..8fbbf55c1 100644 --- a/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__QThread.py +++ b/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__QThread.py @@ -1,23 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import time - from PyQt5.Qt import QMainWindow, QLabel, QFont, QApplication, Qt, QThread class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.label = QLabel() self.label.setAlignment(Qt.AlignCenter) - self.label.setFont(QFont('Courier', 25)) + self.label.setFont(QFont("Courier", 25)) - def loop(): + def loop() -> None: i = 0 while True: @@ -27,7 +26,7 @@ def loop(): time.sleep(1) class Thread(QThread): - def run(self): + def run(self) -> None: loop() self.thread = Thread() @@ -36,7 +35,7 @@ def run(self): self.setCentralWidget(self.label) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__threading.py b/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__threading.py index 75e644223..80d27525b 100644 --- a/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__threading.py +++ b/qt__pyqt__pyside__pyqode/QLabel_setText__in_thread__threading.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import threading @@ -11,14 +11,14 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.label = QLabel() self.label.setAlignment(Qt.AlignCenter) - self.label.setFont(QFont('Courier', 25)) + self.label.setFont(QFont("Courier", 25)) - def loop(): + def loop() -> None: i = 0 while True: @@ -33,7 +33,7 @@ def loop(): self.setCentralWidget(self.label) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QLibraryInfo_location.py b/qt__pyqt__pyside__pyqode/QLibraryInfo_location.py new file mode 100644 index 000000000..9d6ebb392 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QLibraryInfo_location.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtCore import QLibraryInfo + + +print(QLibraryInfo.location(QLibraryInfo.TranslationsPath)) +print(QLibraryInfo.location(QLibraryInfo.PluginsPath)) +print(QLibraryInfo.location(QLibraryInfo.ExamplesPath)) +""" +C:/Users/ipetrash/AppData/Local/Programs/Python/Python310/lib/site-packages/PyQt5/Qt5/translations +C:/Users/ipetrash/AppData/Local/Programs/Python/Python310/lib/site-packages/PyQt5/Qt5/plugins +C:/Users/ipetrash/AppData/Local/Programs/Python/Python310/lib/site-packages/PyQt5/Qt5/examples +""" diff --git a/qt__pyqt__pyside__pyqode/QLineEdit__mask.py b/qt__pyqt__pyside__pyqode/QLineEdit__mask.py index 934aad3f4..934c4f95c 100644 --- a/qt__pyqt__pyside__pyqode/QLineEdit__mask.py +++ b/qt__pyqt__pyside__pyqode/QLineEdit__mask.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://doc.qt.io/qt-5/qlineedit.html#inputMask-prop @@ -11,7 +11,7 @@ class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.mask_qt = QLineEdit("+7([000])[000]-[0000]") @@ -25,14 +25,14 @@ def __init__(self): self.setLayout(layout) - def _on_changed_mask(self, mask): + def _on_changed_mask(self, mask) -> None: text = self.test_input.text() self.test_input.setInputMask(mask) self.test_input.setText(text) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QListWidget_with_custom_widgets.py b/qt__pyqt__pyside__pyqode/QListWidget_with_custom_widgets.py index 5473ae443..9bf014214 100644 --- a/qt__pyqt__pyside__pyqode/QListWidget_with_custom_widgets.py +++ b/qt__pyqt__pyside__pyqode/QListWidget_with_custom_widgets.py @@ -1,38 +1,53 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt -from PyQt5.QtWidgets import QApplication, QListWidget, QListWidgetItem, QFrame, QFormLayout, QLabel +from PyQt5.QtWidgets import ( + QApplication, + QListWidget, + QListWidgetItem, + QFrame, + QFormLayout, + QLabel, +) from PyQt5.QtCore import Qt class ListItem(QFrame): - def __init__(self, host: str, port: int, status: str = 'Not checked!', last_check_time: DT.datetime = None): + def __init__( + self, + host: str, + port: int, + status: str = "Not checked!", + last_check_time: dt.datetime = None, + ) -> None: super().__init__() if not last_check_time: - last_check_time = DT.datetime.now() + last_check_time = dt.datetime.now() self.setFrameShape(QFrame.Box) self.setFrameShadow(QFrame.Plain) - self.label_address = QLabel(f'{host}:{port}') + self.label_address = QLabel(f"{host}:{port}") self.label_status = QLabel(status) - self.label_last_check_time = QLabel(last_check_time.strftime('%d/%m/%Y %H:%M:%S')) + self.label_last_check_time = QLabel( + last_check_time.strftime("%d/%m/%Y %H:%M:%S") + ) main_layout = QFormLayout(self) - main_layout.addRow('Address:', self.label_address) - main_layout.addRow('Status:', self.label_status) - main_layout.addRow('Last check time:', self.label_last_check_time) + main_layout.addRow("Address:", self.label_address) + main_layout.addRow("Status:", self.label_status) + main_layout.addRow("Last check time:", self.label_last_check_time) self.setFixedHeight(self.sizeHint().height()) -def add_item(list_widget: QListWidget, host: str, port: int): +def add_item(list_widget: QListWidget, host: str, port: int) -> None: widget = ListItem(host, port) item = QListWidgetItem() @@ -43,7 +58,7 @@ def add_item(list_widget: QListWidget, host: str, port: int): list_widget.setItemWidget(item, widget) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) list_widgets = QListWidget() @@ -52,10 +67,10 @@ def add_item(list_widget: QListWidget, host: str, port: int): list_widgets.itemClicked.connect(lambda item: print(item.data(Qt.UserRole))) - add_item(list_widgets, '127.0.0.1', 161) - add_item(list_widgets, '192.168.0.0', 161) + add_item(list_widgets, "127.0.0.1", 161) + add_item(list_widgets, "192.168.0.0", 161) for i in range(1, 10 + 1): - add_item(list_widgets, '127.0.0.1', 161 + i) + add_item(list_widgets, "127.0.0.1", 161 + i) list_widgets.show() diff --git a/qt__pyqt__pyside__pyqode/QML__examples/hello_text.qml b/qt__pyqt__pyside__pyqode/QML__examples/hello_text.qml new file mode 100644 index 000000000..001a12cf1 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QML__examples/hello_text.qml @@ -0,0 +1,23 @@ +import QtQuick 2.3 + +Rectangle { + width: 360 + height: 360 + + Text { + id: helloText + objectName: "helloText" + + text: qsTr("Click me!") + anchors.centerIn: parent + font.pointSize: 24 + font.bold: true + + MouseArea { + anchors.fill: parent + onClicked: { + helloText.text = qsTr("Hello World!"); + } + } + } +} \ No newline at end of file diff --git a/qt__pyqt__pyside__pyqode/QML__examples/hello_text_window.qml b/qt__pyqt__pyside__pyqode/QML__examples/hello_text_window.qml new file mode 100644 index 000000000..7daed8dc9 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QML__examples/hello_text_window.qml @@ -0,0 +1,23 @@ +import QtQuick 2.3 +import QtQuick.Window 2.0 + +Window { + visible: true + width: 640 + height: 480 + title: qsTr("Hello World") //The method qsTr() is used for translations from one language to other. + + Text { + id: helloText + text: qsTr("Click me!") + anchors.centerIn: parent + font.pointSize: 24; font.bold: true + + MouseArea { + anchors.fill: parent + onClicked: { + helloText.text = "Hello World!"; + } + } + } +} \ No newline at end of file diff --git a/qt__pyqt__pyside__pyqode/QML__examples/hello_world.py b/qt__pyqt__pyside__pyqode/QML__examples/hello_world.py new file mode 100644 index 000000000..ebeb67d04 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QML__examples/hello_world.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +from PyQt5.QtGui import QGuiApplication +from PyQt5.QtQuick import QQuickView +from PyQt5.QtCore import QUrl + + +app = QGuiApplication([]) + +qml_file = Path(__file__).parent.resolve() / "hello_text.qml" +url_qml_file = QUrl.fromLocalFile(str(qml_file)) + +view = QQuickView() +view.setTitle("Hello World!") +view.setSource(url_qml_file) +view.show() + +app.exec() diff --git a/qt__pyqt__pyside__pyqode/QML__examples/hello_world_window.py b/qt__pyqt__pyside__pyqode/QML__examples/hello_world_window.py new file mode 100644 index 000000000..64ffdd123 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QML__examples/hello_world_window.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from pathlib import Path + +from PyQt5.QtGui import QGuiApplication # NOTE: For only QML, QGuiApplication is enough +from PyQt5.QtQml import QQmlApplicationEngine +from PyQt5.QtCore import QUrl + + +app = QGuiApplication([]) + +qml_file_window = Path(__file__).parent.resolve() / "hello_text_window.qml" +url_qml_file_window = QUrl.fromLocalFile(str(qml_file_window)) + +engine = QQmlApplicationEngine(url_qml_file_window) + +app.exec() diff --git a/qt__pyqt__pyside__pyqode/QML__examples/hello_world_with_widgets.py b/qt__pyqt__pyside__pyqode/QML__examples/hello_world_with_widgets.py new file mode 100644 index 000000000..00fe3caf7 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/QML__examples/hello_world_with_widgets.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from datetime import datetime +from pathlib import Path + +from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout +from PyQt5.QtQuick import QQuickView, QQuickItem +from PyQt5.QtCore import QUrl + + +QML_FILE = Path(__file__).parent.resolve() / "hello_text.qml" +URL_QML_FILE = QUrl.fromLocalFile(str(QML_FILE)) + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + self.setWindowTitle("Hello World! (QML + Qt)") + + self.view = QQuickView() + self.view.setSource(URL_QML_FILE) + + pb_click = QPushButton("Click me!") + pb_click.clicked.connect(self._on_clicked) + + main_layout = QVBoxLayout(self) + main_layout.addWidget(QWidget.createWindowContainer(self.view)) + main_layout.addWidget(pb_click) + + def _on_clicked(self) -> None: + qml_hello_text = self.view.rootObject().findChild(QQuickItem, "helloText") + qml_hello_text.setProperty("text", f"Now: {datetime.now().time():%H:%M:%S}") + + +if __name__ == "__main__": + app = QApplication([]) + + view = QQuickView() + view.setTitle("Hello World! (single)") + view.setSource(URL_QML_FILE) + view.show() + + main_window = MainWindow() + main_window.resize(500, 500) + main_window.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/QMdiSubWindow__hello_world.py b/qt__pyqt__pyside__pyqode/QMdiSubWindow__hello_world.py index 7a5b0acc8..8584615d1 100644 --- a/qt__pyqt__pyside__pyqode/QMdiSubWindow__hello_world.py +++ b/qt__pyqt__pyside__pyqode/QMdiSubWindow__hello_world.py @@ -1,21 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5.QtWidgets import QApplication, QMainWindow, QMdiSubWindow, QMdiArea, QTextEdit, QPushButton +from PyQt5.QtWidgets import ( + QApplication, + QMainWindow, + QMdiSubWindow, + QMdiArea, + QTextEdit, + QPushButton, +) class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.sub_window_1 = QMdiSubWindow() self.sub_window_1.setWidget(QTextEdit("

    Hello World!

    ")) self.sub_window_2 = QMdiSubWindow() - self.sub_window_2.setWidget(QPushButton('Click!')) + self.sub_window_2.setWidget(QPushButton("Click!")) self.mdi_area = QMdiArea() self.mdi_area.addSubWindow(self.sub_window_1) @@ -24,7 +31,7 @@ def __init__(self): self.setCentralWidget(self.mdi_area) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QPainter_setOpacity/main.py b/qt__pyqt__pyside__pyqode/QPainter_setOpacity/main.py index 164ad59da..1db1bf643 100644 --- a/qt__pyqt__pyside__pyqode/QPainter_setOpacity/main.py +++ b/qt__pyqt__pyside__pyqode/QPainter_setOpacity/main.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtGui import QPainter, QImage from PyQt5.QtCore import Qt -img = QImage(r'video-game-192x192.png') +img = QImage(r"video-game-192x192.png") img2 = QImage(img.size(), QImage.Format_ARGB32) img2.fill(Qt.transparent) @@ -16,4 +16,4 @@ p.setOpacity(0.85) p.drawImage(img.rect(), img) -img2.save(r'video-game-192x192__transparent.png') +img2.save(r"video-game-192x192__transparent.png") diff --git a/qt__pyqt__pyside__pyqode/QPixmap_createHeuristicMask/main.py b/qt__pyqt__pyside__pyqode/QPixmap_createHeuristicMask/main.py index 465796e78..e31020d56 100644 --- a/qt__pyqt__pyside__pyqode/QPixmap_createHeuristicMask/main.py +++ b/qt__pyqt__pyside__pyqode/QPixmap_createHeuristicMask/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtGui import QGuiApplication, QPixmap @@ -12,4 +12,4 @@ pixmap = QPixmap("cat.jpg") pixmap.setMask(pixmap.createHeuristicMask(Qt.transparent)) -pixmap.save('cat.png') +pixmap.save("cat.png") diff --git a/qt__pyqt__pyside__pyqode/QTextEdit__with_font_size_panel.py b/qt__pyqt__pyside__pyqode/QTextEdit__with_font_size_panel.py index 22be23e1e..512d88dc0 100644 --- a/qt__pyqt__pyside__pyqode/QTextEdit__with_font_size_panel.py +++ b/qt__pyqt__pyside__pyqode/QTextEdit__with_font_size_panel.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class MainWindow(Qt.QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.sb_font_size = Qt.QSpinBox() self.sb_font_size.setRange(5, 40) self.sb_font_size.valueChanged.connect(self._on_font_size_changed) - self.text_edit = Qt.QTextEdit('Test this!') + self.text_edit = Qt.QTextEdit("Test this!") layout = Qt.QVBoxLayout() layout.addWidget(self.sb_font_size) @@ -26,13 +26,13 @@ def __init__(self): self.setCentralWidget(central_widget) - def _on_font_size_changed(self, value): + def _on_font_size_changed(self, value) -> None: text_char_format = Qt.QTextCharFormat() text_char_format.setFontPointSize(value) self.merge_format_on_word_or_selection(text_char_format) - def merge_format_on_word_or_selection(self, text_char_format): + def merge_format_on_word_or_selection(self, text_char_format) -> None: cursor = self.text_edit.textCursor() if not cursor.hasSelection(): cursor.select(Qt.QTextCursor.WordUnderCursor) @@ -40,7 +40,7 @@ def merge_format_on_word_or_selection(self, text_char_format): cursor.mergeCharFormat(text_char_format) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QThreadPool__QRunnable.py b/qt__pyqt__pyside__pyqode/QThreadPool__QRunnable.py index 7e82d541a..af48f9ec5 100644 --- a/qt__pyqt__pyside__pyqode/QThreadPool__QRunnable.py +++ b/qt__pyqt__pyside__pyqode/QThreadPool__QRunnable.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + import threading import time @@ -9,16 +10,17 @@ from PyQt5.QtCore import QThreadPool, QRunnable + lock = threading.Lock() class HelloWorldTask(QRunnable): - def __init__(self, idx: int): + def __init__(self, idx: int) -> None: super().__init__() self.idx = idx - def run(self): + def run(self) -> None: with lock: print(f"[{self.idx}] Hello world from thread: {threading.current_thread()}") diff --git a/qt__pyqt__pyside__pyqode/QThread_with_pause_and_resume__QWaitCondition.py b/qt__pyqt__pyside__pyqode/QThread_with_pause_and_resume__QWaitCondition.py index 73d0a4870..405f622ee 100644 --- a/qt__pyqt__pyside__pyqode/QThread_with_pause_and_resume__QWaitCondition.py +++ b/qt__pyqt__pyside__pyqode/QThread_with_pause_and_resume__QWaitCondition.py @@ -1,20 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys import time -from typing import List - -from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QComboBox, QPushButton, QLabel +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QGridLayout, + QComboBox, + QPushButton, + QLabel, +) from PyQt5.QtCore import QThread, QTimer, QWaitCondition, QMutex class Thread(QThread): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self._is_pause = False @@ -25,14 +30,14 @@ def __init__(self, parent=None): def is_pause(self) -> bool: return self._is_pause - def pause(self): + def pause(self) -> None: self._is_pause = True - def resume(self): + def resume(self) -> None: self._is_pause = False self.condition.wakeAll() - def run(self): + def run(self) -> None: while True: self.mutex.lock() if self._is_pause: @@ -43,7 +48,7 @@ def run(self): class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.combobox_thread = QComboBox() @@ -51,19 +56,19 @@ def __init__(self): self.label_result = QLabel() - self.button_add = QPushButton('Add') + self.button_add = QPushButton("Add") self.button_add.clicked.connect(self.add) - self.button_pause = QPushButton('Pause') + self.button_pause = QPushButton("Pause") self.button_pause.clicked.connect(self.pause) - self.button_pause_all = QPushButton('Pause all') + self.button_pause_all = QPushButton("Pause all") self.button_pause_all.clicked.connect(self.pause_all) - self.button_resume = QPushButton('Resume') + self.button_resume = QPushButton("Resume") self.button_resume.clicked.connect(self.resume) - self.button_resume_all = QPushButton('Resume all') + self.button_resume_all = QPushButton("Resume all") self.button_resume_all.clicked.connect(self.resume_all) layout = QGridLayout(self) @@ -82,9 +87,9 @@ def __init__(self): self._update_states() - def _update_states(self): + def _update_states(self) -> None: threads_num = self.combobox_thread.count() - self.setWindowTitle(f'Threads: {threads_num}') + self.setWindowTitle(f"Threads: {threads_num}") idx = self.combobox_thread.currentIndex() ok = idx != -1 @@ -94,8 +99,8 @@ def _update_states(self): if ok: title = self.combobox_thread.itemText(idx) - self.button_pause.setText(f'Pause - {title!r}') - self.button_resume.setText(f'Resume - {title!r}') + self.button_pause.setText(f"Pause - {title!r}") + self.button_resume.setText(f"Resume - {title!r}") self.button_pause_all.setEnabled(threads_num) self.button_resume_all.setEnabled(threads_num) @@ -103,48 +108,50 @@ def _update_states(self): def get_thread(self, idx: int) -> Thread: return self.combobox_thread.itemData(idx) - def get_all_thread(self) -> List[Thread]: + def get_all_thread(self) -> list[Thread]: return [self.get_thread(i) for i in range(self.combobox_thread.count())] - def update_info(self): + def update_info(self) -> None: total = sum(thread.sum for thread in self.get_all_thread()) self.label_result.setText(f"SUM: {total}") - def add(self): + def add(self) -> None: thread = Thread(self) thread.start() - self.combobox_thread.addItem(f'Thread #{self.combobox_thread.count() + 1}', thread) + self.combobox_thread.addItem( + f"Thread #{self.combobox_thread.count() + 1}", thread + ) self._update_states() - def pause(self): + def pause(self) -> None: thread = self.combobox_thread.currentData() if thread: thread.pause() self._update_states() - def pause_all(self): + def pause_all(self) -> None: for thread in self.get_all_thread(): thread.pause() self._update_states() - def resume(self): + def resume(self) -> None: thread = self.combobox_thread.currentData() if thread: thread.resume() self._update_states() - def resume_all(self): + def resume_all(self) -> None: for thread in self.get_all_thread(): thread.resume() self._update_states() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QThread_with_pause_and_resume__QWaitCondition_with_QTableWidget.py b/qt__pyqt__pyside__pyqode/QThread_with_pause_and_resume__QWaitCondition_with_QTableWidget.py index 6d8c07847..228b77fce 100644 --- a/qt__pyqt__pyside__pyqode/QThread_with_pause_and_resume__QWaitCondition_with_QTableWidget.py +++ b/qt__pyqt__pyside__pyqode/QThread_with_pause_and_resume__QWaitCondition_with_QTableWidget.py @@ -1,31 +1,37 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt import sys import time -from typing import List - from PyQt5.QtWidgets import ( - QApplication, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, - QTableWidget, QTableWidgetItem, QPushButton, QLabel, QCheckBox + QApplication, + QWidget, + QVBoxLayout, + QHBoxLayout, + QGridLayout, + QTableWidget, + QTableWidgetItem, + QPushButton, + QLabel, + QCheckBox, ) from PyQt5.QtCore import QThread, QTimer, QWaitCondition, QMutex, pyqtSignal, Qt def get_pretty_int(num: int) -> str: - return f'{num:,}'.replace(',', ' ') + return f"{num:,}".replace(",", " ") class Thread(QThread): about_pause = pyqtSignal(bool) about_sum = pyqtSignal(int) - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self._is_pause = False @@ -38,14 +44,14 @@ def sum(self) -> int: return self._sum @sum.setter - def sum(self, value: int): + def sum(self, value: int) -> None: self._sum = value self.about_sum.emit(value) def get_state(self) -> str: - return 'PAUSED' if self._is_pause else 'WORKING' + return "PAUSED" if self._is_pause else "WORKING" - def set_pause(self, pause: bool): + def set_pause(self, pause: bool) -> None: self._is_pause = pause self.about_pause.emit(pause) @@ -55,13 +61,13 @@ def set_pause(self, pause: bool): def is_pause(self) -> bool: return self._is_pause - def pause(self): + def pause(self) -> None: self.set_pause(True) - def resume(self): + def resume(self) -> None: self.set_pause(False) - def run(self): + def run(self) -> None: while True: self.mutex.lock() if self._is_pause: @@ -72,12 +78,12 @@ def run(self): class MainWindow(QWidget): - headers = ['NAME', 'STATE', 'SUM'] + headers = ["NAME", "STATE", "SUM"] - def __init__(self): + def __init__(self) -> None: super().__init__() - self.started: DT.datetime = None + self.started: dt.datetime = None self._sum = 0 self.table_thread = QTableWidget() @@ -88,26 +94,28 @@ def __init__(self): self.table_thread.setSelectionBehavior(QTableWidget.SelectRows) self.table_thread.setSelectionMode(QTableWidget.SingleSelection) self.table_thread.setMinimumHeight(250) - self.table_thread.selectionModel().currentRowChanged.connect(self._update_states) + self.table_thread.selectionModel().currentRowChanged.connect( + self._update_states + ) self.label_result = QLabel() - self.button_add = QPushButton('Add') + self.button_add = QPushButton("Add") self.button_add.clicked.connect(self.add) - self.button_pause = QPushButton('Pause') + self.button_pause = QPushButton("Pause") self.button_pause.clicked.connect(self.pause) - self.button_pause_all = QPushButton('Pause all') + self.button_pause_all = QPushButton("Pause all") self.button_pause_all.clicked.connect(self.pause_all) - self.button_resume = QPushButton('Resume') + self.button_resume = QPushButton("Resume") self.button_resume.clicked.connect(self.resume) - self.button_resume_all = QPushButton('Resume all') + self.button_resume_all = QPushButton("Resume all") self.button_resume_all.clicked.connect(self.resume_all) - self.cb_reset_sum = QCheckBox('Reset sum') + self.cb_reset_sum = QCheckBox("Reset sum") self.cb_reset_sum.setChecked(True) left_layout = QVBoxLayout() @@ -133,15 +141,15 @@ def __init__(self): self._update_states() - def _update_window_title(self): + def _update_window_title(self) -> None: threads_num = self.table_thread.rowCount() if self.started: - elapsed = str(DT.datetime.now() - self.started).split('.')[0] - self.setWindowTitle(f'Threads: {threads_num}. Elapsed: {elapsed}') + elapsed = str(dt.datetime.now() - self.started).split(".")[0] + self.setWindowTitle(f"Threads: {threads_num}. Elapsed: {elapsed}") else: - self.setWindowTitle(f'Threads: {threads_num}') + self.setWindowTitle(f"Threads: {threads_num}") - def _update_states(self): + def _update_states(self) -> None: self._update_window_title() row = self.table_thread.currentRow() @@ -152,8 +160,8 @@ def _update_states(self): if ok: title = self.table_thread.item(row, 0).text() - self.button_pause.setText(f'Pause - {title!r}') - self.button_resume.setText(f'Resume - {title!r}') + self.button_pause.setText(f"Pause - {title!r}") + self.button_resume.setText(f"Resume - {title!r}") threads_num = self.table_thread.rowCount() self.button_pause_all.setEnabled(threads_num) @@ -162,10 +170,10 @@ def _update_states(self): def get_thread(self, row: int) -> Thread: return self.table_thread.item(row, 0).data(Qt.UserRole) - def get_all_thread(self) -> List[Thread]: + def get_all_thread(self) -> list[Thread]: return [self.get_thread(row) for row in range(self.table_thread.rowCount())] - def update_info(self): + def update_info(self) -> None: self._update_window_title() if self.cb_reset_sum.isChecked(): @@ -174,24 +182,28 @@ def update_info(self): self._sum += sum(thread.sum for thread in self.get_all_thread()) self.label_result.setText(f"TOTAL SUM: {get_pretty_int(self._sum)}") - def _on_thread_changed(self, thread: Thread): + def _on_thread_changed(self, thread: Thread) -> None: for row in range(self.table_thread.rowCount()): if thread == self.get_thread(row): self.table_thread.item(row, 1).setText(thread.get_state()) self.table_thread.item(row, 2).setText(get_pretty_int(thread.sum)) - def add(self): + def add(self) -> None: if not self.started: - self.started = DT.datetime.now() + self.started = dt.datetime.now() thread = Thread(self) - thread.about_sum.connect(lambda _, thread=thread: self._on_thread_changed(thread)) - thread.about_pause.connect(lambda _, thread=thread: self._on_thread_changed(thread)) + thread.about_sum.connect( + lambda _, thread=thread: self._on_thread_changed(thread) + ) + thread.about_pause.connect( + lambda _, thread=thread: self._on_thread_changed(thread) + ) row = self.table_thread.rowCount() self.table_thread.setRowCount(row + 1) - title = f'Thread #{row + 1}' + title = f"Thread #{row + 1}" item_title = QTableWidgetItem(title) item_title.setData(Qt.UserRole, thread) @@ -206,7 +218,7 @@ def add(self): self._update_states() - def pause(self): + def pause(self) -> None: item = self.table_thread.currentItem() if item: thread = self.get_thread(item.row()) @@ -214,13 +226,13 @@ def pause(self): self._update_states() - def pause_all(self): + def pause_all(self) -> None: for thread in self.get_all_thread(): thread.pause() self._update_states() - def resume(self): + def resume(self) -> None: item = self.table_thread.currentItem() if item: thread = self.get_thread(item.row()) @@ -228,14 +240,14 @@ def resume(self): self._update_states() - def resume_all(self): + def resume_all(self) -> None: for thread in self.get_all_thread(): thread.resume() self._update_states() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QWebEngineView__load__blank_url.py b/qt__pyqt__pyside__pyqode/QWebEngineView__load__blank_url.py index cb4b1f682..6f7f69feb 100644 --- a/qt__pyqt__pyside__pyqode/QWebEngineView__load__blank_url.py +++ b/qt__pyqt__pyside__pyqode/QWebEngineView__load__blank_url.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/a/971228/201445 @@ -18,24 +18,26 @@ def createWindow(self, _type): page.urlChanged.connect(self.on_url_changed) return page - def on_url_changed(self, url: QUrl): + def on_url_changed(self, url: QUrl) -> None: page = self.sender() self.setUrl(url) page.deleteLater() class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.browser = QWebEngineView() self.browser.setPage(WebEnginePage(self.browser)) - self.browser.load(QUrl("https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_a_target")) + self.browser.load( + QUrl("https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_a_target") + ) self.setCentralWidget(self.browser) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery/main.py b/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery/main.py index 3fd41c545..3da012a4c 100644 --- a/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery/main.py +++ b/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtCore import QUrl @@ -12,7 +12,7 @@ app = QApplication([]) -with open('js/jquery-3.1.1.min.js') as f: +with open("js/jquery-3.1.1.min.js") as f: jquery_text = f.read() jquery_text += "\nvar qt = { 'jQuery': jQuery.noConflict(true) };" @@ -20,7 +20,7 @@ view = QWebEngineView() -def _on_load_finished(ok: bool): +def _on_load_finished(ok: bool) -> None: view.page().runJavaScript(jquery_text) # Test jQuery @@ -29,7 +29,7 @@ def _on_load_finished(ok: bool): view.loadFinished.connect(_on_load_finished) -view.load(QUrl('https://store.steampowered.com/')) +view.load(QUrl("https://store.steampowered.com/")) view.show() diff --git a/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery__with_select_color/main.py b/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery__with_select_color/main.py index 98b0247fd..6018c88a7 100644 --- a/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery__with_select_color/main.py +++ b/qt__pyqt__pyside__pyqode/QWebEngine__append_custom_javascript__jQuery__with_select_color/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtCore import QUrl, Qt @@ -10,41 +10,42 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.view = QWebEngineView() self.view.loadFinished.connect(self._on_load_finished) - with open('../QWebEngine__append_custom_javascript__jQuery/js/jquery-3.1.1.min.js') as f: + file_name = "../QWebEngine__append_custom_javascript__jQuery/js/jquery-3.1.1.min.js" + with open(file_name) as f: self.jquery_text = f.read() self.jquery_text += "\nvar qt = { 'jQuery': jQuery.noConflict(true) };" - tool_bar = self.addToolBar('General') - action = tool_bar.addAction('Change background-color') + tool_bar = self.addToolBar("General") + action = tool_bar.addAction("Change background-color") action.triggered.connect(self._change_background_color) self.setCentralWidget(self.view) - def _on_load_finished(self, ok: bool): + def _on_load_finished(self, ok: bool) -> None: self.view.page().runJavaScript(self.jquery_text) - def _change_background_color(self): + def _change_background_color(self) -> None: color = QColorDialog.getColor(Qt.yellow) if not color.isValid(): return - code = "qt.jQuery('a').css('background-color', '{}');".format(color.name()) + code = f"qt.jQuery('a').css('background-color', '{color.name()}');" self.view.page().runJavaScript(code) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() mw.resize(400, 400) mw.show() - mw.view.load(QUrl('https://store.steampowered.com/')) + mw.view.load(QUrl("https://store.steampowered.com/")) app.exec() diff --git a/qt__pyqt__pyside__pyqode/QWebEngine__fill_form_login__auth__github_com/main.py b/qt__pyqt__pyside__pyqode/QWebEngine__fill_form_login__auth__github_com/main.py index f0e597de5..e5e174f41 100644 --- a/qt__pyqt__pyside__pyqode/QWebEngine__fill_form_login__auth__github_com/main.py +++ b/qt__pyqt__pyside__pyqode/QWebEngine__fill_form_login__auth__github_com/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtCore import QUrl @@ -9,11 +9,12 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView -LOGIN = '' -PASSWORD = '' +LOGIN = "" +PASSWORD = "" -with open('../QWebEngine__append_custom_javascript__jQuery/js/jquery-3.1.1.min.js') as f: +file_name = "../QWebEngine__append_custom_javascript__jQuery/js/jquery-3.1.1.min.js" +with open(file_name) as f: jquery_text = f.read() jquery_text += "\nvar qt = { 'jQuery': jQuery.noConflict(true) };" @@ -21,31 +22,35 @@ app = QApplication([]) view = QWebEngineView() -view.load(QUrl('https://github.com/login')) +view.load(QUrl("https://github.com/login")) -def _on_load_finished(ok: bool): +def _on_load_finished(ok: bool) -> None: page = view.page() url = page.url().toString() print(url) - if not url.endswith('login'): + if not url.endswith("login"): return page.runJavaScript(jquery_text) - page.runJavaScript(f""" + page.runJavaScript( + f""" qt.jQuery('#login_field').val('{LOGIN}'); qt.jQuery('#password').val('{PASSWORD}'); qt.jQuery('input[name="commit"]').click(); - """) + """ + ) print() -view.loadProgress.connect(lambda value: view.setWindowTitle('{} ({}%)'.format(view.url().toString(), value))) +view.loadProgress.connect( + lambda value: view.setWindowTitle(f"{view.url().toString()} ({value}%)") +) view.loadFinished.connect(_on_load_finished) mw = QMainWindow() diff --git a/qt__pyqt__pyside__pyqode/QWebEngine__handler_console_log_javascript/main.py b/qt__pyqt__pyside__pyqode/QWebEngine__handler_console_log_javascript/main.py index 2080e7ead..ca05eaeb8 100644 --- a/qt__pyqt__pyside__pyqode/QWebEngine__handler_console_log_javascript/main.py +++ b/qt__pyqt__pyside__pyqode/QWebEngine__handler_console_log_javascript/main.py @@ -1,43 +1,54 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import sys + from PyQt5.QtCore import QUrl from PyQt5.QtWidgets import QApplication, QMainWindow from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage -import sys - class MyWebEnginePage(QWebEnginePage): - def javaScriptConsoleMessage(self, level: 'JavaScriptConsoleMessage', message: str, line_number: int, source_id: str): - print(f'javascript_console_message: {level}, {message}, {line_number}, {source_id}', file=sys.stderr) + def javaScriptConsoleMessage( + self, + level: "JavaScriptConsoleMessage", + message: str, + line_number: int, + source_id: str, + ) -> None: + print( + f"javascript_console_message: {level}, {message}, {line_number}, {source_id}", + file=sys.stderr, + ) app = QApplication([]) page = MyWebEnginePage() -page.load(QUrl('https://ya.ru')) +page.load(QUrl("https://ya.ru")) view = QWebEngineView() view.setPage(page) -def _on_load_finished(ok: bool): +def _on_load_finished(ok: bool) -> None: page = view.page() print(page.url().toString()) - page.runJavaScript('console.log(document.title)') - page.runJavaScript('console.log(this)') + page.runJavaScript("console.log(document.title)") + page.runJavaScript("console.log(this)") page.runJavaScript('console.log("Hello World!")') - page.runJavaScript('console.log(2 + 2 * 2)') + page.runJavaScript("console.log(2 + 2 * 2)") print() -view.loadProgress.connect(lambda value: view.setWindowTitle('{} ({}%)'.format(view.url().toString(), value))) +view.loadProgress.connect( + lambda value: view.setWindowTitle(f"{view.url().toString()} ({value}%)") +) view.loadFinished.connect(_on_load_finished) mw = QMainWindow() diff --git a/qt__pyqt__pyside__pyqode/QWebEngine__runJavaScript__sync__click_on_element/main.py b/qt__pyqt__pyside__pyqode/QWebEngine__runJavaScript__sync__click_on_element/main.py index 8115f3373..a3a1287be 100644 --- a/qt__pyqt__pyside__pyqode/QWebEngine__runJavaScript__sync__click_on_element/main.py +++ b/qt__pyqt__pyside__pyqode/QWebEngine__runJavaScript__sync__click_on_element/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtCore import QUrl, QEventLoop @@ -12,12 +12,10 @@ def run_js_code(page: QWebEnginePage, code: str) -> object: loop = QEventLoop() - result_value = { - 'value': None - } + result_value = {"value": None} - def _on_callback(result: object): - result_value['value'] = result + def _on_callback(result: object) -> None: + result_value["value"] = result loop.quit() @@ -25,10 +23,11 @@ def _on_callback(result: object): loop.exec() - return result_value['value'] + return result_value["value"] -with open('../QWebEngine__append_custom_javascript__jQuery/js/jquery-3.1.1.min.js') as f: +file_name = "../QWebEngine__append_custom_javascript__jQuery/js/jquery-3.1.1.min.js" +with open(file_name) as f: jquery_text = f.read() jquery_text += "\nvar qt = { 'jQuery': jQuery.noConflict(true) };" @@ -36,17 +35,17 @@ def _on_callback(result: object): app = QApplication([]) view = QWebEngineView() -view.load(QUrl('https://гибдд.рф/request_main')) +view.load(QUrl("https://гибдд.рф/request_main")) -def _on_load_finished(ok: bool): +def _on_load_finished(ok: bool) -> None: page = view.page() print(page.url().toString()) page.runJavaScript(jquery_text) result = run_js_code(page, "document.title") - print('run_java_script:', result) + print("run_java_script:", result) # Клик на флажок "С информацией ознакомлен" run_js_code(page, """qt.jQuery('input[name="agree"]').click();""") @@ -57,7 +56,9 @@ def _on_load_finished(ok: bool): print() -view.loadProgress.connect(lambda value: view.setWindowTitle('{} ({}%)'.format(view.url().toString(), value))) +view.loadProgress.connect( + lambda value: view.setWindowTitle(f"{view.url().toString()} ({value}%)") +) view.loadFinished.connect(_on_load_finished) mw = QMainWindow() diff --git a/qt__pyqt__pyside__pyqode/QWidget_setMask__frameless_window/main.py b/qt__pyqt__pyside__pyqode/QWidget_setMask__frameless_window/main.py index 255b9b8a9..05d200bd9 100644 --- a/qt__pyqt__pyside__pyqode/QWidget_setMask__frameless_window/main.py +++ b/qt__pyqt__pyside__pyqode/QWidget_setMask__frameless_window/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QPixmap, QLabel diff --git a/qt__pyqt__pyside__pyqode/QWidget_with_QMenuBar.py b/qt__pyqt__pyside__pyqode/QWidget_with_QMenuBar.py index 5b9f99bdb..130249ff8 100644 --- a/qt__pyqt__pyside__pyqode/QWidget_with_QMenuBar.py +++ b/qt__pyqt__pyside__pyqode/QWidget_with_QMenuBar.py @@ -1,31 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class MainWindow(Qt.QWidget): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) menu_bar = Qt.QMenuBar() - menu_file = menu_bar.addMenu('File') - action_exit = menu_file.addAction('Exit') + menu_file = menu_bar.addMenu("File") + action_exit = menu_file.addAction("Exit") action_exit.triggered.connect(self.close) layout = Qt.QVBoxLayout() - layout.addWidget(Qt.QLabel('Test')) - layout.addWidget(Qt.QPushButton('Click!')) + layout.addWidget(Qt.QLabel("Test")) + layout.addWidget(Qt.QPushButton("Click!")) layout.setMenuBar(menu_bar) self.setLayout(layout) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/QtWebKit_example/parse_myscore_ru.py b/qt__pyqt__pyside__pyqode/QtWebKit_example/parse_myscore_ru.py index f4d66695e..841364e42 100644 --- a/qt__pyqt__pyside__pyqode/QtWebKit_example/parse_myscore_ru.py +++ b/qt__pyqt__pyside__pyqode/QtWebKit_example/parse_myscore_ru.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys @@ -12,17 +12,19 @@ from PySide.QtNetwork import * -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) # Чтобы не было проблем запуска компов с прокси: QNetworkProxyFactory.setUseSystemConfiguration(True) - QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, True) + QWebSettings.globalSettings().setAttribute( + QWebSettings.DeveloperExtrasEnabled, True + ) view = QWebView() # view.show() - view.load('http://www.myscore.ru/match/nqD8D0j4/#match-statistics;0') + view.load("http://www.myscore.ru/match/nqD8D0j4/#match-statistics;0") # Ждем пока прогрузится страница loop = QEventLoop() @@ -35,8 +37,8 @@ print(doc.findFirst("#statistics-2-statistic a").toPlainText()) table = doc.findFirst("#tab-statistics-0-statistic .parts") - for tr in table.findAll('tr'): - l, text, r = tr.toPlainText().split('\t') + for tr in table.findAll("tr"): + l, text, r = tr.toPlainText().split("\t") print(l, text, r) # sys.exit(app.exec_()) diff --git a/qt__pyqt__pyside__pyqode/RotatedButton__QGraphicsScene.py b/qt__pyqt__pyside__pyqode/RotatedButton__QGraphicsScene.py index 875a53607..9ecac1a74 100644 --- a/qt__pyqt__pyside__pyqode/RotatedButton__QGraphicsScene.py +++ b/qt__pyqt__pyside__pyqode/RotatedButton__QGraphicsScene.py @@ -2,19 +2,27 @@ # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import sys -from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QGraphicsScene, \ - QGraphicsView, QGraphicsLinearLayout, QGraphicsWidget, QHBoxLayout +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QPushButton, + QGraphicsScene, + QGraphicsView, + QGraphicsLinearLayout, + QGraphicsWidget, + QHBoxLayout, +) from PyQt5.QtGui import QPainter from PyQt5.QtCore import Qt class Window(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.scene = QGraphicsScene() @@ -56,7 +64,7 @@ def __init__(self): scene_layout.addItem(proxy_rb3) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) w = Window() diff --git a/qt__pyqt__pyside__pyqode/RunFuncThread.py b/qt__pyqt__pyside__pyqode/RunFuncThread.py index ae35408b0..7329a61cf 100644 --- a/qt__pyqt__pyside__pyqode/RunFuncThread.py +++ b/qt__pyqt__pyside__pyqode/RunFuncThread.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtCore import QThread, pyqtSignal @@ -10,17 +10,18 @@ class RunFuncThread(QThread): run_finished = pyqtSignal(object) - def __init__(self, func): + def __init__(self, func) -> None: super().__init__() self.func = func - def run(self): + def run(self) -> None: self.run_finished.emit(self.func()) -if __name__ == '__main__': +if __name__ == "__main__": from PyQt5.QtCore import QCoreApplication + app = QCoreApplication([]) thread = RunFuncThread(func=lambda: 2 + 2 * 2) diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/switch_button.py b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/switch_button.py index 6b3498d72..7d3cbacd3 100644 --- a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/switch_button.py +++ b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/switch_button.py @@ -1,19 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/51825815/5909792 -from PyQt5.QtWidgets import QAbstractButton, QApplication, QHBoxLayout, QSizePolicy, QWidget +from PyQt5.QtWidgets import ( + QAbstractButton, + QApplication, + QHBoxLayout, + QSizePolicy, + QWidget, +) from PyQt5.QtCore import QPropertyAnimation, QRectF, QSize, Qt, pyqtProperty from PyQt5.QtGui import QPainter class Switch(QAbstractButton): - def __init__(self, parent=None, track_radius=10, thumb_radius=8): + def __init__(self, parent=None, track_radius=10, thumb_radius=8) -> None: super().__init__(parent=parent) self.setCheckable(True) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) @@ -44,8 +50,8 @@ def __init__(self, parent=None, track_radius=10, thumb_radius=8): False: palette.dark().color(), } self._thumb_text = { - True: '', - False: '', + True: "", + False: "", } self._track_opacity = 0.5 else: @@ -62,8 +68,8 @@ def __init__(self, parent=None, track_radius=10, thumb_radius=8): False: palette.dark().color(), } self._thumb_text = { - True: '✔', - False: '✕', + True: "✔", + False: "✕", } self._track_opacity = 1 @@ -72,7 +78,7 @@ def offset(self): return self._offset @offset.setter - def offset(self, value): + def offset(self, value) -> None: self._offset = value self.update() @@ -82,15 +88,15 @@ def sizeHint(self): # pylint: disable=invalid-name 2 * self._track_radius + 2 * self._margin, ) - def setChecked(self, checked): + def setChecked(self, checked) -> None: super().setChecked(checked) self.offset = self._end_offset[checked]() - def resizeEvent(self, event): + def resizeEvent(self, event) -> None: super().resizeEvent(event) self.offset = self._end_offset[self.isChecked()]() - def paintEvent(self, event): # pylint: disable=invalid-name, unused-argument + def paintEvent(self, event) -> None: # pylint: disable=invalid-name, unused-argument p = QPainter(self) p.setRenderHint(QPainter.Antialiasing, True) p.setPen(Qt.NoPen) @@ -141,29 +147,29 @@ def paintEvent(self, event): # pylint: disable=invalid-name, unused-argument self._thumb_text[self.isChecked()], ) - def mouseReleaseEvent(self, event): # pylint: disable=invalid-name + def mouseReleaseEvent(self, event) -> None: # pylint: disable=invalid-name super().mouseReleaseEvent(event) if event.button() == Qt.LeftButton: - anim = QPropertyAnimation(self, b'offset', self) + anim = QPropertyAnimation(self, b"offset", self) anim.setDuration(120) anim.setStartValue(self.offset) anim.setEndValue(self._end_offset[self.isChecked()]()) anim.start() - def enterEvent(self, event): # pylint: disable=invalid-name + def enterEvent(self, event) -> None: # pylint: disable=invalid-name self.setCursor(Qt.PointingHandCursor) super().enterEvent(event) -def main(): +def main() -> None: app = QApplication([]) # Thumb size < track size (Gitlab style) s1 = Switch() - s1.toggled.connect(lambda c: print('toggled', c)) - s1.clicked.connect(lambda c: print('clicked', c)) - s1.pressed.connect(lambda: print('pressed')) - s1.released.connect(lambda: print('released')) + s1.toggled.connect(lambda c: print("toggled", c)) + s1.clicked.connect(lambda c: print("clicked", c)) + s1.pressed.connect(lambda: print("pressed")) + s1.released.connect(lambda: print("released")) s2 = Switch() s2.setEnabled(False) @@ -184,5 +190,5 @@ def main(): app.exec() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/test.py b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/test.py index e9a94844c..2a9dfecad 100644 --- a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/test.py +++ b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QAbstractButton/test.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel @@ -9,20 +9,24 @@ class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() switch_btn1 = Switch() - label_switch_btn1 = QLabel(f'Checked: {switch_btn1.isChecked()}') - switch_btn1.clicked.connect(lambda checked: label_switch_btn1.setText( - f'Checked: {switch_btn1.isChecked()}' - )) + label_switch_btn1 = QLabel(f"Checked: {switch_btn1.isChecked()}") + switch_btn1.clicked.connect( + lambda checked: label_switch_btn1.setText( + f"Checked: {switch_btn1.isChecked()}" + ) + ) switch_btn2 = Switch() - label_switch_btn2 = QLabel(f'Checked: {switch_btn2.isChecked()}') - switch_btn2.clicked.connect(lambda checked: label_switch_btn2.setText( - f'Checked: {switch_btn2.isChecked()}' - )) + label_switch_btn2 = QLabel(f"Checked: {switch_btn2.isChecked()}") + switch_btn2.clicked.connect( + lambda checked: label_switch_btn2.setText( + f"Checked: {switch_btn2.isChecked()}" + ) + ) main_layout = QVBoxLayout(self) main_layout.addWidget(switch_btn1) @@ -32,7 +36,7 @@ def __init__(self): main_layout.addWidget(label_switch_btn2) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/switch_button.py b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/switch_button.py index 601cdfa03..1db714f21 100644 --- a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/switch_button.py +++ b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/switch_button.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/a/1355594/201445 @@ -11,19 +11,26 @@ from PyQt5.QtWidgets import QCheckBox from PyQt5.QtGui import QPainter, QColor -from PyQt5.QtCore import Qt, QEasingCurve, QPropertyAnimation, pyqtProperty, QPoint, QRect +from PyQt5.QtCore import ( + Qt, + QEasingCurve, + QPropertyAnimation, + pyqtProperty, + QPoint, + QRect, +) class ToggleButton(QCheckBox): def __init__( - self, - width=70, - height=40, - bg_color="#777", - circle_color="#DDD", - active_color="#00BCff", - animation_curve=QEasingCurve.OutBounce, - ): + self, + width=70, + height=40, + bg_color="#777", + circle_color="#DDD", + active_color="#00BCff", + animation_curve=QEasingCurve.OutBounce, + ) -> None: super().__init__() self.setFixedSize(width, height) @@ -47,11 +54,11 @@ def circle_position(self) -> int: return self._circle_position @circle_position.setter - def circle_position(self, pos: int): + def circle_position(self, pos: int) -> None: self._circle_position = pos self.update() - def start_transition(self, value): + def start_transition(self, value) -> None: self.animation.setStartValue(self.circle_position) if value: self.animation.setEndValue(self.width() - self._circle_size) @@ -62,7 +69,7 @@ def start_transition(self, value): def hitButton(self, pos: QPoint) -> bool: return self.contentsRect().contains(pos) - def paintEvent(self, e): + def paintEvent(self, e) -> None: p = QPainter(self) p.setRenderHint(QPainter.Antialiasing) @@ -72,24 +79,25 @@ def paintEvent(self, e): p.setBrush(QColor(self._active_color if self.isChecked() else self._bg_color)) p.drawRoundedRect( - 0, 0, - rect.width(), - self.height(), - self.height() / 2, - self.height() / 2 + 0, 0, rect.width(), self.height(), self.height() / 2, self.height() / 2 ) p.setBrush(QColor(self._circle_color)) x, y = self._circle_position, self._circle_margin - p.drawEllipse(x, y, self._circle_size - self._circle_margin, self._circle_size - self._circle_margin) + p.drawEllipse( + x, + y, + self._circle_size - self._circle_margin, + self._circle_size - self._circle_margin, + ) -if __name__ == '__main__': +if __name__ == "__main__": from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("Анимация кнопки переключения") @@ -103,4 +111,3 @@ def __init__(self): w.resize(500, 500) w.show() sys.exit(app.exec_()) - diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/test.py b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/test.py index 230f36053..90a7515ac 100644 --- a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/test.py +++ b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QCheckBox/test.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel @@ -9,20 +9,24 @@ class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() switch_btn1 = ToggleButton() - label_switch_btn1 = QLabel(f'Checked: {switch_btn1.isChecked()}') - switch_btn1.clicked.connect(lambda checked: label_switch_btn1.setText( - f'Checked: {switch_btn1.isChecked()}' - )) + label_switch_btn1 = QLabel(f"Checked: {switch_btn1.isChecked()}") + switch_btn1.clicked.connect( + lambda checked: label_switch_btn1.setText( + f"Checked: {switch_btn1.isChecked()}" + ) + ) switch_btn2 = ToggleButton(width=200) - label_switch_btn2 = QLabel(f'Checked: {switch_btn2.isChecked()}') - switch_btn2.clicked.connect(lambda checked: label_switch_btn2.setText( - f'Checked: {switch_btn2.isChecked()}' - )) + label_switch_btn2 = QLabel(f"Checked: {switch_btn2.isChecked()}") + switch_btn2.clicked.connect( + lambda checked: label_switch_btn2.setText( + f"Checked: {switch_btn2.isChecked()}" + ) + ) main_layout = QVBoxLayout(self) main_layout.addWidget(switch_btn1) @@ -32,7 +36,7 @@ def __init__(self): main_layout.addWidget(label_switch_btn2) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/switch_button.py b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/switch_button.py index 6d1433094..d48da89c2 100644 --- a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/switch_button.py +++ b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/switch_button.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://stackoverflow.com/a/51023362/5909792 @@ -13,23 +13,27 @@ class SwitchButton(QtWidgets.QWidget): clicked = QtCore.pyqtSignal(bool) - def __init__(self, parent=None, w1="Yes", l1=12, w2="No", l2=33, width=60): + def __init__(self, parent=None, w1="Yes", l1=12, w2="No", l2=33, width=60) -> None: super(SwitchButton, self).__init__(parent) self.setWindowFlags(QtCore.Qt.FramelessWindowHint) self.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.__labeloff = QtWidgets.QLabel(self) self.__labeloff.setText(w2) - self.__labeloff.setStyleSheet("""color: rgb(120, 120, 120); font-weight: bold;""") - self.__background = Background(self) + self.__labeloff.setStyleSheet( + """color: rgb(120, 120, 120); font-weight: bold;""" + ) + self.__background = Background(self) self.__labelon = QtWidgets.QLabel(self) self.__labelon.setText(w1) - self.__labelon.setStyleSheet("""color: rgb(255, 255, 255); font-weight: bold;""") - self.__circle = Circle(self) - self.__circlemove = None + self.__labelon.setStyleSheet( + """color: rgb(255, 255, 255); font-weight: bold;""" + ) + self.__circle = Circle(self) + self.__circlemove = None self.__ellipsemove = None - self.__enabled = True - self.__duration = 100 - self.__value = False + self.__enabled = True + self.__duration = 100 + self.__value = False self.setFixedSize(width, 24) self.__background.resize(20, 20) @@ -44,10 +48,10 @@ def isChecked(self) -> bool: def valueText(self) -> str: return self.__labelon.text() if self.isChecked() else self.__labeloff.text() - def setDuration(self, time): + def setDuration(self, time) -> None: self.__duration = time - def mousePressEvent(self, event): + def mousePressEvent(self, event) -> None: if not self.__enabled: return @@ -58,16 +62,16 @@ def mousePressEvent(self, event): self.__ellipsemove.setDuration(self.__duration) xs = 2 - y = 2 - xf = self.width()-22 + y = 2 + xf = self.width() - 22 hback = 20 isize = QtCore.QSize(hback, hback) - bsize = QtCore.QSize(self.width()-4, hback) + bsize = QtCore.QSize(self.width() - 4, hback) if self.__value: xf = 2 - xs = self.width()-22 + xs = self.width() - 22 bsize = QtCore.QSize(hback, hback) - isize = QtCore.QSize(self.width()-4, hback) + isize = QtCore.QSize(self.width() - 4, hback) self.__circlemove.setStartValue(QtCore.QPoint(xs, y)) self.__circlemove.setEndValue(QtCore.QPoint(xf, y)) @@ -81,7 +85,7 @@ def mousePressEvent(self, event): self.clicked.emit(self.isChecked()) - def paintEvent(self, event): + def paintEvent(self, event) -> None: s = self.size() qp = QtGui.QPainter() qp.begin(self) @@ -96,7 +100,7 @@ def paintEvent(self, event): lg.setColorAt(0.82, QtGui.QColor(255, 255, 255, 255)) lg.setColorAt(1, QtGui.QColor(210, 210, 210, 255)) qp.setBrush(lg) - qp.drawRoundedRect(1, 1, s.width()-2, s.height()-2, 10, 10) + qp.drawRoundedRect(1, 1, s.width() - 2, s.height() - 2, 10, 10) qp.setBrush(QtGui.QColor(210, 210, 210)) qp.drawRoundedRect(2, 2, s.width() - 4, s.height() - 4, 10, 10) @@ -121,12 +125,12 @@ def paintEvent(self, event): class Circle(QtWidgets.QWidget): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super(Circle, self).__init__(parent) self.__enabled = True self.setFixedSize(20, 20) - def paintEvent(self, event): + def paintEvent(self, event) -> None: s = self.size() qp = QtGui.QPainter() qp.begin(self) @@ -139,19 +143,19 @@ def paintEvent(self, event): rg.setColorAt(0.6, QtGui.QColor(255, 255, 255)) rg.setColorAt(1, QtGui.QColor(205, 205, 205)) qp.setBrush(QtGui.QBrush(rg)) - qp.drawEllipse(1,1, 18, 18) + qp.drawEllipse(1, 1, 18, 18) qp.setBrush(QtGui.QColor(210, 210, 210)) qp.drawEllipse(2, 2, 16, 16) if self.__enabled: - lg = QtGui.QLinearGradient(3, 18,20, 4) + lg = QtGui.QLinearGradient(3, 18, 20, 4) lg.setColorAt(0, QtGui.QColor(255, 255, 255, 255)) lg.setColorAt(0.55, QtGui.QColor(230, 230, 230, 255)) lg.setColorAt(0.72, QtGui.QColor(255, 255, 255, 255)) lg.setColorAt(1, QtGui.QColor(255, 255, 255, 255)) qp.setBrush(lg) - qp.drawEllipse(3,3, 14, 14) + qp.drawEllipse(3, 3, 14, 14) else: lg = QtGui.QLinearGradient(3, 18, 20, 4) lg.setColorAt(0, QtGui.QColor(230, 230, 230)) @@ -164,19 +168,19 @@ def paintEvent(self, event): class Background(QtWidgets.QWidget): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super(Background, self).__init__(parent) self.__enabled = True self.setFixedHeight(20) - def paintEvent(self, event): + def paintEvent(self, event) -> None: s = self.size() qp = QtGui.QPainter() qp.begin(self) qp.setRenderHint(QtGui.QPainter.Antialiasing, True) pen = QtGui.QPen(QtCore.Qt.NoPen) qp.setPen(pen) - qp.setBrush(QtGui.QColor(154,205,50)) + qp.setBrush(QtGui.QColor(154, 205, 50)) if self.__enabled: qp.setBrush(QtGui.QColor(154, 190, 50)) qp.drawRoundedRect(0, 0, s.width(), s.height(), 10, 10) diff --git a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/test.py b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/test.py index 162297b55..33534f734 100644 --- a/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/test.py +++ b/qt__pyqt__pyside__pyqode/SwitchButton__example/from_QWidget/test.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel @@ -9,20 +9,28 @@ class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() switch_btn1 = SwitchButton(self, "On", 15, "Off", 31, width=60) - label_switch_btn1 = QLabel(f'Checked: {switch_btn1.isChecked()}, text: {switch_btn1.valueText()}') - switch_btn1.clicked.connect(lambda checked: label_switch_btn1.setText( - f'Checked: {switch_btn1.isChecked()}, text: {switch_btn1.valueText()}' - )) + label_switch_btn1 = QLabel( + f"Checked: {switch_btn1.isChecked()}, text: {switch_btn1.valueText()}" + ) + switch_btn1.clicked.connect( + lambda checked: label_switch_btn1.setText( + f"Checked: {switch_btn1.isChecked()}, text: {switch_btn1.valueText()}" + ) + ) switch_btn2 = SwitchButton(self, "Вкл", 15, "Откл", 31, width=120) - label_switch_btn2 = QLabel(f'Checked: {switch_btn2.isChecked()}, text: {switch_btn2.valueText()}') - switch_btn2.clicked.connect(lambda checked: label_switch_btn2.setText( - f'Checked: {switch_btn2.isChecked()}, text: {switch_btn2.valueText()}' - )) + label_switch_btn2 = QLabel( + f"Checked: {switch_btn2.isChecked()}, text: {switch_btn2.valueText()}" + ) + switch_btn2.clicked.connect( + lambda checked: label_switch_btn2.setText( + f"Checked: {switch_btn2.isChecked()}, text: {switch_btn2.valueText()}" + ) + ) main_layout = QVBoxLayout(self) main_layout.addWidget(switch_btn1) @@ -32,7 +40,7 @@ def __init__(self): main_layout.addWidget(label_switch_btn2) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/ToolBarArea/main.py b/qt__pyqt__pyside__pyqode/ToolBarArea/main.py index b1ed9c8a3..d7f4625c3 100644 --- a/qt__pyqt__pyside__pyqode/ToolBarArea/main.py +++ b/qt__pyqt__pyside__pyqode/ToolBarArea/main.py @@ -1,43 +1,43 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class Widget(Qt.QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('ToolBarArea') + self.setWindowTitle("ToolBarArea") - self.toolbar = self.addToolBar('Panel2') - self.toolbar.addAction('1') - self.toolbar.addAction('2') - self.toolbar.addAction('3') + self.toolbar = self.addToolBar("Panel2") + self.toolbar.addAction("1") + self.toolbar.addAction("2") + self.toolbar.addAction("3") - self.toolbar2 = self.addToolBar('Panel1') + self.toolbar2 = self.addToolBar("Panel1") self.addToolBar(Qt.Qt.LeftToolBarArea, self.toolbar2) - self.toolbar2.addAction('a') - self.toolbar2.addAction('b') - self.toolbar2.addAction('c') + self.toolbar2.addAction("a") + self.toolbar2.addAction("b") + self.toolbar2.addAction("c") - self.toolbar3 = Qt.QToolBar('Panel3') + self.toolbar3 = Qt.QToolBar("Panel3") self.addToolBar(Qt.Qt.RightToolBarArea, self.toolbar3) - self.toolbar3.addAction('x') - self.toolbar3.addAction('y') - self.toolbar3.addAction('z') + self.toolbar3.addAction("x") + self.toolbar3.addAction("y") + self.toolbar3.addAction("z") - self.toolbar4 = Qt.QToolBar('Panel4') + self.toolbar4 = Qt.QToolBar("Panel4") self.addToolBar(Qt.Qt.BottomToolBarArea, self.toolbar4) - self.toolbar4.addAction('+') - self.toolbar4.addAction('-') - self.toolbar4.addAction('=') + self.toolbar4.addAction("+") + self.toolbar4.addAction("-") + self.toolbar4.addAction("=") -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QNetworkAccessManager.py b/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QNetworkAccessManager.py index 8e1c4a706..3a584fbc8 100644 --- a/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QNetworkAccessManager.py +++ b/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QNetworkAccessManager.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class URLView(Qt.QWidget): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) layout = Qt.QVBoxLayout(self) self.urlEdit = Qt.QLineEdit() - self.urlEdit.setText('https://www.python.org/static/img/python-logo.png') + self.urlEdit.setText("https://www.python.org/static/img/python-logo.png") layout.addWidget(self.urlEdit) self.imageLabel = Qt.QLabel("No image") @@ -29,20 +29,20 @@ def __init__(self, parent=None): self.nam = Qt.QNetworkAccessManager() self.nam.finished.connect(self.finish_request) - def on_load(self): + def on_load(self) -> None: print("Load image") url = self.urlEdit.text() self.nam.get(Qt.QNetworkRequest(Qt.QUrl(url))) - def finish_request(self, reply): + def finish_request(self, reply) -> None: img = Qt.QPixmap() img.loadFromData(reply.readAll()) self.imageLabel.setPixmap(img) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = URLView() w.show() diff --git a/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QThread.py b/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QThread.py index 6502cd07c..bf3431aad 100644 --- a/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QThread.py +++ b/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/async__QThread.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5 import Qt from urllib import request +from PyQt5 import Qt class LoadImageThread(Qt.QThread): about_new_image = Qt.pyqtSignal(Qt.QPixmap) - def run(self): + def run(self) -> None: data = request.urlopen(self.url).read() pixmap = Qt.QPixmap() pixmap.loadFromData(data) @@ -21,13 +21,13 @@ def run(self): class URLView(Qt.QWidget): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) layout = Qt.QVBoxLayout(self) self.urlEdit = Qt.QLineEdit() - self.urlEdit.setText('https://www.python.org/static/img/python-logo.png') + self.urlEdit.setText("https://www.python.org/static/img/python-logo.png") layout.addWidget(self.urlEdit) self.imageLabel = Qt.QLabel("No image") @@ -47,7 +47,7 @@ def __init__(self, parent=None): self.thread.started.connect(lambda: self.loadButton.setEnabled(False)) self.thread.finished.connect(lambda: self.loadButton.setEnabled(True)) - def on_load(self): + def on_load(self) -> None: print("Load image") # Запускаем поток @@ -55,7 +55,7 @@ def on_load(self): self.thread.start() -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = URLView() w.show() diff --git a/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/sync.py b/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/sync.py index 6547604f6..8c12217f3 100644 --- a/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/sync.py +++ b/qt__pyqt__pyside__pyqode/URLView__download_and_show_image_on_label/sync.py @@ -1,22 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5 import Qt from urllib import request +from PyQt5 import Qt class URLView(Qt.QWidget): - - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) layout = Qt.QVBoxLayout(self) self.urlEdit = Qt.QLineEdit() - self.urlEdit.setText('https://www.python.org/static/img/python-logo.png') + self.urlEdit.setText("https://www.python.org/static/img/python-logo.png") layout.addWidget(self.urlEdit) self.imageLabel = Qt.QLabel("No image") @@ -28,7 +27,7 @@ def __init__(self, parent=None): self.loadButton.clicked.connect(self.on_load) - def on_load(self): + def on_load(self) -> None: print("Load image") url = self.urlEdit.text() @@ -38,7 +37,7 @@ def on_load(self): self.imageLabel.setPixmap(pixmap) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = URLView() w.show() diff --git a/qt__pyqt__pyside__pyqode/advanced_list_widget.py b/qt__pyqt__pyside__pyqode/advanced_list_widget.py new file mode 100644 index 000000000..4da454915 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/advanced_list_widget.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QListWidgetItem, QListWidget, QWidget, QVBoxLayout, QToolBar + + +class AdvancedListWidget(QWidget): + def __init__(self) -> None: + super().__init__() + + self.list_widget = QListWidget() + self.list_widget.itemSelectionChanged.connect(self._update_states) + + tool_bar = QToolBar() + self.action_append = tool_bar.addAction("➕", self._append) + self.action_remove = tool_bar.addAction("➖", self._remove) + self.action_move_up = tool_bar.addAction("🔼", self._move_up) + self.action_move_down = tool_bar.addAction("🔽", self._move_down) + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + main_layout.addWidget(tool_bar) + main_layout.addWidget(self.list_widget) + + self._update_states() + + def _update_states(self) -> None: + current_row: int = self.list_widget.currentRow() + + is_selected: bool = current_row != -1 + self.action_remove.setVisible(is_selected) + + self.action_move_up.setVisible(is_selected and current_row > 0) + self.action_move_down.setVisible(is_selected and current_row < self.count() - 1) + + def _append(self) -> None: + item = self.append("New item") + + self.list_widget.setCurrentItem(item) + self.list_widget.editItem(item) + + def _remove(self) -> None: + current_row: int = self.list_widget.currentRow() + if current_row == -1: + return + + self.list_widget.takeItem(current_row) + + def _move_item(self, from_row: int, to_row: int) -> None: + item = self.list_widget.takeItem(from_row) + self.list_widget.insertItem(to_row, item) + self.list_widget.setCurrentItem(item) + + def _move_up(self) -> None: + current_row: int = self.list_widget.currentRow() + if current_row < 1: + return + + self._move_item(current_row, current_row - 1) + + def _move_down(self) -> None: + current_row: int = self.list_widget.currentRow() + if current_row != -1 and current_row >= self.count() - 1: + return + + self._move_item(current_row, current_row + 1) + + def set_items(self, items: list[str]) -> None: + self.clear() + self.append_all(items) + + def append(self, text: str) -> QListWidgetItem: + item = QListWidgetItem(text) + item.setFlags(item.flags() | Qt.ItemIsEditable) + + self.list_widget.addItem(item) + + return item + + def append_all(self, items: list[str]) -> None: + for text in items: + self.append(text) + + def clear(self) -> None: + self.list_widget.clear() + + def count(self) -> int: + return self.list_widget.count() + + def get(self, i: int) -> str: + return self.list_widget.item(i).text() + + def items(self) -> list[str]: + return [self.get(i) for i in range(self.count())] + + +if __name__ == "__main__": + from PyQt5.QtWidgets import QApplication + + app = QApplication([]) + + mw = AdvancedListWidget() + mw.set_items( + [ + "FOO", + "BAR", + "GO", + "ABC", + "123", + "!!!", + ] + ) + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/advanced_list_widget_pyqt6.py b/qt__pyqt__pyside__pyqode/advanced_list_widget_pyqt6.py new file mode 100644 index 000000000..75cee8a60 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/advanced_list_widget_pyqt6.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QListWidgetItem, QListWidget, QWidget, QVBoxLayout, QToolBar + + +class AdvancedListWidget(QWidget): + def __init__(self): + super().__init__() + + self.list_widget = QListWidget() + self.list_widget.itemSelectionChanged.connect(self._update_states) + + tool_bar = QToolBar() + self.action_append = tool_bar.addAction("➕", self._append) + self.action_remove = tool_bar.addAction("➖", self._remove) + self.action_move_up = tool_bar.addAction("🔼", self._move_up) + self.action_move_down = tool_bar.addAction("🔽", self._move_down) + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + main_layout.addWidget(tool_bar) + main_layout.addWidget(self.list_widget) + + self._update_states() + + def _update_states(self): + current_row: int = self.list_widget.currentRow() + + is_selected: bool = current_row != -1 + self.action_remove.setVisible(is_selected) + + self.action_move_up.setVisible(is_selected and current_row > 0) + self.action_move_down.setVisible(is_selected and current_row < self.count() - 1) + + def _append(self): + item = self.append("New item") + + self.list_widget.setCurrentItem(item) + self.list_widget.editItem(item) + + def _remove(self): + current_row: int = self.list_widget.currentRow() + if current_row == -1: + return + + self.list_widget.takeItem(current_row) + + def _move_item(self, from_row: int, to_row: int): + item = self.list_widget.takeItem(from_row) + self.list_widget.insertItem(to_row, item) + self.list_widget.setCurrentItem(item) + + def _move_up(self): + current_row: int = self.list_widget.currentRow() + if current_row < 1: + return + + self._move_item(current_row, current_row - 1) + + def _move_down(self): + current_row: int = self.list_widget.currentRow() + if current_row != -1 and current_row >= self.count() - 1: + return + + self._move_item(current_row, current_row + 1) + + def set_items(self, items: list[str]): + self.clear() + self.append_all(items) + + def append(self, text: str) -> QListWidgetItem: + item = QListWidgetItem(text) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable) + + self.list_widget.addItem(item) + + return item + + def append_all(self, items: list[str]): + for text in items: + self.append(text) + + def clear(self): + return self.list_widget.clear() + + def count(self) -> int: + return self.list_widget.count() + + def get(self, i: int) -> str: + return self.list_widget.item(i).text() + + def items(self) -> list[str]: + return [self.get(i) for i in range(self.count())] + + +if __name__ == "__main__": + from PyQt6.QtWidgets import QApplication + + app = QApplication([]) + + mw = AdvancedListWidget() + mw.set_items( + [ + "FOO", + "BAR", + "GO", + "ABC", + "123", + "!!!", + ] + ) + mw.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/animate_move_and_rotate_rect_draw_dynamic_QPainter_QTimer.py b/qt__pyqt__pyside__pyqode/animate_move_and_rotate_rect_draw_dynamic_QPainter_QTimer.py index 414c6724f..dd7ca64ea 100644 --- a/qt__pyqt__pyside__pyqode/animate_move_and_rotate_rect_draw_dynamic_QPainter_QTimer.py +++ b/qt__pyqt__pyside__pyqode/animate_move_and_rotate_rect_draw_dynamic_QPainter_QTimer.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import math @@ -12,7 +12,7 @@ class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.angle = 0 @@ -24,7 +24,7 @@ def __init__(self): self.timer.timeout.connect(self._do_tick) self.timer.start() - def _do_tick(self): + def _do_tick(self) -> None: self.angle += 5 self.x += 5 @@ -35,7 +35,7 @@ def _do_tick(self): # Перерисование окна self.update() - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) @@ -48,7 +48,10 @@ def paintEvent(self, event): y = y0 + math.sin(x * frequency) * amplitude / 2 + amplitude / 2 triangle_points = [ - QPointF(0, 0), QPointF(0, 50), QPointF(50, 50), QPointF(50, 0) + QPointF(0, 0), + QPointF(0, 50), + QPointF(50, 50), + QPointF(50, 0), ] for p in triangle_points: p.setX(p.x() + x) @@ -67,10 +70,12 @@ def paintEvent(self, event): p1 = p2 painter.restore() - transform = QTransform() \ - .translate(center.x(), center.y())\ - .rotate(self.angle)\ + transform = ( + QTransform() + .translate(center.x(), center.y()) + .rotate(self.angle) .translate(-center.x(), -center.y()) + ) p = transform.map(polygon) @@ -78,7 +83,7 @@ def paintEvent(self, event): painter.drawPolygon(p) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/animate_move_and_rotate_triangle_QPolygonF_QPainter_QTimer.py b/qt__pyqt__pyside__pyqode/animate_move_and_rotate_triangle_QPolygonF_QPainter_QTimer.py index 60aa71918..27e1c4c38 100644 --- a/qt__pyqt__pyside__pyqode/animate_move_and_rotate_triangle_QPolygonF_QPainter_QTimer.py +++ b/qt__pyqt__pyside__pyqode/animate_move_and_rotate_triangle_QPolygonF_QPainter_QTimer.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import math @@ -12,7 +12,7 @@ class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.angle = 0 @@ -23,7 +23,7 @@ def __init__(self): self.timer.timeout.connect(self._do_tick) self.timer.start() - def _do_tick(self): + def _do_tick(self) -> None: self.angle += 5 self.x += 5 @@ -33,7 +33,7 @@ def _do_tick(self): # Перерисование окна self.update() - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) @@ -58,9 +58,7 @@ def paintEvent(self, event): x = self.x y = y0 + math.sin(x * frequency) * amplitude / 2 + amplitude / 2 - triangle_points = [ - QPointF(0, 0), QPointF(0, 50), QPointF(50, 50) - ] + triangle_points = [QPointF(0, 0), QPointF(0, 50), QPointF(50, 50)] for p in triangle_points: p.setX(p.x() + x) p.setY(p.y() + y) @@ -68,16 +66,18 @@ def paintEvent(self, event): p = QPolygonF(triangle_points) center = p.boundingRect().center() - transform = QTransform() \ - .translate(center.x(), center.y())\ - .rotate(self.angle)\ + transform = ( + QTransform() + .translate(center.x(), center.y()) + .rotate(self.angle) .translate(-center.x(), -center.y()) + ) p = transform.map(p) painter.drawPolygon(p) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/animate_move_and_rotate_triangle_draw_dynamic_QPolygonF_QPainter_QTimer.py b/qt__pyqt__pyside__pyqode/animate_move_and_rotate_triangle_draw_dynamic_QPolygonF_QPainter_QTimer.py index bb5d054b4..05f066d5e 100644 --- a/qt__pyqt__pyside__pyqode/animate_move_and_rotate_triangle_draw_dynamic_QPolygonF_QPainter_QTimer.py +++ b/qt__pyqt__pyside__pyqode/animate_move_and_rotate_triangle_draw_dynamic_QPolygonF_QPainter_QTimer.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import math @@ -12,7 +12,7 @@ class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.angle = 0 @@ -24,7 +24,7 @@ def __init__(self): self.timer.timeout.connect(self._do_tick) self.timer.start() - def _do_tick(self): + def _do_tick(self) -> None: self.angle += 5 self.x += 5 @@ -35,7 +35,7 @@ def _do_tick(self): # Перерисование окна self.update() - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) @@ -47,9 +47,7 @@ def paintEvent(self, event): x = self.x y = y0 + math.sin(x * frequency) * amplitude / 2 + amplitude / 2 - triangle_points = [ - QPointF(0, 0), QPointF(0, 50), QPointF(50, 50) - ] + triangle_points = [QPointF(0, 0), QPointF(0, 50), QPointF(50, 50)] for p in triangle_points: p.setX(p.x() + x) p.setY(p.y() + y) @@ -67,10 +65,12 @@ def paintEvent(self, event): p1 = p2 painter.restore() - transform = QTransform() \ - .translate(center.x(), center.y())\ - .rotate(self.angle)\ + transform = ( + QTransform() + .translate(center.x(), center.y()) + .rotate(self.angle) .translate(-center.x(), -center.y()) + ) p = transform.map(polygon) @@ -78,7 +78,7 @@ def paintEvent(self, event): painter.drawPolygon(p) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/animate_rotate_triangle_QPainterPath_QPainter_QTimer.py b/qt__pyqt__pyside__pyqode/animate_rotate_triangle_QPainterPath_QPainter_QTimer.py index 05aa6c47f..38ad62cf4 100644 --- a/qt__pyqt__pyside__pyqode/animate_rotate_triangle_QPainterPath_QPainter_QTimer.py +++ b/qt__pyqt__pyside__pyqode/animate_rotate_triangle_QPainterPath_QPainter_QTimer.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QWidget -from PyQt5.QtGui import QPainter, QPainterPath, QTransform, QPolygonF +from PyQt5.QtGui import QPainter, QPainterPath, QTransform from PyQt5.QtCore import QPointF, Qt, QRectF, QTimer class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.angle = 0 @@ -20,13 +20,13 @@ def __init__(self): self.timer.timeout.connect(self._do_tick) self.timer.start() - def _do_tick(self): + def _do_tick(self) -> None: self.angle += 5 # Перерисования окна self.update() - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) @@ -46,17 +46,19 @@ def paintEvent(self, event): path.setElementPositionAt(i, point.x(), point.y()) center = rect.center() - transform = QTransform()\ - .translate(center.x() + x, center.y() + y)\ - .rotate(self.angle)\ + transform = ( + QTransform() + .translate(center.x() + x, center.y() + y) + .rotate(self.angle) .translate(-center.x() - x, -center.y() - y) + ) path = transform.map(path) painter.fillPath(path, Qt.blue) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/animate_rotate_triangle_QPolygonF_QPainter_QTimer.py b/qt__pyqt__pyside__pyqode/animate_rotate_triangle_QPolygonF_QPainter_QTimer.py index 476b3aa89..5749c31fc 100644 --- a/qt__pyqt__pyside__pyqode/animate_rotate_triangle_QPolygonF_QPainter_QTimer.py +++ b/qt__pyqt__pyside__pyqode/animate_rotate_triangle_QPolygonF_QPainter_QTimer.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QWidget @@ -10,7 +10,7 @@ class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.angle = 0 @@ -20,13 +20,13 @@ def __init__(self): self.timer.timeout.connect(self._do_tick) self.timer.start() - def _do_tick(self): + def _do_tick(self) -> None: self.angle += 5 # Перерисования окна self.update() - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) @@ -34,9 +34,7 @@ def paintEvent(self, event): painter.setBrush(Qt.blue) - triangle_points = [ - QPointF(0, 0), QPointF(0, 50), QPointF(50, 50) - ] + triangle_points = [QPointF(0, 0), QPointF(0, 50), QPointF(50, 50)] for p in triangle_points: p.setX(p.x() + x) p.setY(p.y() + y) @@ -44,16 +42,18 @@ def paintEvent(self, event): p = QPolygonF(triangle_points) center = p.boundingRect().center() - transform = QTransform() \ - .translate(center.x(), center.y())\ - .rotate(self.angle)\ + transform = ( + QTransform() + .translate(center.x(), center.y()) + .rotate(self.angle) .translate(-center.x(), -center.y()) + ) p = transform.map(p) painter.drawPolygon(p) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/append_custom_QStatusBar__with_icon/main.py b/qt__pyqt__pyside__pyqode/append_custom_QStatusBar__with_icon/main.py index 9efdfca20..43224f8e8 100644 --- a/qt__pyqt__pyside__pyqode/append_custom_QStatusBar__with_icon/main.py +++ b/qt__pyqt__pyside__pyqode/append_custom_QStatusBar__with_icon/main.py @@ -1,21 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class Widget(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.indicator = Qt.QLabel() self.indicator.hide() self.indicator.setFixedSize(32, 32) self.indicator.setScaledContents(True) - self.indicator.setPixmap(Qt.QPixmap('light-bulb-icon_34400.png')) + self.indicator.setPixmap(Qt.QPixmap("light-bulb-icon_34400.png")) self.line_edit = Qt.QLineEdit() self.line_edit.textEdited.connect(self._on_text_edited) @@ -25,14 +25,17 @@ def __init__(self): self.status_bar.setSizeGripEnabled(False) # Убираем снизу-справа уголок main_layout = Qt.QVBoxLayout() - main_layout.setContentsMargins(5, 5, 5, 0) # Сверху, слева и справа отступ 5, внизу его нет + # Сверху, слева и справа отступ 5, внизу его нет + main_layout.setContentsMargins( + 5, 5, 5, 0 + ) main_layout.addWidget(self.line_edit) main_layout.addStretch() main_layout.addWidget(self.status_bar) self.setLayout(main_layout) - def _on_text_edited(self, text): + def _on_text_edited(self, text) -> None: # Показываем сообщение на 2 секунды self.status_bar.showMessage(text, msecs=2000) @@ -43,7 +46,7 @@ def _on_text_edited(self, text): Qt.QTimer.singleShot(2000, self.indicator.hide) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/autowidth__QLineEdit.py b/qt__pyqt__pyside__pyqode/autowidth__QLineEdit.py index 77eb3a201..cda5582f3 100644 --- a/qt__pyqt__pyside__pyqode/autowidth__QLineEdit.py +++ b/qt__pyqt__pyside__pyqode/autowidth__QLineEdit.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class Widget(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.line_edit = Qt.QLineEdit() @@ -19,13 +19,13 @@ def __init__(self): self.setLayout(layout) - def on_text_changed(self, text): + def on_text_changed(self, text) -> None: # Рассчитываем ширину текст по шрифту width = self.line_edit.fontMetrics().width(text) self.line_edit.setMinimumWidth(width) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = Widget() diff --git a/qt__pyqt__pyside__pyqode/bar_chart__QtChart.py b/qt__pyqt__pyside__pyqode/bar_chart__QtChart.py index 609467bc7..064c10a6a 100644 --- a/qt__pyqt__pyside__pyqode/bar_chart__QtChart.py +++ b/qt__pyqt__pyside__pyqode/bar_chart__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import random @@ -12,7 +12,7 @@ class MainWidow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() series = self.append_series() @@ -48,6 +48,7 @@ def append_series(self): if __name__ == "__main__": import sys + app = QApplication(sys.argv) mw = MainWidow() diff --git a/qt__pyqt__pyside__pyqode/buttons_setShortcut.py b/qt__pyqt__pyside__pyqode/buttons_setShortcut.py index 458a5746c..d2150a9cc 100644 --- a/qt__pyqt__pyside__pyqode/buttons_setShortcut.py +++ b/qt__pyqt__pyside__pyqode/buttons_setShortcut.py @@ -1,23 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QPushButton, QLabel class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() label_result = QLabel() - button_left = QPushButton('Left') + button_left = QPushButton("Left") button_left.setShortcut("Ctrl+Alt+Left") button_left.clicked.connect(lambda: label_result.setText("left")) - button_right = QPushButton('Right') + button_right = QPushButton("Right") button_right.setShortcut("Ctrl+Alt+Right") button_right.clicked.connect(lambda: label_result.setText("right")) @@ -27,7 +27,7 @@ def __init__(self): main_layout.addWidget(button_right) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__QtChart.py index 020e2e975..1035e2e11 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QMainWindow @@ -10,7 +10,7 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() @@ -34,7 +34,7 @@ def __init__(self): self.setCentralWidget(chart_view) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__dark_theme__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__dark_theme__QtChart.py index 666007d6f..62e431635 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__dark_theme__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__dark_theme__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QMainWindow @@ -10,7 +10,7 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() @@ -35,7 +35,7 @@ def __init__(self): self.setCentralWidget(chart_view) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_QDateTime__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_QDateTime__QtChart.py index 837eb065e..0c5545dcd 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_QDateTime__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_QDateTime__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://doc.qt.io/qt-5/qtcharts-datetimeaxis-example.html @@ -10,7 +10,6 @@ from PyQt5.QtWidgets import QApplication, QMainWindow from PyQt5.QtGui import QPainter from PyQt5.QtChart import QChart, QChartView, QLineSeries, QDateTimeAxis, QValueAxis - from PyQt5.QtCore import QDateTime, Qt @@ -25,7 +24,7 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() @@ -43,12 +42,12 @@ def __init__(self): axisX = QDateTimeAxis() axisX.setFormat("dd/MM/yyyy") - axisX.setTitleText('Date') + axisX.setTitleText("Date") chart.addAxis(axisX, Qt.AlignBottom) series.attachAxis(axisX) axisY = QValueAxis() - axisY.setTitleText('Value') + axisY.setTitleText("Value") chart.addAxis(axisY, Qt.AlignLeft) series.attachAxis(axisY) @@ -59,7 +58,7 @@ def __init__(self): self.setCentralWidget(chart_view) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_datetime__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_datetime__QtChart.py index f8aa9f212..e29a2278e 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_datetime__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__datetime_axis__using_datetime__QtChart.py @@ -1,19 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://doc.qt.io/qt-5/qtcharts-datetimeaxis-example.html -import datetime as DT - +import datetime as dt from PyQt5.QtWidgets import QApplication, QMainWindow from PyQt5.QtGui import QPainter from PyQt5.QtChart import QChart, QChartView, QLineSeries, QDateTimeAxis, QValueAxis - from PyQt5.QtCore import Qt @@ -28,13 +26,13 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() for date, value in DATA: - date_value = DT.datetime(*date).timestamp() * 1000 + date_value = dt.datetime(*date).timestamp() * 1000 series.append(date_value, value) chart = QChart() @@ -46,13 +44,13 @@ def __init__(self): axisX = QDateTimeAxis() axisX.setFormat("dd/MM/yyyy") - axisX.setTitleText('Date') + axisX.setTitleText("Date") # axisX.setTickCount(10) chart.addAxis(axisX, Qt.AlignBottom) series.attachAxis(axisX) axisY = QValueAxis() - axisY.setTitleText('Value') + axisY.setTitleText("Value") chart.addAxis(axisY, Qt.AlignLeft) series.attachAxis(axisY) @@ -63,7 +61,7 @@ def __init__(self): self.setCentralWidget(chart_view) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__no_margin__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__no_margin__QtChart.py index 9c9219212..59aa609b7 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__no_margin__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__no_margin__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication @@ -31,7 +31,7 @@ def create_QChartView() -> QChartView: return chart_view -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = create_QChartView() diff --git a/qt__pyqt__pyside__pyqode/chart_line__show_all_theme__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__show_all_theme__QtChart.py index da5c51836..906164059 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__show_all_theme__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__show_all_theme__QtChart.py @@ -1,18 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import List, Tuple - from PyQt5.QtWidgets import QApplication, QMainWindow, QTabWidget from PyQt5.QtGui import QPainter from PyQt5.QtChart import QChart, QChartView, QLineSeries -def get_themes() -> List[Tuple[str, QChart.ChartTheme]]: - NAME = 'ChartTheme' +def get_themes() -> list[tuple[str, QChart.ChartTheme]]: + NAME = "ChartTheme" themes = [] for theme_name in dir(QChart): @@ -20,7 +18,7 @@ def get_themes() -> List[Tuple[str, QChart.ChartTheme]]: continue theme = getattr(QChart, theme_name) - theme_name = theme_name.replace(NAME, '') + theme_name = theme_name.replace(NAME, "") themes.append((theme_name, theme)) @@ -29,13 +27,13 @@ def get_themes() -> List[Tuple[str, QChart.ChartTheme]]: class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() tab_widget = QTabWidget() chart_view = self.create_QChartView() - tab_widget.addTab(chart_view, '') + tab_widget.addTab(chart_view, "") for name, theme in get_themes(): chart_view = self.create_QChartView() @@ -67,7 +65,7 @@ def create_QChartView(self) -> QChartView: return chart_view -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__show_point_marks__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__show_point_marks__QtChart.py index b8f0f56a9..75a63f16a 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__show_point_marks__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__show_point_marks__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QMainWindow @@ -10,7 +10,7 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() @@ -37,7 +37,7 @@ def __init__(self): self.setCentralWidget(chart_view) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__QtChart.py index 81277edde..ed1edad19 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__QtChart.py @@ -1,21 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import math from PyQt5.QtWidgets import QApplication -from PyQt5.QtGui import QPainter -from PyQt5.QtCore import QRectF, QPoint, QSizeF -from PyQt5.QtChart import QChart, QChartView, QLineSeries +from PyQt5.QtChart import QChart, QLineSeries from chart_line__show_tooltip_on_series__QtChart import Callout, ChartViewToolTips class MainWindow(ChartViewToolTips): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() @@ -40,7 +38,7 @@ def __init__(self): self.setChart(self._chart) - def show_series_tooltip(self, point, state: bool): + def show_series_tooltip(self, point, state: bool) -> None: if not self._tooltip: self._tooltip = Callout(self._chart) @@ -53,7 +51,7 @@ def show_series_tooltip(self, point, state: bool): + (p.y() - point.y()) * (p.y() - point.y()) ) if current_distance < distance: - self._tooltip.setText("X: {}\nY: {}".format(p.x(), p.y())) + self._tooltip.setText(f"X: {p.x()}\nY: {p.y()}") self._tooltip.setAnchor(p) self._tooltip.setZValue(11) self._tooltip.updateGeometry() @@ -62,7 +60,7 @@ def show_series_tooltip(self, point, state: bool): self._tooltip.hide() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__check_distance_by_pos__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__check_distance_by_pos__QtChart.py index fd900bfa7..81aa59b79 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__check_distance_by_pos__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_point_marks__check_distance_by_pos__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import math @@ -13,7 +13,7 @@ class MainWindow(ChartViewToolTips): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() @@ -38,7 +38,7 @@ def __init__(self): self.setChart(self._chart) - def show_series_tooltip(self, point, state: bool): + def show_series_tooltip(self, point, state: bool) -> None: # value -> pos point = self._chart.mapToPosition(point) @@ -56,7 +56,7 @@ def show_series_tooltip(self, point, state: bool): + (p.y() - point.y()) * (p.y() - point.y()) ) if current_distance < distance: - self._tooltip.setText("X: {}\nY: {}".format(p.x(), p.y())) + self._tooltip.setText(f"X: {p.x()}\nY: {p.y()}") self._tooltip.setAnchor(p_value) self._tooltip.setZValue(11) self._tooltip.updateGeometry() @@ -65,7 +65,7 @@ def show_series_tooltip(self, point, state: bool): self._tooltip.hide() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_series__QtChart.py b/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_series__QtChart.py index d31d99f11..9faf17c25 100644 --- a/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_series__QtChart.py +++ b/qt__pyqt__pyside__pyqode/chart_line__show_tooltip_on_series__QtChart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QGraphicsItem, QGraphicsSceneMouseEvent @@ -12,7 +12,7 @@ # SOURCE: https://doc-snapshots.qt.io/qt5-5.12/qtcharts-callout-callout-h.html class Callout(QGraphicsItem): - def __init__(self, chart: QChart, parent=None): + def __init__(self, chart: QChart, parent=None) -> None: super().__init__(parent) self.hide() @@ -20,26 +20,26 @@ def __init__(self, chart: QChart, parent=None): self._chart = chart self._chart.scene().addItem(self) - self._text = '' + self._text = "" self._textRect = QRectF() self._rect = QRectF() self._anchor = QPointF() self._font = QFont() - def setText(self, text: str): + def setText(self, text: str) -> None: self._text = text metrics = QFontMetrics(self._font) - self._textRect = QRectF(metrics.boundingRect( - QRect(0, 0, 150, 150), Qt.AlignLeft, self._text - )) + self._textRect = QRectF( + metrics.boundingRect(QRect(0, 0, 150, 150), Qt.AlignLeft, self._text) + ) self._textRect.translate(5, 5) self.prepareGeometryChange() self._rect = QRectF(self._textRect.adjusted(-5, -5, 5, 5)) - def setAnchor(self, point: QPointF): + def setAnchor(self, point: QPointF) -> None: self._anchor = point - def updateGeometry(self): + def updateGeometry(self) -> None: self.prepareGeometryChange() self.setPos(self._chart.mapToPosition(self._anchor) + QPoint(10, -50)) @@ -70,42 +70,77 @@ def boundingRect(self): rect.setBottom(max(self._rect.bottom(), anchor.y())) return rect - def paint(self, painter: QPainter, option, widget=None): + def paint(self, painter: QPainter, option, widget=None) -> None: path = QPainterPath() path.addRoundedRect(self._rect, 5, 5) - + anchor = self.mapFromParent(self._chart.mapToPosition(self._anchor)) if not self._rect.contains(anchor): point1 = QPointF() point2 = QPointF() - + # establish the position of the anchor point in relation to self._rect above = anchor.y() <= self._rect.top() - aboveCenter = anchor.y() > self._rect.top() and anchor.y() <= self._rect.center().y() - belowCenter = anchor.y() > self._rect.center().y() and anchor.y() <= self._rect.bottom() + aboveCenter = ( + anchor.y() > self._rect.top() and anchor.y() <= self._rect.center().y() + ) + belowCenter = ( + anchor.y() > self._rect.center().y() + and anchor.y() <= self._rect.bottom() + ) below = anchor.y() > self._rect.bottom() - + onLeft = anchor.x() <= self._rect.left() - leftOfCenter = anchor.x() > self._rect.left() and anchor.x() <= self._rect.center().x() - rightOfCenter = anchor.x() > self._rect.center().x() and anchor.x() <= self._rect.right() + leftOfCenter = ( + anchor.x() > self._rect.left() and anchor.x() <= self._rect.center().x() + ) + rightOfCenter = ( + anchor.x() > self._rect.center().x() + and anchor.x() <= self._rect.right() + ) onRight = anchor.x() > self._rect.right() - + # get the nearest self._rect corner. x = (onRight + rightOfCenter) * self._rect.width() y = (below + belowCenter) * self._rect.height() - cornerCase = (above and onLeft) or (above and onRight) or (below and onLeft) or (below and onRight) + cornerCase = ( + (above and onLeft) + or (above and onRight) + or (below and onLeft) + or (below and onRight) + ) vertical = abs(anchor.x() - x) > abs(anchor.y() - y) - - x1 = x + leftOfCenter * 10 - rightOfCenter * 20 + cornerCase * (not vertical) * (onLeft * 10 - onRight * 20) - y1 = y + aboveCenter * 10 - belowCenter * 20 + cornerCase * vertical * (above * 10 - below * 20) + + x1 = ( + x + + leftOfCenter * 10 + - rightOfCenter * 20 + + cornerCase * (not vertical) * (onLeft * 10 - onRight * 20) + ) + y1 = ( + y + + aboveCenter * 10 + - belowCenter * 20 + + cornerCase * vertical * (above * 10 - below * 20) + ) point1.setX(x1) point1.setY(y1) - - x2 = x + leftOfCenter * 20 - rightOfCenter * 10 + cornerCase * (not vertical) * (onLeft * 20 - onRight * 10) - y2 = y + aboveCenter * 20 - belowCenter * 10 + cornerCase * vertical * (above * 20 - below * 10) + + x2 = ( + x + + leftOfCenter * 20 + - rightOfCenter * 10 + + cornerCase * (not vertical) * (onLeft * 20 - onRight * 10) + ) + y2 = ( + y + + aboveCenter * 20 + - belowCenter * 10 + + cornerCase * vertical * (above * 20 - below * 10) + ) point2.setX(x2) point2.setY(y2) - + path.moveTo(point1) path.lineTo(anchor) path.lineTo(point2) @@ -116,19 +151,21 @@ def paint(self, painter: QPainter, option, widget=None): painter.drawPath(path) painter.drawText(self._textRect, self._text) - def mousePressEvent(self, event: QGraphicsSceneMouseEvent): + def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: event.setAccepted(True) - def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): + def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: if event.buttons() & Qt.LeftButton: - self.setPos(self.mapToParent(event.pos() - event.buttonDownPos(Qt.LeftButton))) + self.setPos( + self.mapToParent(event.pos() - event.buttonDownPos(Qt.LeftButton)) + ) event.setAccepted(True) else: event.setAccepted(False) class ChartViewToolTips(QChartView): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setRenderHint(QPainter.Antialiasing) @@ -137,7 +174,7 @@ def __init__(self): self._callout_font_family = None self._callouts = [] - def clear_all_tooltips(self): + def clear_all_tooltips(self) -> None: if self._tooltip: self.scene().removeItem(self._tooltip) @@ -152,7 +189,7 @@ def _add_Callout(self) -> Callout: return callout - def show_series_tooltip(self, point, state: bool): + def show_series_tooltip(self, point, state: bool) -> None: if not self.chart(): return @@ -160,7 +197,7 @@ def show_series_tooltip(self, point, state: bool): self._tooltip = self._add_Callout() if state: - self._tooltip.setText("X: {} \nY: {}".format(point.x(), point.y())) + self._tooltip.setText(f"X: {point.x()} \nY: {point.y()}") self._tooltip.setAnchor(point) self._tooltip.setZValue(11) self._tooltip.updateGeometry() @@ -168,7 +205,7 @@ def show_series_tooltip(self, point, state: bool): else: self._tooltip.hide() - def keepCallout(self, point): + def keepCallout(self, point) -> None: if not self.chart() or not self._tooltip: return @@ -177,7 +214,7 @@ def keepCallout(self, point): self._tooltip = self._add_Callout() - def mouseReleaseEvent(self, event): + def mouseReleaseEvent(self, event) -> None: if self.chart(): pos = event.pos() point = self.chart().mapToValue(pos) @@ -185,7 +222,7 @@ def mouseReleaseEvent(self, event): super().mouseReleaseEvent(event) - def resizeEvent(self, event): + def resizeEvent(self, event) -> None: if self.scene(): size = QSizeF(event.size()) @@ -199,7 +236,7 @@ def resizeEvent(self, event): class MainWindow(ChartViewToolTips): - def __init__(self): + def __init__(self) -> None: super().__init__() series = QLineSeries() @@ -225,7 +262,7 @@ def __init__(self): self.setChart(self._chart) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/check_image_on_transparents/main.py b/qt__pyqt__pyside__pyqode/check_image_on_transparents/main.py index 473101a01..c79b07e5c 100644 --- a/qt__pyqt__pyside__pyqode/check_image_on_transparents/main.py +++ b/qt__pyqt__pyside__pyqode/check_image_on_transparents/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QWidget @@ -9,17 +9,17 @@ from PyQt5.QtCore import Qt, QRect -FILE_NAME = 'emoji-video-game-512x512.png' +FILE_NAME = "emoji-video-game-512x512.png" class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.img = QPixmap() self.img.load(FILE_NAME) - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setBrush(Qt.darkGreen) @@ -32,7 +32,7 @@ def paintEvent(self, event): painter.drawPixmap(img_rect.topLeft(), self.img) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = Widget() diff --git a/qt__pyqt__pyside__pyqode/check_urls.py b/qt__pyqt__pyside__pyqode/check_urls.py index 73515889f..28ad3b224 100644 --- a/qt__pyqt__pyside__pyqode/check_urls.py +++ b/qt__pyqt__pyside__pyqode/check_urls.py @@ -1,29 +1,35 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import traceback import sys -from typing import List +import requests from PyQt5.QtWidgets import ( - QApplication, QMainWindow, QTableWidget, QTableWidgetItem, QMessageBox, QPushButton, - QHBoxLayout, QVBoxLayout, QWidget, QPlainTextEdit + QApplication, + QMainWindow, + QTableWidget, + QTableWidgetItem, + QMessageBox, + QPushButton, + QHBoxLayout, + QVBoxLayout, + QWidget, + QPlainTextEdit, ) from PyQt5.QtCore import QThread, pyqtSignal, Qt -import requests - -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit() @@ -31,18 +37,20 @@ def log_uncaught_exceptions(ex_cls, ex, tb): session = requests.session() -session.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0' +session.headers[ + "User-Agent" +] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0" class CheckUrlThread(QThread): about_check_url = pyqtSignal(str, str) - def __init__(self, urls: List[str] = None): + def __init__(self, urls: list[str] = None) -> None: super().__init__() self.urls = urls if urls else [] - def run(self): + def run(self) -> None: for url in self.urls: try: rs = session.get(url) @@ -58,7 +66,7 @@ def run(self): class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.urls = QPlainTextEdit() @@ -72,7 +80,7 @@ def __init__(self): self.result_table.horizontalHeader().setStretchLastSection(True) self.result_table.horizontalHeader().resizeSection(0, 200) - self.pb_check = QPushButton('Check') + self.pb_check = QPushButton("Check") self.pb_check.clicked.connect(self._on_click_check) layout = QHBoxLayout() @@ -92,7 +100,7 @@ def __init__(self): self.thread.started.connect(lambda: self.pb_check.setEnabled(False)) self.thread.finished.connect(lambda: self.pb_check.setEnabled(True)) - def _on_click_check(self): + def _on_click_check(self) -> None: urls = self.urls.toPlainText().strip().splitlines() self.result_table.clearContents() @@ -108,7 +116,7 @@ def _on_click_check(self): self.thread.urls = urls self.thread.start() - def _on_about_check_url(self, url: str, code: str): + def _on_about_check_url(self, url: str, code: str) -> None: for item in self.result_table.findItems(url, Qt.MatchCaseSensitive): row = item.row() self.result_table.item(row, 1).setText(code) @@ -117,13 +125,15 @@ def _on_about_check_url(self, url: str, code: str): if __name__ == "__main__": app = QApplication([]) - text = '\n'.join([ - 'https://ru.stackoverflow.com', - 'https://ru.stackoverflow.com/questions/893436/', - 'https://google.com', - 'http://ya.ru', - 'http://not_found_site.123', - ]) + text = "\n".join( + [ + "https://ru.stackoverflow.com", + "https://ru.stackoverflow.com/questions/893436/", + "https://google.com", + "http://ya.ru", + "http://not_found_site.123", + ] + ) mw = MainWindow() mw.urls.setPlainText(text) diff --git a/qt__pyqt__pyside__pyqode/clickable_image/main.py b/qt__pyqt__pyside__pyqode/clickable_image/main.py index f2520e183..85792d283 100644 --- a/qt__pyqt__pyside__pyqode/clickable_image/main.py +++ b/qt__pyqt__pyside__pyqode/clickable_image/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt @@ -10,17 +10,17 @@ app = Qt.QApplication([]) button = Qt.QToolButton() -button.setIcon(Qt.QIcon('img.png')) +button.setIcon(Qt.QIcon("img.png")) button.setAutoRaise(True) button.setMinimumSize(40, 50) button.setIconSize(button.minimumSize()) layout = Qt.QVBoxLayout() -layout.addWidget(Qt.QLabel('Example:')) +layout.addWidget(Qt.QLabel("Example:")) layout.addWidget(button) central = Qt.QWidget() -central.setStyleSheet('background-color: green;') +central.setStyleSheet("background-color: green;") central.setLayout(layout) mw = Qt.QMainWindow() diff --git a/qt__pyqt__pyside__pyqode/clipboard__example.py b/qt__pyqt__pyside__pyqode/clipboard__example.py index 1c8d522bd..c4e46d003 100644 --- a/qt__pyqt__pyside__pyqode/clipboard__example.py +++ b/qt__pyqt__pyside__pyqode/clipboard__example.py @@ -1,29 +1,37 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QLabel, QPushButton, QMessageBox +import sys +import traceback + +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QGridLayout, + QLabel, + QPushButton, + QMessageBox, +) from PyQt5.QtCore import Qt -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowFlag(Qt.WindowStaysOnTopHint) @@ -36,13 +44,13 @@ def __init__(self): button = QPushButton(x) button.clicked.connect(lambda ok, x=x: QApplication.clipboard().setText(x)) - layout.addWidget(QLabel(f'#{i}'), 0, i, Qt.AlignHCenter) + layout.addWidget(QLabel(f"#{i}"), 0, i, Qt.AlignHCenter) layout.addWidget(button, 1, i) self.setLayout(layout) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/qt_custom_tray_menu.py b/qt__pyqt__pyside__pyqode/context_menu/qt_custom_tray_menu.py similarity index 89% rename from qt__pyqt__pyside__pyqode/qt_custom_tray_menu.py rename to qt__pyqt__pyside__pyqode/context_menu/qt_custom_tray_menu.py index 6c38e5d1d..d8c3c2d45 100644 --- a/qt__pyqt__pyside__pyqode/qt_custom_tray_menu.py +++ b/qt__pyqt__pyside__pyqode/context_menu/qt_custom_tray_menu.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QSystemTrayIcon, QStyle, QLabel, QWidgetAction, QMenu @@ -15,7 +15,7 @@ tray = QSystemTrayIcon(icon) -menu_widget = QLabel('Hello, World!') +menu_widget = QLabel("Hello, World!") menu_widget_action = QWidgetAction(menu_widget) menu_widget_action.setDefaultWidget(menu_widget) diff --git a/qt__pyqt__pyside__pyqode/context_menu/qt_tray_with_simple_menu.py b/qt__pyqt__pyside__pyqode/context_menu/qt_tray_with_simple_menu.py new file mode 100644 index 000000000..66b358a0b --- /dev/null +++ b/qt__pyqt__pyside__pyqode/context_menu/qt_tray_with_simple_menu.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.Qt import QApplication, QSystemTrayIcon, QStyle, QMenu, QMessageBox, QWidget + + +app = QApplication([]) +# app.setQuitOnLastWindowClosed(False) + +mw = QWidget() +mw.show() + +icon = app.style().standardIcon(QStyle.SP_DirOpenIcon) +# OR +# icon = QIcon('favicon.ico') + +tray = QSystemTrayIcon(icon) + +menu = QMenu() + +menu_about = menu.addMenu("About") +action_about = menu_about.addAction("About") +action_about.triggered.connect( + lambda: QMessageBox.information(None, "Info", "Hello World!") +) + +action_about_qt = menu_about.addAction("About Qt") +action_about_qt.triggered.connect(lambda: QMessageBox.aboutQt(None)) + +menu.addSeparator() +action_quit = menu.addAction("Quit") +action_quit.triggered.connect(app.quit) + +tray.setContextMenu(menu) + + +def on_tray_activated(reason: QSystemTrayIcon.ActivationReason) -> None: + if reason == QSystemTrayIcon.ActivationReason.Context: + return + + mw.setVisible(not mw.isVisible()) + + +tray.activated.connect(on_tray_activated) + +tray.show() + +app.exec() diff --git a/qt__pyqt__pyside__pyqode/context_menu/table.py b/qt__pyqt__pyside__pyqode/context_menu/table.py new file mode 100644 index 000000000..f64040069 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/context_menu/table.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os + +from PyQt5.QtCore import QPoint, Qt, QModelIndex +from PyQt5.QtGui import QStandardItemModel, QStandardItem +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QVBoxLayout, + QTableWidget, + QTableWidgetItem, + QTableView, + QMenu, +) + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + header_labels = ["NAME", "URL"] + + self.table_widget = QTableWidget() + self.table_widget.setEditTriggers(QTableView.NoEditTriggers) + self.table_widget.setSelectionBehavior(QTableView.SelectRows) + self.table_widget.setSelectionMode(QTableView.SingleSelection) + self.table_widget.setColumnCount(len(header_labels)) + self.table_widget.setHorizontalHeaderLabels(header_labels) + self.table_widget.horizontalHeader().setStretchLastSection(True) + self.table_widget.setContextMenuPolicy(Qt.CustomContextMenu) + self.table_widget.customContextMenuRequested.connect( + self._custom_menu_requested + ) + + self.model = QStandardItemModel() + self.model.setHorizontalHeaderLabels(header_labels) + + self.table_view = QTableView() + self.table_view.setEditTriggers(QTableView.NoEditTriggers) + self.table_view.setSelectionBehavior(QTableView.SelectRows) + self.table_view.setSelectionMode(QTableView.SingleSelection) + self.table_view.horizontalHeader().setStretchLastSection(True) + self.table_view.setModel(self.model) + self.table_view.setContextMenuPolicy(Qt.CustomContextMenu) + self.table_view.customContextMenuRequested.connect(self._custom_menu_requested) + + main_layout = QVBoxLayout(self) + main_layout.addWidget(self.table_view) + main_layout.addWidget(self.table_widget) + + self.fill_tables() + + def fill_tables(self) -> None: + self.model.appendRow( + [QStandardItem("rutube"), QStandardItem("https://rutube.ru/")] + ) + self.model.appendRow( + [QStandardItem("yandex"), QStandardItem("https://yandex.ru/")] + ) + self.model.appendRow( + [QStandardItem("youtube"), QStandardItem("https://www.youtube.com")] + ) + self.model.appendRow( + [QStandardItem("google"), QStandardItem("https://google.com/")] + ) + + self.table_widget.setRowCount(self.model.rowCount()) + for row in range(self.model.rowCount()): + for column in range(self.model.columnCount()): + text = self.model.item(row, column).text() + self.table_widget.setItem(row, column, QTableWidgetItem(text)) + + def _custom_menu_requested(self, p: QPoint) -> None: + table = self.sender() + + index: QModelIndex = table.indexAt(p) + if not index.isValid(): + return + + row: int = index.row() + + if isinstance(table, QTableWidget): + name = table.item(row, 0).text() + url = table.item(row, 1).text() + else: + name = self.model.item(row, 0).text() + url = self.model.item(row, 1).text() + + menu = QMenu(self) + menu.addAction(table.__class__.__name__).setEnabled(False) + menu.addSeparator() + menu.addAction("Copy NAME", lambda: QApplication.clipboard().setText(name)) + menu.addAction("Copy URL", lambda: QApplication.clipboard().setText(url)) + menu.addSeparator() + menu.addAction("Open URL", lambda: os.startfile(url)) + menu.exec(table.viewport().mapToGlobal(p)) + + +if __name__ == "__main__": + app = QApplication([]) + + w = MainWindow() + w.resize(600, 500) + w.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/context_menu/table_auto_menu.py b/qt__pyqt__pyside__pyqode/context_menu/table_auto_menu.py new file mode 100644 index 000000000..7722d202c --- /dev/null +++ b/qt__pyqt__pyside__pyqode/context_menu/table_auto_menu.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import os +from typing import Callable + +from PyQt5.QtCore import QPoint, Qt, QModelIndex +from PyQt5.QtGui import QStandardItemModel, QStandardItem +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QVBoxLayout, + QTableWidget, + QTableWidgetItem, + QTableView, + QMenu, + QAction, +) + + +def open_context_menu( + table: QTableView, + p: QPoint, + get_additional_actions_func: Callable[[QTableView, int], list[QAction]] = None, +) -> None: + index: QModelIndex = table.indexAt(p) + if not index.isValid(): + return + + menu = QMenu(table) + menu.addAction(table.__class__.__name__).setEnabled(False) + menu.addSeparator() + + row: int = index.row() + model = table.model() + + for column in range(model.columnCount()): + title = model.headerData(column, Qt.Horizontal) + + idx: QModelIndex = model.index(row, column) + value: str = str(model.data(idx)) + + menu.addAction( + f'Copy "{title}"', + lambda value=value: QApplication.clipboard().setText(value), + ) + + if get_additional_actions_func: + if actions := get_additional_actions_func(table, row): + menu.addSeparator() + menu.addActions(actions) + + menu.exec(table.viewport().mapToGlobal(p)) + + +class MainWindow(QWidget): + def __init__(self) -> None: + super().__init__() + + header_labels = ["NAME", "URL"] + + self.table_widget = QTableWidget() + self.table_widget.setEditTriggers(QTableView.NoEditTriggers) + self.table_widget.setSelectionBehavior(QTableView.SelectRows) + self.table_widget.setSelectionMode(QTableView.SingleSelection) + self.table_widget.setColumnCount(len(header_labels)) + self.table_widget.setHorizontalHeaderLabels(header_labels) + self.table_widget.horizontalHeader().setStretchLastSection(True) + self.table_widget.setContextMenuPolicy(Qt.CustomContextMenu) + self.table_widget.customContextMenuRequested.connect( + self._custom_menu_requested + ) + + self.model = QStandardItemModel() + self.model.setHorizontalHeaderLabels(header_labels) + + self.table_view = QTableView() + self.table_view.setEditTriggers(QTableView.NoEditTriggers) + self.table_view.setSelectionBehavior(QTableView.SelectRows) + self.table_view.setSelectionMode(QTableView.SingleSelection) + self.table_view.horizontalHeader().setStretchLastSection(True) + self.table_view.setModel(self.model) + self.table_view.setContextMenuPolicy(Qt.CustomContextMenu) + self.table_view.customContextMenuRequested.connect(self._custom_menu_requested) + + main_layout = QVBoxLayout(self) + main_layout.addWidget(self.table_view) + main_layout.addWidget(self.table_widget) + + self.fill_tables() + + def fill_tables(self) -> None: + self.model.appendRow( + [QStandardItem("rutube"), QStandardItem("https://rutube.ru/")] + ) + self.model.appendRow( + [QStandardItem("yandex"), QStandardItem("https://yandex.ru/")] + ) + self.model.appendRow( + [QStandardItem("youtube"), QStandardItem("https://www.youtube.com")] + ) + self.model.appendRow( + [QStandardItem("google"), QStandardItem("https://google.com/")] + ) + + self.table_widget.setRowCount(self.model.rowCount()) + for row in range(self.model.rowCount()): + for column in range(self.model.columnCount()): + text = self.model.item(row, column).text() + self.table_widget.setItem(row, column, QTableWidgetItem(text)) + + def _custom_menu_requested(self, p: QPoint) -> None: + def _get_additional_actions(table: QTableView, row: int) -> list[QAction]: + model = table.model() + + actions = [] + for column in range(model.columnCount()): + idx = model.index(row, column) + value: str = str(model.data(idx)) + if not value.startswith("http"): + continue + + action_open_url = QAction(f'Open "{value}"') + action_open_url.triggered.connect(lambda: os.startfile(value)) + + actions.append(action_open_url) + + return actions + + table: QTableView = self.sender() + open_context_menu(table, p, _get_additional_actions) + + +if __name__ == "__main__": + app = QApplication([]) + + w = MainWindow() + w.resize(600, 500) + w.show() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/create_child_window_by_center.py b/qt__pyqt__pyside__pyqode/create_child_window_by_center.py index 5f3c7a3e4..b4835f743 100644 --- a/qt__pyqt__pyside__pyqode/create_child_window_by_center.py +++ b/qt__pyqt__pyside__pyqode/create_child_window_by_center.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QStyle @@ -9,19 +9,19 @@ class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - pb_1 = QPushButton('Create [1]') + pb_1 = QPushButton("Create [1]") pb_1.clicked.connect(self._create_new_window_1) - pb_2 = QPushButton('Create [2]') + pb_2 = QPushButton("Create [2]") pb_2.clicked.connect(self._create_new_window_2) - pb_3 = QPushButton('Create [3]') + pb_3 = QPushButton("Create [3]") pb_3.clicked.connect(self._create_new_window_3) - pb_4 = QPushButton('Create [4]') + pb_4 = QPushButton("Create [4]") pb_4.clicked.connect(self._create_new_window_4) layout = QVBoxLayout() @@ -33,12 +33,12 @@ def __init__(self): self.setLayout(layout) - def _create_new_window_1(self): + def _create_new_window_1(self) -> None: widget = QWidget(self, flags=Qt.Window) widget.resize(200, 100) widget.show() - def _create_new_window_2(self): + def _create_new_window_2(self) -> None: widget = QWidget(self, flags=Qt.Window) widget.resize(200, 100) @@ -47,7 +47,7 @@ def _create_new_window_2(self): widget.show() - def _create_new_window_3(self): + def _create_new_window_3(self) -> None: widget = QWidget(self, flags=Qt.Window) widget.resize(200, 100) @@ -58,15 +58,19 @@ def _create_new_window_3(self): widget.show() - def _create_new_window_4(self): + def _create_new_window_4(self) -> None: widget = QWidget(self, flags=Qt.Window) widget.resize(200, 100) - widget.setGeometry(QStyle.alignedRect(Qt.LeftToRight, Qt.AlignCenter, widget.size(), self.geometry())) + widget.setGeometry( + QStyle.alignedRect( + Qt.LeftToRight, Qt.AlignCenter, widget.size(), self.geometry() + ) + ) widget.show() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/crypto__decrypto__xor.py b/qt__pyqt__pyside__pyqode/crypto__decrypto__xor.py index 995d3ea6e..1f275d954 100644 --- a/qt__pyqt__pyside__pyqode/crypto__decrypto__xor.py +++ b/qt__pyqt__pyside__pyqode/crypto__decrypto__xor.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt @@ -9,27 +9,27 @@ # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/7c3e004f8038cf7942226283fe0f5c184008b561/xor_crypto.py def crypto_xor_1(message, secret): - return ''.join(chr(ord(c) ^ secret) for c in message) + return "".join(chr(ord(c) ^ secret) for c in message) class Window(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('crypto / decrypto') + self.setWindowTitle("crypto / decrypto") - self.pte_text = Qt.QPlainTextEdit('Test / Тест') + self.pte_text = Qt.QPlainTextEdit("Test / Тест") self.sb_key = Qt.QSpinBox() self.sb_key.setRange(1, 1000000) self.sb_key.setValue(42) self.sb_key.setSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Preferred) - self.pb_convert = Qt.QPushButton('Convert') + self.pb_convert = Qt.QPushButton("Convert") self.pb_convert.clicked.connect(self._on_convert) layout_command = Qt.QHBoxLayout() - layout_command.addWidget(Qt.QLabel('Key:')) + layout_command.addWidget(Qt.QLabel("Key:")) layout_command.addWidget(self.sb_key) layout = Qt.QVBoxLayout() @@ -39,7 +39,7 @@ def __init__(self): self.setLayout(layout) - def _on_convert(self): + def _on_convert(self) -> None: text = self.pte_text.toPlainText() key = self.sb_key.value() @@ -48,7 +48,7 @@ def _on_convert(self): self.pte_text.setPlainText(text) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = Window() diff --git a/qt__pyqt__pyside__pyqode/darkstyle__QDarkStyleSheet__load_from_ui/main.py b/qt__pyqt__pyside__pyqode/darkstyle__QDarkStyleSheet__load_from_ui/main.py index cfcfae003..b2affa7f6 100644 --- a/qt__pyqt__pyside__pyqode/darkstyle__QDarkStyleSheet__load_from_ui/main.py +++ b/qt__pyqt__pyside__pyqode/darkstyle__QDarkStyleSheet__load_from_ui/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt @@ -12,16 +12,16 @@ class MainWindow(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - uic.loadUi('mainwidget.ui', self) + uic.loadUi("mainwidget.ui", self) - self.cbPenStyle.addItem('Solid', Qt.Qt.SolidLine) - self.cbPenStyle.addItem('Dash', Qt.Qt.DashLine) - self.cbPenStyle.addItem('Dot', Qt.Qt.DotLine) - self.cbPenStyle.addItem('Dash Dot', Qt.Qt.DashDotLine) - self.cbPenStyle.addItem('Dash Dot Dot', Qt.Qt.DashDotDotLine) + self.cbPenStyle.addItem("Solid", Qt.Qt.SolidLine) + self.cbPenStyle.addItem("Dash", Qt.Qt.DashLine) + self.cbPenStyle.addItem("Dot", Qt.Qt.DotLine) + self.cbPenStyle.addItem("Dash Dot", Qt.Qt.DashDotLine) + self.cbPenStyle.addItem("Dash Dot Dot", Qt.Qt.DashDotDotLine) self.pen_color = Qt.Qt.green @@ -30,7 +30,7 @@ def __init__(self): self._update_pen_color() - def _choose_color(self): + def _choose_color(self) -> None: color = Qt.QColorDialog.getColor(self.pen_color) if not color.isValid(): return @@ -38,21 +38,21 @@ def _choose_color(self): self.pen_color = color self._update_pen_color() - def _update_pen_color(self): + def _update_pen_color(self) -> None: pixmap = Qt.QPixmap(self.pbPenColor.size()) pixmap.fill(self.pen_color) self.pbPenColor.setIcon(Qt.QIcon(pixmap)) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw1 = MainWindow() - mw1.setWindowTitle('Standard') + mw1.setWindowTitle("Standard") mw1.show() mw2 = MainWindow() - mw2.setWindowTitle('Dark') + mw2.setWindowTitle("Dark") mw2.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) mw2.show() diff --git a/qt__pyqt__pyside__pyqode/disable_specific_items_in_QComboBox.py b/qt__pyqt__pyside__pyqode/disable_specific_items_in_QComboBox.py index 4202eea32..029588bef 100644 --- a/qt__pyqt__pyside__pyqode/disable_specific_items_in_QComboBox.py +++ b/qt__pyqt__pyside__pyqode/disable_specific_items_in_QComboBox.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import QApplication, QWidget, QVBoxLayout, QComboBox diff --git a/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QGraphicsScene_QNetworkAccessManager.py b/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QGraphicsScene_QNetworkAccessManager.py index 9b100a1bd..1fa1f3891 100644 --- a/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QGraphicsScene_QNetworkAccessManager.py +++ b/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QGraphicsScene_QNetworkAccessManager.py @@ -1,23 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtGui import QPixmap from PyQt5.QtCore import QUrl from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest from PyQt5.QtWidgets import ( - QApplication, QWidget, QVBoxLayout, QGraphicsScene, QGraphicsView, QGraphicsPixmapItem, QGraphicsTextItem + QApplication, + QWidget, + QVBoxLayout, + QGraphicsScene, + QGraphicsView, + QGraphicsPixmapItem, + QGraphicsTextItem, ) class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.scene = QGraphicsScene() - self.scene.addItem(QGraphicsTextItem('loading...')) + self.scene.addItem(QGraphicsTextItem("loading...")) self.view = QGraphicsView() self.view.setScene(self.scene) @@ -33,7 +39,7 @@ def __init__(self): self.setLayout(layout) - def finish_request(self, reply): + def finish_request(self, reply) -> None: self.scene.clear() img = QPixmap() @@ -43,7 +49,7 @@ def finish_request(self, reply): self.scene.addItem(item) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QLabel_QNetworkAccessManager.py b/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QLabel_QNetworkAccessManager.py index 92c684af3..0b06c223c 100644 --- a/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QLabel_QNetworkAccessManager.py +++ b/qt__pyqt__pyside__pyqode/download_and_show_async_image_on_QLabel_QNetworkAccessManager.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtGui import QPixmap @@ -11,7 +11,7 @@ class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.lbl = QLabel("loading...") @@ -28,14 +28,14 @@ def __init__(self): self.setLayout(layout) - def finish_request(self, reply): + def finish_request(self, reply) -> None: img = QPixmap() img.loadFromData(reply.readAll()) self.lbl.setPixmap(img) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/download_file/complex.py b/qt__pyqt__pyside__pyqode/download_file/complex.py index 474760e48..fb1e0f5b8 100644 --- a/qt__pyqt__pyside__pyqode/download_file/complex.py +++ b/qt__pyqt__pyside__pyqode/download_file/complex.py @@ -1,14 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os.path import traceback from urllib.request import urlretrieve -from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLineEdit, QFormLayout, QTextEdit +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QPushButton, + QLineEdit, + QFormLayout, + QTextEdit, +) from PyQt5.QtCore import QThread, pyqtSignal @@ -17,14 +24,14 @@ class ThreadDownload(QThread): about_file_name = pyqtSignal(str) about_error = pyqtSignal(str) - def __init__(self, url: str = None, file_name: str = None): + def __init__(self, url: str = None, file_name: str = None) -> None: super().__init__() self.url = url self.file_name = file_name # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/4692cbb36b5addb0f2bd5e93e69eb8c7c257bdf8/download_file/with_progress.py#L12 - def reporthook(self, blocknum, blocksize, totalsize): + def reporthook(self, blocknum, blocksize, totalsize) -> None: readsofar = blocknum * blocksize if totalsize > 0: percent = readsofar * 100.0 / totalsize @@ -32,16 +39,23 @@ def reporthook(self, blocknum, blocksize, totalsize): percent = 100 readsofar = totalsize - s = "%5.1f%% %*d / %d" % (percent, len(str(totalsize)), readsofar, totalsize) + s = "%5.1f%% %*d / %d" % ( + percent, + len(str(totalsize)), + readsofar, + totalsize, + ) self.about_progress.emit(s) # Total size is unknown else: self.about_progress.emit(f"read {readsofar}") - def run(self): + def run(self) -> None: try: - file_name, _ = urlretrieve(self.url, self.file_name, reporthook=self.reporthook) + file_name, _ = urlretrieve( + self.url, self.file_name, reporthook=self.reporthook + ) file_name = os.path.abspath(file_name) self.about_file_name.emit(file_name) @@ -50,20 +64,22 @@ def run(self): class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.line_edit_url = QLineEdit('https://codeload.github.com/gil9red/SimplePyScripts/zip/master') - self.line_edit_file_name = QLineEdit('SimplePyScripts.zip') + self.line_edit_url = QLineEdit( + "https://codeload.github.com/gil9red/SimplePyScripts/zip/master" + ) + self.line_edit_file_name = QLineEdit("SimplePyScripts.zip") - self.button_download = QPushButton('Download') + self.button_download = QPushButton("Download") self.button_download.clicked.connect(self.download) self.text_edit_log = QTextEdit() layout = QFormLayout() - layout.addRow('URL:', self.line_edit_url) - layout.addRow('File Name:', self.line_edit_file_name) + layout.addRow("URL:", self.line_edit_url) + layout.addRow("File Name:", self.line_edit_file_name) layout.addWidget(self.button_download) layout.addWidget(self.text_edit_log) @@ -76,22 +92,24 @@ def __init__(self): self.setLayout(layout) - def download(self): + def download(self) -> None: self.thread.url = self.line_edit_url.text() self.thread.file_name = self.line_edit_file_name.text() self.thread.start() - def _handle_about_progress(self, text: str): + def _handle_about_progress(self, text: str) -> None: self.setWindowTitle(text) - def _handle_about_file_name(self, text: str): - self.text_edit_log.append(f"""

    File name: {text}

    """) + def _handle_about_file_name(self, text: str) -> None: + self.text_edit_log.append( + f"""

    File name: {text}

    """ + ) - def _handle_about_error(self, text: str): + def _handle_about_error(self, text: str) -> None: self.text_edit_log.append(f"""
    {text}
    """) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/download_file/simple.py b/qt__pyqt__pyside__pyqode/download_file/simple.py index baddf6f1e..9f62bc7b6 100644 --- a/qt__pyqt__pyside__pyqode/download_file/simple.py +++ b/qt__pyqt__pyside__pyqode/download_file/simple.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import os.path -from threading import Thread import traceback + +from threading import Thread from urllib.request import urlretrieve from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLineEdit, QFormLayout -def download_file(url: str, file_name: str): +def download_file(url: str, file_name: str) -> None: try: local_file_name, _ = urlretrieve(url, file_name) print(os.path.abspath(local_file_name)) @@ -21,23 +22,25 @@ def download_file(url: str, file_name: str): class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.line_edit_url = QLineEdit('https://codeload.github.com/gil9red/SimplePyScripts/zip/master') - self.line_edit_file_name = QLineEdit('SimplePyScripts.zip') + self.line_edit_url = QLineEdit( + "https://codeload.github.com/gil9red/SimplePyScripts/zip/master" + ) + self.line_edit_file_name = QLineEdit("SimplePyScripts.zip") - self.button_download = QPushButton('Download') + self.button_download = QPushButton("Download") self.button_download.clicked.connect(self.download) layout = QFormLayout() - layout.addRow('URL:', self.line_edit_url) - layout.addRow('File Name:', self.line_edit_file_name) + layout.addRow("URL:", self.line_edit_url) + layout.addRow("File Name:", self.line_edit_file_name) layout.addWidget(self.button_download) self.setLayout(layout) - def download(self): + def download(self) -> None: url = self.line_edit_url.text() file_name = self.line_edit_file_name.text() @@ -45,7 +48,7 @@ def download(self): thread.start() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/draw__ellipse_board/main.py b/qt__pyqt__pyside__pyqode/draw__ellipse_board/main.py index eaa0d385b..e565d4012 100644 --- a/qt__pyqt__pyside__pyqode/draw__ellipse_board/main.py +++ b/qt__pyqt__pyside__pyqode/draw__ellipse_board/main.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class CellWidget(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.ball_size = 100 - def paintEvent(self, event: Qt.QPaintEvent): + def paintEvent(self, event: Qt.QPaintEvent) -> None: painter = Qt.QPainter(self) painter.setRenderHint(Qt.QPainter.Antialiasing) painter.setPen(Qt.Qt.NoPen) @@ -30,10 +30,10 @@ def paintEvent(self, event: Qt.QPaintEvent): class Window(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('draw__ellipse_board') + self.setWindowTitle("draw__ellipse_board") self.cell_widget = CellWidget() @@ -41,7 +41,9 @@ def __init__(self): self.pb_ball_size.setRange(5, 1000) self.sl_ball_size = Qt.QSlider(Qt.Qt.Horizontal) - self.sl_ball_size.setRange(self.pb_ball_size.minimum(), self.pb_ball_size.maximum()) + self.sl_ball_size.setRange( + self.pb_ball_size.minimum(), self.pb_ball_size.maximum() + ) self.sl_ball_size.valueChanged.connect(self._set_ball_size) self.pb_ball_size.valueChanged.connect(self._set_ball_size) @@ -58,7 +60,7 @@ def __init__(self): self.setLayout(layout) - def _set_ball_size(self, value): + def _set_ball_size(self, value) -> None: self.sl_ball_size.setValue(value) self.pb_ball_size.setValue(value) @@ -66,7 +68,7 @@ def _set_ball_size(self, value): self.cell_widget.update() -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = Window() diff --git a/qt__pyqt__pyside__pyqode/draw__ellipse_board__chess_order__decrease/main.py b/qt__pyqt__pyside__pyqode/draw__ellipse_board__chess_order__decrease/main.py index 419e4c956..15fae80ce 100644 --- a/qt__pyqt__pyside__pyqode/draw__ellipse_board__chess_order__decrease/main.py +++ b/qt__pyqt__pyside__pyqode/draw__ellipse_board__chess_order__decrease/main.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class Window(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('draw__ellipse_board__chess_order__decrease') + self.setWindowTitle("draw__ellipse_board__chess_order__decrease") - def paintEvent(self, event: Qt.QPaintEvent): + def paintEvent(self, event: Qt.QPaintEvent) -> None: painter = Qt.QPainter(self) painter.setRenderHint(Qt.QPainter.Antialiasing) painter.setPen(Qt.Qt.NoPen) @@ -44,7 +44,7 @@ def paintEvent(self, event: Qt.QPaintEvent): y += h_indent -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = Window() diff --git a/qt__pyqt__pyside__pyqode/draw__ellipse_board__decrease/main.py b/qt__pyqt__pyside__pyqode/draw__ellipse_board__decrease/main.py index e0b3e9383..cf6110eb3 100644 --- a/qt__pyqt__pyside__pyqode/draw__ellipse_board__decrease/main.py +++ b/qt__pyqt__pyside__pyqode/draw__ellipse_board__decrease/main.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class Window(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('draw__ellipse_board__decrease') + self.setWindowTitle("draw__ellipse_board__decrease") - def paintEvent(self, event: Qt.QPaintEvent): + def paintEvent(self, event: Qt.QPaintEvent) -> None: painter = Qt.QPainter(self) painter.setRenderHint(Qt.QPainter.Antialiasing) painter.setPen(Qt.Qt.NoPen) @@ -41,7 +41,7 @@ def paintEvent(self, event: Qt.QPaintEvent): y += h_indent -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = Window() diff --git a/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/main.py b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/main.py index 1bcc66d49..f077dd9d8 100644 --- a/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/main.py +++ b/qt__pyqt__pyside__pyqode/draw_frame_with_color_info/main.py @@ -1,25 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from typing import Union import sys from pathlib import Path -sys.path.append('..') - -from Good_text_foreground_color_for_a_given_background_color import get_good_text_foreground_color - from PyQt5.QtGui import QGuiApplication, QPainter, QImage, QColor, QFont, QFontMetrics from PyQt5.QtCore import Qt, QByteArray, QBuffer, QIODevice +sys.path.append("..") +from Good_text_foreground_color_for_a_given_background_color import ( + get_good_text_foreground_color, +) + APP = QGuiApplication([]) SIZE = 300 RADIUS = 20 -FAMILY_FONT = 'Courier New' # Need monospaced +FAMILY_FONT = "Courier New" # Need monospaced def get_optimal_font(family_font: str, w, h, text: str) -> QFont: @@ -28,21 +28,23 @@ def get_optimal_font(family_font: str, w, h, text: str) -> QFont: metrics = QFontMetrics(font) # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/add91e36e1ee59b3956b9fafdcffc9f4ff10ed3d/qt__pyqt__pyside__pyqode/pyqt__QPainter__draw_table.py#L98 - factor = w / metrics.boundingRect(0, 0, int(w), int(h), Qt.AlignCenter, text).width() + factor = ( + w / metrics.boundingRect(0, 0, int(w), int(h), Qt.AlignCenter, text).width() + ) if factor < 1 or factor > 1.25: font.setPointSizeF(font.pointSizeF() * factor) return font -def draw_hex(painter: QPainter, size: int, color: QColor): +def draw_hex(painter: QPainter, size: int, color: QColor) -> None: r, g, b, _ = color.getRgb() - value = ''.join(f'{x:02X}' for x in [r, g, b]) + value = "".join(f"{x:02X}" for x in [r, g, b]) text_color = get_good_text_foreground_color(color) x, y, w_hex, h_hex = size * 0.05, size * 0.1, size * 0.9, size * 0.25 - font = get_optimal_font(FAMILY_FONT, w_hex, h_hex, text='FFFFFF') + font = get_optimal_font(FAMILY_FONT, w_hex, h_hex, text="FFFFFF") painter.save() @@ -54,14 +56,14 @@ def draw_hex(painter: QPainter, size: int, color: QColor): painter.restore() -def draw_rgb(painter: QPainter, size: int, color: QColor): +def draw_rgb(painter: QPainter, size: int, color: QColor) -> None: r, g, b, _ = color.getRgb() rgb = list(map(str, [r, g, b])) text_color, back_color = Qt.black, Qt.white w_rgb, h_rgb = size // 4, size // 6 - font = get_optimal_font(FAMILY_FONT, w_rgb, h_rgb, text='255') + font = get_optimal_font(FAMILY_FONT, w_rgb, h_rgb, text="255") font.setWeight(QFont.Bold) y = size - size // 4 @@ -85,7 +87,9 @@ def draw_rgb(painter: QPainter, size: int, color: QColor): painter.restore() -def get_frame_with_color_info(color: QColor, size=SIZE, rounded=True, as_bytes=False) -> Union[QImage, bytes]: +def get_frame_with_color_info( + color: QColor, size=SIZE, rounded=True, as_bytes=False +) -> QImage | bytes: image = QImage(size, size, QImage.Format_ARGB32) image.fill(Qt.transparent) @@ -95,7 +99,9 @@ def get_frame_with_color_info(color: QColor, size=SIZE, rounded=True, as_bytes=F painter.setBrush(color) if rounded: - painter.drawRoundedRect(0, 0, image.width(), image.height(), RADIUS, RADIUS, Qt.RelativeSize) + painter.drawRoundedRect( + 0, 0, image.width(), image.height(), RADIUS, RADIUS, Qt.RelativeSize + ) else: painter.drawRect(0, 0, image.width(), image.height()) @@ -114,15 +120,15 @@ def get_frame_with_color_info(color: QColor, size=SIZE, rounded=True, as_bytes=F return image -if __name__ == '__main__': - path = Path('.') / 'images' +if __name__ == "__main__": + path = Path(".") / "images" path.mkdir(parents=True, exist_ok=True) - for name in ['#007396', '#ff8c69', 'green', '#a000ff00', '#333']: + for name in ["#007396", "#ff8c69", "green", "#a000ff00", "#333"]: color = QColor(name) image = get_frame_with_color_info(color) - image.save(f'{path}/{name}.png') + image.save(f"{path}/{name}.png") - name = '#007396' + name = "#007396" image = get_frame_with_color_info(QColor(name), rounded=False) - image.save(f'images/no_rounded_{name}.png') + image.save(f"images/no_rounded_{name}.png") diff --git a/qt__pyqt__pyside__pyqode/draw_image_label__on_scroll_area/main.py b/qt__pyqt__pyside__pyqode/draw_image_label__on_scroll_area/main.py index d74febe31..7d29b5702 100644 --- a/qt__pyqt__pyside__pyqode/draw_image_label__on_scroll_area/main.py +++ b/qt__pyqt__pyside__pyqode/draw_image_label__on_scroll_area/main.py @@ -1,35 +1,43 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5.QtWidgets import QWidget, QApplication, QLabel, QHBoxLayout, QScrollArea, QMessageBox +import sys +import traceback + +from PyQt5.QtWidgets import ( + QWidget, + QApplication, + QLabel, + QHBoxLayout, + QScrollArea, + QMessageBox, +) from PyQt5.QtGui import QPainter, QColor, QPixmap, QPalette from PyQt5.QtCore import Qt, QEvent -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) print(text) - QMessageBox.critical(None, 'Error', text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions class Example(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.flag = False - self.image = QPixmap('image.jpg') + self.image = QPixmap("image.jpg") self.label = QLabel() self.label.setAlignment(Qt.AlignLeft) @@ -48,7 +56,7 @@ def __init__(self): # Рисовать будем на картинке self.painter = QPainter(self.image) - self.painter.setBrush(QColor('yellow')) + self.painter.setBrush(QColor("yellow")) def eventFilter(self, obj, e): # Если событие пришло от self.label @@ -69,12 +77,12 @@ def eventFilter(self, obj, e): # Стандартная обработка событий return super().eventFilter(obj, e) - def draw_ellipse(self, e): + def draw_ellipse(self, e) -> None: self.painter.drawEllipse(e.pos(), 20, 20) self.label.setPixmap(self.image) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Example() diff --git a/qt__pyqt__pyside__pyqode/draw_spiral.py b/qt__pyqt__pyside__pyqode/draw_spiral.py index 8b82f5812..38ab2b73c 100644 --- a/qt__pyqt__pyside__pyqode/draw_spiral.py +++ b/qt__pyqt__pyside__pyqode/draw_spiral.py @@ -1,19 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://doc.qt.io/qt-5/qtopengl-2dpainting-example.html from PyQt5.QtWidgets import QApplication, QWidget -from PyQt5.QtGui import QBrush, QFont, QPen, QPainter, QPaintEvent, QLinearGradient, QColor +from PyQt5.QtGui import ( + QBrush, + QFont, + QPen, + QPainter, + QPaintEvent, + QLinearGradient, + QColor, +) from PyQt5.QtCore import Qt, QPointF, QRectF, QTimer class Helper: - def __init__(self, is_reverse_rotation: bool, is_draw_all_spiral=False): + def __init__(self, is_reverse_rotation: bool, is_draw_all_spiral=False) -> None: super().__init__() self.is_reverse_rotation = is_reverse_rotation @@ -21,7 +29,7 @@ def __init__(self, is_reverse_rotation: bool, is_draw_all_spiral=False): gradient = QLinearGradient(QPointF(50, -20), QPointF(80, 20)) gradient.setColorAt(0.0, Qt.white) - gradient.setColorAt(1.0, QColor(0xa6, 0xce, 0x39)) + gradient.setColorAt(1.0, QColor(0xA6, 0xCE, 0x39)) self.background: QBrush = QBrush(QColor(64, 32, 64)) @@ -35,7 +43,7 @@ def __init__(self, is_reverse_rotation: bool, is_draw_all_spiral=False): self.textPen: QPen = QPen(Qt.white) - def _draw_spiral(self, painter: QPainter, elapsed: int, is_reverse_rotation=False): + def _draw_spiral(self, painter: QPainter, elapsed: int, is_reverse_rotation=False) -> None: r = elapsed / 1000.0 n = 30 @@ -49,11 +57,13 @@ def _draw_spiral(self, painter: QPainter, elapsed: int, is_reverse_rotation=Fals factor = (i + r) / n radius = 0 + 120.0 * factor circleRadius = 1 + factor * 20 - painter.drawEllipse(QRectF(radius, -circleRadius, circleRadius * 2, circleRadius * 2)) + painter.drawEllipse( + QRectF(radius, -circleRadius, circleRadius * 2, circleRadius * 2) + ) painter.restore() - def paint(self, painter: QPainter, event: QPaintEvent, elapsed: int): + def paint(self, painter: QPainter, event: QPaintEvent, elapsed: int) -> None: painter.fillRect(event.rect(), self.background) painter.translate(100, 100) @@ -63,7 +73,7 @@ def paint(self, painter: QPainter, event: QPaintEvent, elapsed: int): class Widget(QWidget): - def __init__(self, is_reverse_rotation: bool, is_draw_all_spiral=False): + def __init__(self, is_reverse_rotation: bool, is_draw_all_spiral=False) -> None: super().__init__() self.setFixedSize(200, 200) @@ -75,18 +85,18 @@ def __init__(self, is_reverse_rotation: bool, is_draw_all_spiral=False): self.timer.timeout.connect(self.animate) self.timer.start(50) - def animate(self): + def animate(self) -> None: self.elapsed = (self.elapsed + self.timer.interval()) % 1000 self.update() - def paintEvent(self, event: QPaintEvent): + def paintEvent(self, event: QPaintEvent) -> None: painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) self.helper.paint(painter, event, self.elapsed) painter.end() -if __name__ == '__main__': +if __name__ == "__main__": from PyQt5.QtWidgets import QLabel, QHBoxLayout app = QApplication([]) @@ -99,9 +109,9 @@ def paintEvent(self, event: QPaintEvent): layout = QHBoxLayout(mw) layout.addWidget(w1) - layout.addWidget(QLabel('+')) + layout.addWidget(QLabel("+")) layout.addWidget(w2) - layout.addWidget(QLabel('=')) + layout.addWidget(QLabel("=")) layout.addWidget(w3) mw.show() diff --git a/qt__pyqt__pyside__pyqode/draw_wave_SIN_and_COS.py b/qt__pyqt__pyside__pyqode/draw_wave_SIN_and_COS.py index 5a1a21fe6..fab8cc9d3 100644 --- a/qt__pyqt__pyside__pyqode/draw_wave_SIN_and_COS.py +++ b/qt__pyqt__pyside__pyqode/draw_wave_SIN_and_COS.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" import math @@ -12,7 +12,7 @@ class MainWindow(QWidget): - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) @@ -39,7 +39,7 @@ def paintEvent(self, event): painter.drawPoint(x, y) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/example_QStackedWidget/use_buttons.py b/qt__pyqt__pyside__pyqode/example_QStackedWidget/use_buttons.py index d59736de9..dae8ceada 100644 --- a/qt__pyqt__pyside__pyqode/example_QStackedWidget/use_buttons.py +++ b/qt__pyqt__pyside__pyqode/example_QStackedWidget/use_buttons.py @@ -1,29 +1,39 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5.QtWidgets import QApplication, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, QLabel, QStackedWidget +from PyQt5.QtWidgets import ( + QApplication, + QVBoxLayout, + QHBoxLayout, + QWidget, + QPushButton, + QLabel, + QStackedWidget, +) class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.stacked_widget = QStackedWidget() - self.stacked_widget.addWidget(QLabel('1234')) - self.stacked_widget.addWidget(QLabel('ABCD')) - self.stacked_widget.addWidget(QLabel('FOO_BAR')) + self.stacked_widget.addWidget(QLabel("1234")) + self.stacked_widget.addWidget(QLabel("ABCD")) + self.stacked_widget.addWidget(QLabel("FOO_BAR")) - self.button_123 = QPushButton('1234') + self.button_123 = QPushButton("1234") self.button_123.clicked.connect(lambda: self.stacked_widget.setCurrentIndex(0)) - self.button_ABCD = QPushButton('ABCD') + self.button_ABCD = QPushButton("ABCD") self.button_ABCD.clicked.connect(lambda: self.stacked_widget.setCurrentIndex(1)) - self.button_FOO_BAR = QPushButton('FOO_BAR') - self.button_FOO_BAR.clicked.connect(lambda: self.stacked_widget.setCurrentIndex(2)) + self.button_FOO_BAR = QPushButton("FOO_BAR") + self.button_FOO_BAR.clicked.connect( + lambda: self.stacked_widget.setCurrentIndex(2) + ) layout_buttons = QVBoxLayout() layout_buttons.addWidget(self.button_123) @@ -35,7 +45,7 @@ def __init__(self): main_layout.addWidget(self.stacked_widget) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/example_QStackedWidget/use_listwidget.py b/qt__pyqt__pyside__pyqode/example_QStackedWidget/use_listwidget.py index c5459772e..0fcdaee31 100644 --- a/qt__pyqt__pyside__pyqode/example_QStackedWidget/use_listwidget.py +++ b/qt__pyqt__pyside__pyqode/example_QStackedWidget/use_listwidget.py @@ -1,32 +1,41 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5.QtWidgets import QApplication, QHBoxLayout, QWidget, QLabel, QStackedWidget, QListWidget +from PyQt5.QtWidgets import ( + QApplication, + QHBoxLayout, + QWidget, + QLabel, + QStackedWidget, + QListWidget, +) class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.stacked_widget = QStackedWidget() - self.stacked_widget.addWidget(QLabel('1234')) - self.stacked_widget.addWidget(QLabel('ABCD')) - self.stacked_widget.addWidget(QLabel('FOO_BAR')) + self.stacked_widget.addWidget(QLabel("1234")) + self.stacked_widget.addWidget(QLabel("ABCD")) + self.stacked_widget.addWidget(QLabel("FOO_BAR")) self.control_list = QListWidget() - self.control_list.addItems(['1234', 'ABCD', 'FOO_BAR']) + self.control_list.addItems(["1234", "ABCD", "FOO_BAR"]) self.control_list.setFixedWidth(80) - self.control_list.clicked.connect(lambda index: self.stacked_widget.setCurrentIndex(index.row())) + self.control_list.clicked.connect( + lambda index: self.stacked_widget.setCurrentIndex(index.row()) + ) main_layout = QHBoxLayout(self) main_layout.addWidget(self.control_list) main_layout.addWidget(self.stacked_widget) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/extractor_html_using_QWebEnginePage.py b/qt__pyqt__pyside__pyqode/extractor_html_using_QWebEnginePage.py deleted file mode 100644 index 1affddc74..000000000 --- a/qt__pyqt__pyside__pyqode/extractor_html_using_QWebEnginePage.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -__author__ = 'ipetrash' - - -# Основа взята из http://stackoverflow.com/a/37755811/5909792 -def get_html(url, check_content_func=None): - from PyQt5.QtCore import QUrl - from PyQt5.QtWidgets import QApplication - from PyQt5.QtWebEngineWidgets import QWebEnginePage - - class ExtractorHtml: - def __init__(self, url): - self.html = None - - _app = QApplication([]) - self._page = QWebEnginePage() - self._page.load(QUrl(url)) - self._page.loadFinished.connect(self._load_finished_handler) - - # Ожидание загрузки страницы и получения его содержимого - # Этот цикл асинхронный код делает синхронным - while self.html is None: - _app.processEvents() - - _app.quit() - - self._page = None - - def _callable(self, data): - if check_content_func: - if check_content_func(data): - self.html = data - - else: - self.html = data - - def _load_finished_handler(self): - self._page.toHtml(self._callable) - - return ExtractorHtml(url).html - - -url = 'http://www.dns-shop.ru/' -html = get_html(url, lambda html: 'price-list-downloader' in html) -print(html) diff --git a/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/async.py b/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/async.py index e46bd6dcc..d51d5a8ff 100644 --- a/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/async.py +++ b/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/async.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -18,11 +18,11 @@ from PyQt5.QtCore import QThread, pyqtSignal -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) - print('Error: ', text) + print("Error: ", text) sys.excepthook = log_uncaught_exceptions @@ -31,13 +31,13 @@ def log_uncaught_exceptions(ex_cls, ex, tb): class MyThread(QThread): about_new_value = pyqtSignal(int) - def __init__(self): + def __init__(self) -> None: super().__init__() self.executed = False - def run(self): - print('start thread') + def run(self) -> None: + print("start thread") try: for i in range(1000000): @@ -51,7 +51,7 @@ def run(self): time.sleep(0.005) finally: - print('finish thread') + print("finish thread") def start(self, priority=QThread.InheritPriority): self.executed = True @@ -65,7 +65,7 @@ def exit(self, returnCode=0): class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.lw = QListWidget() @@ -79,19 +79,19 @@ def __init__(self): self.thread = MyThread() self.thread.about_new_value.connect(lambda x: self.append_item(str(x))) - def append_item(self, item): + def append_item(self, item) -> None: self.lw.addItem(item) self.lw.scrollToBottom() - def fill(self): + def fill(self) -> None: self.thread.start() - def closeEvent(self, event): + def closeEvent(self, event) -> None: # После закрытия окна приложение не завершится пока список работает sys.exit() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/sync.py b/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/sync.py index 99e694b01..ef922ccf3 100644 --- a/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/sync.py +++ b/qt__pyqt__pyside__pyqode/filling the large list without delay Qt/sync.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -16,11 +16,11 @@ from PyQt5.QtWidgets import QWidget, QListWidget, QApplication, QVBoxLayout -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) - print('Error: ', text) + print("Error: ", text) sys.excepthook = log_uncaught_exceptions @@ -33,7 +33,7 @@ def generator_large_list(): class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.lw = QListWidget() @@ -44,17 +44,17 @@ def __init__(self): self.setLayout(layout) - def fill(self): + def fill(self) -> None: for i in generator_large_list(): self.lw.addItem(str(i)) self.lw.scrollToBottom() - def closeEvent(self, event): + def closeEvent(self, event) -> None: # После закрытия окна приложение не завершится пока список работает sys.exit() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/full_black_screen.py b/qt__pyqt__pyside__pyqode/full_black_screen.py index fd6715d2d..ac42ed33a 100644 --- a/qt__pyqt__pyside__pyqode/full_black_screen.py +++ b/qt__pyqt__pyside__pyqode/full_black_screen.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QWidget @@ -10,7 +10,7 @@ class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setMouseTracking(True) @@ -18,22 +18,22 @@ def __init__(self): self._enabledClose = False QTimer.singleShot(5000, self._setEnabledClose) - def _setEnabledClose(self): + def _setEnabledClose(self) -> None: self._enabledClose = True - def mouseMoveEvent(self, event): + def mouseMoveEvent(self, event) -> None: if self._enabledClose: self.close() - def mousePressEvent(self, event): + def mousePressEvent(self, event) -> None: if self._enabledClose: self.close() - def keyPressEvent(self, event): + def keyPressEvent(self, event) -> None: if self._enabledClose: self.close() - def paintEvent(self, event): + def paintEvent(self, event) -> None: color = Qt.black painter = QPainter(self) @@ -43,7 +43,7 @@ def paintEvent(self, event): painter.drawRect(self.rect()) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/full_black_screen_close_manual.py b/qt__pyqt__pyside__pyqode/full_black_screen_close_manual.py index e27651d13..251972571 100644 --- a/qt__pyqt__pyside__pyqode/full_black_screen_close_manual.py +++ b/qt__pyqt__pyside__pyqode/full_black_screen_close_manual.py @@ -1,40 +1,82 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5.QtWidgets import QApplication, QWidget, QToolButton -from PyQt5.QtGui import QResizeEvent +from PyQt5.QtWidgets import ( + QApplication, + QWidget, + QVBoxLayout, + QToolButton, + QSizePolicy, + QGraphicsOpacityEffect, +) +from PyQt5.QtGui import QKeyEvent +from PyQt5.QtCore import QPropertyAnimation, QSequentialAnimationGroup, Qt class MainWindow(QWidget): - def __init__(self): + def __init__(self, background_color: str = "black", text_color: str = "white") -> None: super().__init__() - self.setStyleSheet(""" - MainWindow { - background-color: black; - } + self.setStyleSheet( + f""" + MainWindow {{ + background-color: {background_color}; + }} - QToolButton { - color : white; - background-color: black; + QToolButton {{ + color: {text_color}; + background-color: {background_color}; border: 1px solid darkgray; - } - """) + }} + """ + ) - self.button_close = QToolButton(self) - self.button_close.setText('ЗАКРЫТЬ') + self.button_close = QToolButton() + self.button_close.setText("ЗАКРЫТЬ") + self.button_close.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + self.button_close.setFixedHeight(60) self.button_close.clicked.connect(self.close) - def resizeEvent(self, event: QResizeEvent): - size = event.size() - size = min(size.width(), size.height()) // 4 - self.button_close.resize(size, size) + self._add_animations() + main_layout = QVBoxLayout(self) + main_layout.addStretch() + main_layout.addWidget(self.button_close) -if __name__ == '__main__': + def _add_animations(self) -> None: + self.button_close.setGraphicsEffect(QGraphicsOpacityEffect()) + + animation_object = self.button_close.graphicsEffect() + animation_property = b"opacity" + duration = 5000 + start_value = 1.0 + end_value = 0.0 + + animation1 = QPropertyAnimation(animation_object, animation_property) + animation1.setDuration(duration) + animation1.setStartValue(start_value) + animation1.setEndValue(end_value) + + animation2 = QPropertyAnimation(animation_object, animation_property) + animation2.setDuration(duration) + animation2.setStartValue(end_value) + animation2.setEndValue(start_value) + + self.animation_group = QSequentialAnimationGroup() + self.animation_group.setLoopCount(-1) + self.animation_group.addAnimation(animation1) + self.animation_group.addAnimation(animation2) + self.animation_group.start() + + def keyPressEvent(self, event: QKeyEvent) -> None: + if event.key() in [Qt.Key_Escape, Qt.Key_Return, Qt.Key_Enter, Qt.Key_Space]: + self.close() + + +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/full_black_screen_close_manual_with_animations.py b/qt__pyqt__pyside__pyqode/full_black_screen_close_manual_with_animations.py new file mode 100644 index 000000000..dfc1dd7cf --- /dev/null +++ b/qt__pyqt__pyside__pyqode/full_black_screen_close_manual_with_animations.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import enum +from random import randint, choice + +from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtGui import QPainter, QPaintEvent +from PyQt5.QtCore import QTimer + +from full_black_screen_close_manual import MainWindow as BaseMainWindow +from pyq5__simple_balls__with_part_transparent_body import ( + Ball as BaseBall, + get_random_vector, + get_random_color, +) + + +class Animation: + def __init__(self, owner: QWidget = None) -> None: + self.owner = owner + + def set_owner(self, owner: QWidget) -> None: + self.owner = owner + + def prepare(self) -> None: + pass + + def tick(self) -> None: + pass + + def draw(self, painter: QPainter) -> None: + pass + + +class DirectionEnum(enum.IntEnum): + UP = 1 + DOWN = -1 + + +class Ball(BaseBall): + def __init__(self, x, y, r, v_x, v_y, color) -> None: + super().__init__(x, y, r, v_x, v_y, color) + + self.animation_alpha_direction = choice(list(DirectionEnum)) + + +class AnimationBalls(Animation): + def __init__( + self, + owner: QWidget = None, + number_balls: int = 50, + min_ball_alpha_color: int = 35, # Минимальное значение может быть 0 + max_ball_alpha_color: int = 160, # Максимальное значение может быть 255 + animation_ball_alpha_color: bool = True, + ) -> None: + super().__init__(owner) + + self.balls: list[Ball] = [] + self.number_balls = number_balls + self.min_ball_alpha_color = min_ball_alpha_color + self.max_ball_alpha_color = max_ball_alpha_color + self.animation_ball_alpha_color = animation_ball_alpha_color + + def prepare(self) -> None: + for _ in range(self.number_balls): + self.append_random_ball() + + def append_random_ball(self) -> None: + x = self.owner.width() // 2 + randint( + -self.owner.width() // 3, self.owner.width() // 3 + ) + y = self.owner.height() // 2 + randint( + -self.owner.height() // 3, self.owner.height() // 3 + ) + v_x, v_y = get_random_vector() + r, g, b = get_random_color() + a = randint(self.min_ball_alpha_color, self.max_ball_alpha_color) + + ball = Ball( + x, + y, + r=randint(50, 70), + v_x=v_x, + v_y=v_y, + color=(r, g, b, a), + ) + self.balls.append(ball) + + def tick(self) -> None: + for ball in self.balls: + ball.update() + + # Условия отскакивания шарика от левого и правого края + if ball.left <= 0 or ball.right >= self.owner.width(): + ball.v_x = -ball.v_x + + # Условия отскакивания шарика верхнего и нижнего края + if ball.top <= 0 or ball.bottom >= self.owner.height(): + ball.v_y = -ball.v_y + + if self.animation_ball_alpha_color: + alpha = ball.color[3] + ball.animation_alpha_direction + + if alpha >= self.max_ball_alpha_color: + ball.animation_alpha_direction = DirectionEnum.DOWN + + if alpha <= self.min_ball_alpha_color: + ball.animation_alpha_direction = DirectionEnum.UP + + ball.color = ball.color[:3] + (alpha,) + + def draw(self, painter: QPainter) -> None: + for ball in self.balls: + ball.draw(painter) + + +class MainWindow(BaseMainWindow): + def __init__(self, animations: list[Animation] = None) -> None: + super().__init__() + + self.animations: list[Animation] = animations + + # Таймер для обновления анимаций + self.timer = QTimer() + self.timer.setInterval(1000 // 20) + self.timer.timeout.connect(self.tick) + + def tick(self) -> None: + if not self.animations: + return + + for animation in self.animations: + animation.tick() + + self.update() + + def start_animations(self) -> None: + if not self.animations: + return + + for animation in self.animations: + animation.set_owner(self) + animation.prepare() + + self.timer.start() + + def paintEvent(self, event: QPaintEvent) -> None: + super().paintEvent(event) + + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + for animation in self.animations: + animation.draw(painter) + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow( + animations=[ + AnimationBalls(), + ] + ) + mw.resize(600, 600) + mw.showFullScreen() + mw.start_animations() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/full_white_screen_close_manual.py b/qt__pyqt__pyside__pyqode/full_white_screen_close_manual.py new file mode 100644 index 000000000..b03d41b33 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/full_white_screen_close_manual.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import QApplication + +from full_black_screen_close_manual import MainWindow + + +if __name__ == "__main__": + app = QApplication([]) + + mw = MainWindow(background_color="white", text_color="black") + mw.resize(600, 600) + mw.showFullScreen() + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/get_html_from_Qt5_QWebEnginePage.py b/qt__pyqt__pyside__pyqode/get_html_from_Qt5_QWebEnginePage.py index 4d234ff76..b88e0790b 100644 --- a/qt__pyqt__pyside__pyqode/get_html_from_Qt5_QWebEnginePage.py +++ b/qt__pyqt__pyside__pyqode/get_html_from_Qt5_QWebEnginePage.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + + +from PyQt5.QtCore import QUrl +from PyQt5.QtWidgets import QApplication +from PyQt5.QtWebEngineWidgets import QWebEnginePage # Основа взята из http://stackoverflow.com/a/37755811/5909792 def get_html(url): - from PyQt5.QtCore import QUrl - from PyQt5.QtWidgets import QApplication - from PyQt5.QtWebEngineWidgets import QWebEnginePage - class ExtractorHtml: - def __init__(self, url): + def __init__(self, url) -> None: _app = QApplication([]) self._page = QWebEnginePage() self._page.loadFinished.connect(self._load_finished_handler) @@ -35,10 +36,10 @@ def __init__(self, url): # Чтобы избежать падений скрипта self._page = None - def _callable(self, data): + def _callable(self, data) -> None: self.html = data - def _load_finished_handler(self, _): + def _load_finished_handler(self, _) -> None: self._counter_finished += 1 if self._counter_finished == 2: @@ -47,6 +48,6 @@ def _load_finished_handler(self, _): return ExtractorHtml(url).html -if __name__ == '__main__': - url = 'http://gama-gama.ru/search/?searchField=titan' +if __name__ == "__main__": + url = "http://gama-gama.ru/search/?searchField=titan" print(get_html(url)) diff --git a/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/gui_with_timer.py b/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/gui_with_timer.py index 76915cfe6..46db7da3a 100644 --- a/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/gui_with_timer.py +++ b/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/gui_with_timer.py @@ -1,22 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import win32gui + from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout from PyQt5.QtGui import QPixmap from PyQt5.QtCore import QTimer from PyQt5.QtWinExtras import QtWin -import win32gui class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.DEFAULT_MOUSE_PIXMAP = QPixmap('default_mouse.png').scaledToWidth(16) + self.DEFAULT_MOUSE_PIXMAP = QPixmap("default_mouse.png").scaledToWidth(16) self.label = QLabel() @@ -28,7 +29,7 @@ def __init__(self): self.timer.setInterval(100) self.timer.start() - def _on_tick(self): + def _on_tick(self) -> None: # flags, hcursor, (x, y) = _, hcursor, _ = win32gui.GetCursorInfo() @@ -42,7 +43,7 @@ def _on_tick(self): self.label.setPixmap(pixmap) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/main.py b/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/main.py index 6664295b9..cdd1fb928 100644 --- a/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/main.py +++ b/qt__pyqt__pyside__pyqode/get_mouse_cursor_to_pixmap__win10/main.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import win32gui + from PyQt5.QtWidgets import QApplication from PyQt5.QtWinExtras import QtWin -import win32gui - app = QApplication([]) @@ -21,4 +21,4 @@ pixmap = QtWin.fromHBITMAP(hbmColor.handle, QtWin.HBitmapPremultipliedAlpha) print(pixmap.size(), pixmap) -pixmap.save('img.png') +pixmap.save("img.png") diff --git a/qt__pyqt__pyside__pyqode/hello_world__QWebEngineView.py b/qt__pyqt__pyside__pyqode/hello_world__QWebEngineView.py index f7a3f7096..f4628243c 100644 --- a/qt__pyqt__pyside__pyqode/hello_world__QWebEngineView.py +++ b/qt__pyqt__pyside__pyqode/hello_world__QWebEngineView.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtCore import QUrl @@ -14,7 +14,7 @@ view = QWebEngineView() view.show() -url = 'https://www.google.com/search?q=hello world' +url = "https://www.google.com/search?q=hello world" view.load(QUrl(url)) app.exec() diff --git a/qt__pyqt__pyside__pyqode/highlight previous tab Qt.py b/qt__pyqt__pyside__pyqode/highlight previous tab Qt.py index 5b7f50463..09e1b1718 100644 --- a/qt__pyqt__pyside__pyqode/highlight previous tab Qt.py +++ b/qt__pyqt__pyside__pyqode/highlight previous tab Qt.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" """ @@ -14,25 +14,25 @@ class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.prev_tab_index = -1 self.curr_tab_index = 0 self.tab_widget = QTabWidget() - self.tab_widget.addTab(QLabel('1'), '1') - self.tab_widget.addTab(QLabel('2'), '2') - self.tab_widget.addTab(QLabel('3'), '3') - self.tab_widget.addTab(QLabel('4'), '4') - self.tab_widget.addTab(QLabel('5'), '5') + self.tab_widget.addTab(QLabel("1"), "1") + self.tab_widget.addTab(QLabel("2"), "2") + self.tab_widget.addTab(QLabel("3"), "3") + self.tab_widget.addTab(QLabel("4"), "4") + self.tab_widget.addTab(QLabel("5"), "5") self.tab_widget.setCurrentIndex(self.curr_tab_index) self.tab_widget.currentChanged.connect(self.current_tab_changed) self.setCentralWidget(self.tab_widget) - def current_tab_changed(self, i): + def current_tab_changed(self, i) -> None: # Запоминание индексов предыдущей и текущей вкладки. # При переключении вкладок: # У предыдущей убирается выделение @@ -45,12 +45,12 @@ def current_tab_changed(self, i): self.prev_tab_index = self.curr_tab_index prev_text = self.tab_widget.tabText(self.prev_tab_index) - self.tab_widget.setTabText(self.prev_tab_index, '_' + prev_text + '_') + self.tab_widget.setTabText(self.prev_tab_index, "_" + prev_text + "_") self.curr_tab_index = i -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/hook_exceptions_in_Qt.py b/qt__pyqt__pyside__pyqode/hook_exceptions_in_Qt.py index 26f99c2a6..17305c9de 100644 --- a/qt__pyqt__pyside__pyqode/hook_exceptions_in_Qt.py +++ b/qt__pyqt__pyside__pyqode/hook_exceptions_in_Qt.py @@ -1,32 +1,32 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import sys +import traceback + from PyQt5.QtWidgets import * # Для отлова всех исключений, которые в слотах Qt могут "затеряться" и привести к тихому падению -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) - import traceback - text += ''.join(traceback.format_tb(tb)) - - print('Error: ', text) - QMessageBox.critical(None, 'Error', text) + print("Error: ", text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions app = QApplication([]) w = QLineEdit() -w.textChanged.conneect(lambda x: 1 + '1') +w.textChanged.conneect(lambda x: 1 + "1") # Emit textChanged signal -w.setText('!!!') +w.setText("!!!") diff --git a/qt__pyqt__pyside__pyqode/image on whole cell in QTableWidget/main.py b/qt__pyqt__pyside__pyqode/image on whole cell in QTableWidget/main.py index f8c5cf186..5a8ee92f8 100644 --- a/qt__pyqt__pyside__pyqode/image on whole cell in QTableWidget/main.py +++ b/qt__pyqt__pyside__pyqode/image on whole cell in QTableWidget/main.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import sys +import traceback + try: from PyQt5.QtWidgets import * from PyQt5.QtGui import * @@ -14,17 +17,15 @@ # Для отлова всех исключений, которые в слотах Qt могут "затеряться" и привести к тихому падению -def log_uncaught_exceptions(ex_cls, ex, tb): - text = '{}: {}:\n'.format(ex_cls.__name__, ex) - import traceback - text += ''.join(traceback.format_tb(tb)) +def log_uncaught_exceptions(ex_cls, ex, tb) -> None: + text = f"{ex_cls.__name__}: {ex}:\n" + text += "".join(traceback.format_tb(tb)) - print('Error: ', text) - QMessageBox.critical(None, 'Error', text) + print("Error: ", text) + QMessageBox.critical(None, "Error", text) sys.exit(1) -import sys sys.excepthook = log_uncaught_exceptions @@ -36,7 +37,7 @@ def create_item(img): class MyDelegate_1(QStyledItemDelegate): - def paint(self, painter, option, index): + def paint(self, painter, option, index) -> None: img = index.model().data(index, Qt.DecorationRole) if img is None: super().paint(painter, option, index) @@ -68,7 +69,7 @@ def paint(self, painter, option, index): class MyDelegate_2(QStyledItemDelegate): - def paint(self, painter, option, index): + def paint(self, painter, option, index) -> None: img = index.model().data(index, Qt.DecorationRole) if img is None: super().paint(painter, option, index) @@ -106,7 +107,7 @@ def paint(self, painter, option, index): # super().paint(painter, option, index) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) table = QTableWidget() @@ -114,15 +115,15 @@ def paint(self, painter, option, index): table.show() table.resize(400, 200) - headers = ['Normal', 'Delegate v1', 'Delegate v1 (without img)', 'Delegate v2'] + headers = ["Normal", "Delegate v1", "Delegate v1 (without img)", "Delegate v2"] table.setColumnCount(len(headers)) table.setHorizontalHeaderLabels(headers) table.setRowCount(3) table.verticalHeader().hide() - pix_1 = QPixmap('favicon_google.png') - pix_2 = QPixmap('favicon_prog_org.png') - pix_3 = QPixmap('favicon_google_tr.png') + pix_1 = QPixmap("favicon_google.png") + pix_2 = QPixmap("favicon_prog_org.png") + pix_3 = QPixmap("favicon_google_tr.png") for col in range(table.columnCount()): if col == 2: diff --git a/qt__pyqt__pyside__pyqode/images__QListWidget__QTableWidget_QStyledItemDelegate.py b/qt__pyqt__pyside__pyqode/images__QListWidget__QTableWidget_QStyledItemDelegate.py index 88281062c..27ba3cf83 100644 --- a/qt__pyqt__pyside__pyqode/images__QListWidget__QTableWidget_QStyledItemDelegate.py +++ b/qt__pyqt__pyside__pyqode/images__QListWidget__QTableWidget_QStyledItemDelegate.py @@ -1,25 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -import datetime as DT +import datetime as dt from glob import glob from pathlib import Path from PyQt5.QtWidgets import ( - QApplication, QListWidget, QListWidgetItem, QTableWidget, QTableWidgetItem, QStyledItemDelegate, QStyle + QApplication, + QListWidget, + QListWidgetItem, + QTableWidget, + QTableWidgetItem, + QStyledItemDelegate, + QStyle, ) from PyQt5.QtGui import QIcon, QPixmap from PyQt5.QtCore import QSize, Qt class ImageDelegate(QStyledItemDelegate): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) - def paint(self, painter, option, index): + def paint(self, painter, option, index) -> None: pixmap = index.data(Qt.DecorationRole) if pixmap: # pixmap = pixmap.scaled( @@ -40,14 +46,15 @@ def paint(self, painter, option, index): ICON_HEIGHT = 128 -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) - file_names = glob(r'D:\все фотки\**\*.jpg', recursive=True) - print(f'Files: {len(file_names)}') + file_names = glob(r"D:\все фотки\**\*.jpg", recursive=True) + print(f"Files: {len(file_names)}") list_widget = QListWidget() - list_widget.setStyleSheet(""" + list_widget.setStyleSheet( + """ QListWidget::item { border: 1px solid gray; } @@ -55,7 +62,8 @@ def paint(self, painter, option, index): color: black; background-color: #0087BD; } - """) + """ + ) list_widget.setMovement(QListWidget.Static) list_widget.setDragEnabled(False) list_widget.setDragDropMode(QListWidget.NoDragDrop) @@ -70,7 +78,7 @@ def paint(self, painter, option, index): list_widget.resize(800, 600) list_widget.show() - headers = ['IMAGE', 'FILE NAME', 'DIRECTORY'] + headers = ["IMAGE", "FILE NAME", "DIRECTORY"] table_widget = QTableWidget() table_widget.setColumnCount(len(headers)) table_widget.setHorizontalHeaderLabels(headers) @@ -82,12 +90,16 @@ def paint(self, painter, option, index): table_widget.resize(800, 600) table_widget.show() - start_time = DT.datetime.now() + start_time = dt.datetime.now() row = 0 for i, file_name in enumerate(file_names, 1): - list_widget.setWindowTitle(f'{i} / {len(file_names)} ({i / len(file_names):.0%})') - table_widget.setWindowTitle(f'{i} / {len(file_names)} ({i / len(file_names):.0%})') + list_widget.setWindowTitle( + f"{i} / {len(file_names)} ({i / len(file_names):.0%})" + ) + table_widget.setWindowTitle( + f"{i} / {len(file_names)} ({i / len(file_names):.0%})" + ) pixmap = QPixmap(file_name) if pixmap.isNull(): @@ -98,10 +110,7 @@ def paint(self, painter, option, index): ) base_file_name = Path(file_name).name - item = QListWidgetItem( - QIcon(pixmap), - base_file_name - ) + item = QListWidgetItem(QIcon(pixmap), base_file_name) item.setTextAlignment(Qt.AlignHCenter | Qt.AlignBottom) item.setSizeHint(QSize(ICON_WIDTH, ICON_HEIGHT + 20)) list_widget.addItem(item) @@ -119,10 +128,14 @@ def paint(self, painter, option, index): QApplication.processEvents() list_widget.setWindowTitle( - list_widget.windowTitle() + '. Elapsed: ' + str(DT.datetime.now() - start_time).split('.')[0] + list_widget.windowTitle() + + ". Elapsed: " + + str(dt.datetime.now() - start_time).split(".")[0] ) table_widget.setWindowTitle( - table_widget.windowTitle() + '. Elapsed: ' + str(DT.datetime.now() - start_time).split('.')[0] + table_widget.windowTitle() + + ". Elapsed: " + + str(dt.datetime.now() - start_time).split(".")[0] ) app.exec() diff --git a/qt__pyqt__pyside__pyqode/images_widget__api.vk photos.search requests qt.py b/qt__pyqt__pyside__pyqode/images_widget__api.vk photos.search requests qt.py index 29ee8612e..68c79ca65 100644 --- a/qt__pyqt__pyside__pyqode/images_widget__api.vk photos.search requests qt.py +++ b/qt__pyqt__pyside__pyqode/images_widget__api.vk photos.search requests qt.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import requests + try: from PyQt5.QtWidgets import * from PyQt5.QtGui import * @@ -20,13 +22,12 @@ def get_img_urls(): - import requests rs = requests.get("https://api.vk.com/method/photos.search?v=5.64") img_urls = rs.json()["response"]["items"] - return [url['photo_130'] for url in img_urls] + return [url["photo_130"] for url in img_urls] -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = QScrollArea() @@ -40,9 +41,9 @@ def get_img_urls(): columns = 4 i, j = 0, 0 - mw.setWindowTitle('Loading...') + mw.setWindowTitle("Loading...") - print('Images:', len(img_urls)) + print("Images:", len(img_urls)) url_by_label = dict() @@ -65,7 +66,6 @@ def get_img_urls(): mw.setWidget(w) for n, (url, label) in enumerate(url_by_label.items()): - import requests rs = requests.get(url) image = QImage.fromData(rs.content) @@ -73,9 +73,9 @@ def get_img_urls(): label.setPixmap(pixmap) - mw.setWindowTitle('{}/{} Loading...'.format(n, len(url_by_label))) + mw.setWindowTitle(f"{n}/{len(url_by_label)} Loading...") QApplication.processEvents() - mw.setWindowTitle('Ok!') + mw.setWindowTitle("Ok!") app.exec() diff --git a/qt__pyqt__pyside__pyqode/install_translator.py b/qt__pyqt__pyside__pyqode/install_translator.py new file mode 100644 index 000000000..7ac4ed192 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/install_translator.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +from PyQt5.QtWidgets import QApplication, QMessageBox +from PyQt5.QtCore import QTranslator, QLibraryInfo + + +def question() -> None: + QMessageBox.question( + None, + "TITLE", + "TEXT", + QMessageBox.Yes | QMessageBox.No | QMessageBox.Abort, + ) + + +app = QApplication([]) + +# NOTE: EN +question() + +translator = QTranslator() +if translator.load("qtbase_ru", directory=QLibraryInfo.location(QLibraryInfo.TranslationsPath)): + app.installTranslator(translator) + +# NOTE: RU +question() diff --git a/qt__pyqt__pyside__pyqode/label_framed.py b/qt__pyqt__pyside__pyqode/label_framed.py index c0caf5682..3f249fa00 100644 --- a/qt__pyqt__pyside__pyqode/label_framed.py +++ b/qt__pyqt__pyside__pyqode/label_framed.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class Widget(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() layout = Qt.QVBoxLayout() - layout.addWidget(self.create_label_framed('Hello')) - layout.addWidget(self.create_label_framed('World')) - layout.addWidget(self.create_label_framed('999')) + layout.addWidget(self.create_label_framed("Hello")) + layout.addWidget(self.create_label_framed("World")) + layout.addWidget(self.create_label_framed("999")) self.setLayout(layout) @@ -26,7 +26,7 @@ def create_label_framed(text): return label -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/latin_letters_similar_to_cyrillic/main.py b/qt__pyqt__pyside__pyqode/latin_letters_similar_to_cyrillic/main.py index 51630af6a..47019a532 100644 --- a/qt__pyqt__pyside__pyqode/latin_letters_similar_to_cyrillic/main.py +++ b/qt__pyqt__pyside__pyqode/latin_letters_similar_to_cyrillic/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://ru.stackoverflow.com/questions/839750/ @@ -14,26 +14,26 @@ class MainWindow(Qt.QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - loadUi('main.ui', self) + loadUi("main.ui", self) self.btn_solve.clicked.connect(self.solve) self.btn_clear.clicked.connect(self.textEdit_words.clear) - self.textEdit_text.setPlainText('СВЕТА РОЕТ РОВ, ВОВКА СЕЕТ ОВЁС') + self.textEdit_text.setPlainText("СВЕТА РОЕТ РОВ, ВОВКА СЕЕТ ОВЁС") - def solve(self): + def solve(self) -> None: # Исходный текст text = self.textEdit_text.toPlainText() # Буквы, совпадающие с английскими - for word in re.findall(r'\b[АВСТРХОНКМУЕ]+\b', text): + for word in re.findall(r"\b[АВСТРХОНКМУЕ]+\b", text): self.textEdit_words.append(word) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/layout_append_line__horizontal_vertical.py b/qt__pyqt__pyside__pyqode/layout_append_line__horizontal_vertical.py index b0ad05da2..05ba470bc 100644 --- a/qt__pyqt__pyside__pyqode/layout_append_line__horizontal_vertical.py +++ b/qt__pyqt__pyside__pyqode/layout_append_line__horizontal_vertical.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QFrame class HorizontalLineWidget(QFrame): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setFrameShape(QFrame.HLine) @@ -17,7 +17,7 @@ def __init__(self): class VerticalLineWidget(QFrame): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setFrameShape(QFrame.VLine) @@ -25,18 +25,26 @@ def __init__(self): self.setLineWidth(1) -if __name__ == '__main__': - from PyQt5.QtWidgets import QApplication, QFormLayout, QHBoxLayout, QLineEdit, QCheckBox, QWidget, QLabel +if __name__ == "__main__": + from PyQt5.QtWidgets import ( + QApplication, + QFormLayout, + QHBoxLayout, + QLineEdit, + QCheckBox, + QWidget, + QLabel, + ) from PyQt5.QtCore import Qt app = QApplication([]) layout = QFormLayout() - layout.addRow('First', QLineEdit()) - layout.addRow('Last', QLineEdit()) + layout.addRow("First", QLineEdit()) + layout.addRow("Last", QLineEdit()) layout.addRow(HorizontalLineWidget()) - layout.addRow('Phone', QLineEdit()) + layout.addRow("Phone", QLineEdit()) layout.addRow(HorizontalLineWidget()) h_layout = QHBoxLayout() @@ -56,7 +64,7 @@ def __init__(self): layout.addItem(h_layout) - layout.addRow('Ok?', QCheckBox()) + layout.addRow("Ok?", QCheckBox()) mw = QWidget() mw.setLayout(layout) diff --git a/qt__pyqt__pyside__pyqode/layout_column_resizer/README.md b/qt__pyqt__pyside__pyqode/layout_column_resizer/README.md new file mode 100644 index 000000000..d79a879b3 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/layout_column_resizer/README.md @@ -0,0 +1,3 @@ +# Screenshot + +![screenshot.png](screenshot.png) diff --git a/qt__pyqt__pyside__pyqode/layout_column_resizer/column_resizer.py b/qt__pyqt__pyside__pyqode/layout_column_resizer/column_resizer.py new file mode 100644 index 000000000..f3a07c642 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/layout_column_resizer/column_resizer.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/agateau/columnresizer + + +from dataclasses import dataclass + +from PyQt5.QtCore import QEvent, QTimer, QSize, QRect, Qt, QObject, qCritical +from PyQt5.QtWidgets import ( + QFormLayout, + QGridLayout, + QWidget, + QWidgetItem, + QLayout, + QLayoutItem, +) + + +class FormLayoutWidgetItem(QWidgetItem): + def __init__( + self, + widget: QWidget, + formLayout: QFormLayout, + itemRole: QFormLayout.ItemRole, + ) -> None: + super().__init__(widget) + + self.m_width: int = -1 + self.m_formLayout: QFormLayout = formLayout + self.m_itemRole: QFormLayout.ItemRole = itemRole + + def sizeHint(self) -> QSize: + size: QSize = super().sizeHint() + if self.m_width != -1: + size.setWidth(self.m_width) + + return size + + def minimumSize(self) -> QSize: + size: QSize = super().minimumSize() + if self.m_width != -1: + size.setWidth(self.m_width) + + return size + + def maximumSize(self) -> QSize: + size: QSize = super().maximumSize() + if self.m_width != -1: + size.setWidth(self.m_width) + + return size + + def setWidth(self, width: int) -> None: + if width != self.m_width: + self.m_width = width + self.invalidate() + + def setGeometry(self, _rect: QRect) -> None: + rect: QRect = _rect + width = self.widget().sizeHint().width() + if ( + self.m_itemRole == QFormLayout.LabelRole + and self.m_formLayout.labelAlignment() & Qt.AlignRight + ): + rect.setLeft(rect.right() - width) + + super().setGeometry(rect) + + def formLayout(self) -> QFormLayout: + return self.m_formLayout + + +@dataclass +class GridColumnInfo: + layout: QGridLayout + column: int + + +class ColumnResizerPrivate: + def __init__(self, q_ptr: "ColumnResizer") -> None: + self.q: ColumnResizer = q_ptr + + self.m_widgets: list[QWidget] = [] + self.m_wrWidgetItemList: list[FormLayoutWidgetItem] = [] + self.m_gridColumnInfoList: list[GridColumnInfo] = [] + + self.m_updateTimer: QTimer = QTimer(self.q) + self.m_updateTimer.setSingleShot(True) + self.m_updateTimer.setInterval(0) + self.m_updateTimer.timeout.connect(self.q.updateWidth) + + def scheduleWidthUpdate(self) -> None: + self.m_updateTimer.start() + + +class ColumnResizer(QObject): + def __init__(self, parent: QObject) -> None: + super().__init__(parent) + + self.d = ColumnResizerPrivate(self) + + def addWidget(self, widget: QWidget) -> None: + self.d.m_widgets.append(widget) + widget.installEventFilter(self) + self.d.scheduleWidthUpdate() + + # NOTE: Here the logic is changed relative to the original + def updateWidth(self) -> None: + width: int = 0 + x: int = 0 + for widget in self.d.m_widgets: + x = max(widget.pos().x(), x) + width = max(widget.sizeHint().width(), width) + + width += x + + for item in self.d.m_wrWidgetItemList: + item.setWidth(width - item.widget().pos().x()) + item.formLayout().update() + + for info in self.d.m_gridColumnInfoList: + info.layout.setColumnMinimumWidth(info.column, width) + + def eventFilter(self, _: QObject, event: QEvent) -> bool: + if event.type() == QEvent.Resize: + self.d.scheduleWidthUpdate() + + return False + + def addWidgetsFromLayout(self, layout: QLayout, column: int) -> None: + assert column >= 0 + + if isinstance(layout, QGridLayout): + self.addWidgetsFromGridLayout(layout, column) + elif isinstance(layout, QFormLayout): + if column > QFormLayout.ItemRole.SpanningRole: + qCritical( + f"column should not be more than {QFormLayout.ItemRole.SpanningRole} for QFormLayout" + ) + return + + role: QFormLayout.ItemRole = QFormLayout.ItemRole(column) + self.addWidgetsFromFormLayout(layout, role) + else: + qCritical(f"Don't know how to handle layout {layout}") + + def addWidgetsFromGridLayout(self, layout: QGridLayout, column: int) -> None: + for row in range(layout.rowCount()): + item: QLayoutItem = layout.itemAtPosition(row, column) + if not item: + continue + + widget: QWidget = item.widget() + if not widget: + continue + + self.addWidget(widget) + + self.d.m_gridColumnInfoList.append(GridColumnInfo(layout, column)) + + def addWidgetsFromFormLayout(self, layout: QFormLayout, role: QFormLayout.ItemRole) -> None: + for row in range(layout.rowCount()): + item: QLayoutItem = layout.itemAt(row, role) + if not item: + continue + + widget: QWidget = item.widget() + if not widget: + continue + + layout.removeItem(item) + newItem: FormLayoutWidgetItem = FormLayoutWidgetItem(widget, layout, role) + layout.setItem(row, role, newItem) + self.addWidget(widget) + self.d.m_wrWidgetItemList.append(newItem) diff --git a/qt__pyqt__pyside__pyqode/layout_column_resizer/column_resizer_pyqt6.py b/qt__pyqt__pyside__pyqode/layout_column_resizer/column_resizer_pyqt6.py new file mode 100644 index 000000000..f3e1756ec --- /dev/null +++ b/qt__pyqt__pyside__pyqode/layout_column_resizer/column_resizer_pyqt6.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +# SOURCE: https://github.com/agateau/columnresizer + + +from dataclasses import dataclass + +from PyQt6.QtCore import QEvent, QTimer, QSize, QRect, Qt, QObject, qCritical +from PyQt6.QtWidgets import ( + QFormLayout, + QGridLayout, + QWidget, + QWidgetItem, + QLayout, + QLayoutItem, +) + + +class FormLayoutWidgetItem(QWidgetItem): + def __init__( + self, + widget: QWidget, + formLayout: QFormLayout, + itemRole: QFormLayout.ItemRole, + ): + super().__init__(widget) + + self.m_width: int = -1 + self.m_formLayout: QFormLayout = formLayout + self.m_itemRole: QFormLayout.ItemRole = itemRole + + def sizeHint(self) -> QSize: + size: QSize = super().sizeHint() + if self.m_width != -1: + size.setWidth(self.m_width) + + return size + + def minimumSize(self) -> QSize: + size: QSize = super().minimumSize() + if self.m_width != -1: + size.setWidth(self.m_width) + + return size + + def maximumSize(self) -> QSize: + size: QSize = super().maximumSize() + if self.m_width != -1: + size.setWidth(self.m_width) + + return size + + def setWidth(self, width: int): + if width != self.m_width: + self.m_width = width + self.invalidate() + + def setGeometry(self, _rect: QRect): + rect: QRect = _rect + width = self.widget().sizeHint().width() + if ( + self.m_itemRole == QFormLayout.ItemRole.LabelRole + and self.m_formLayout.labelAlignment() & Qt.AlignmentFlag.AlignRight + ): + rect.setLeft(rect.right() - width) + + super().setGeometry(rect) + + def formLayout(self) -> QFormLayout: + return self.m_formLayout + + +@dataclass +class GridColumnInfo: + layout: QGridLayout + column: int + + +class ColumnResizerPrivate: + def __init__(self, q_ptr: "ColumnResizer"): + self.q: ColumnResizer = q_ptr + + self.m_widgets: list[QWidget] = [] + self.m_wrWidgetItemList: list[FormLayoutWidgetItem] = [] + self.m_gridColumnInfoList: list[GridColumnInfo] = [] + + self.m_updateTimer: QTimer = QTimer(self.q) + self.m_updateTimer.setSingleShot(True) + self.m_updateTimer.setInterval(0) + self.m_updateTimer.timeout.connect(self.q.updateWidth) + + def scheduleWidthUpdate(self): + self.m_updateTimer.start() + + +class ColumnResizer(QObject): + def __init__(self, parent: QObject): + super().__init__(parent) + + self.d = ColumnResizerPrivate(self) + + def addWidget(self, widget: QWidget): + self.d.m_widgets.append(widget) + widget.installEventFilter(self) + self.d.scheduleWidthUpdate() + + # NOTE: Here the logic is changed relative to the original + def updateWidth(self): + width: int = 0 + x: int = 0 + for widget in self.d.m_widgets: + x = max(widget.pos().x(), x) + width = max(widget.sizeHint().width(), width) + + width += x + + for item in self.d.m_wrWidgetItemList: + item.setWidth(width - item.widget().pos().x()) + item.formLayout().update() + + for info in self.d.m_gridColumnInfoList: + info.layout.setColumnMinimumWidth(info.column, width) + + def eventFilter(self, _: QObject, event: QEvent) -> bool: + if event.type() == QEvent.Type.Resize: + self.d.scheduleWidthUpdate() + + return False + + def addWidgetsFromLayout(self, layout: QLayout, column: int): + assert column >= 0 + + if isinstance(layout, QGridLayout): + self.addWidgetsFromGridLayout(layout, column) + elif isinstance(layout, QFormLayout): + if column > QFormLayout.ItemRole.SpanningRole.value: + qCritical( + f"column should not be more than {QFormLayout.ItemRole.SpanningRole.value} for QFormLayout" + ) + return + + role: QFormLayout.ItemRole = QFormLayout.ItemRole(column) + self.addWidgetsFromFormLayout(layout, role) + else: + qCritical(f"Don't know how to handle layout {layout}") + + def addWidgetsFromGridLayout(self, layout: QGridLayout, column: int): + for row in range(layout.rowCount()): + item: QLayoutItem = layout.itemAtPosition(row, column) + if not item: + continue + + widget: QWidget = item.widget() + if not widget: + continue + + self.addWidget(widget) + + self.d.m_gridColumnInfoList.append(GridColumnInfo(layout, column)) + + def addWidgetsFromFormLayout(self, layout: QFormLayout, role: QFormLayout.ItemRole): + for row in range(layout.rowCount()): + item: QLayoutItem = layout.itemAt(row, role) + if not item: + continue + + widget: QWidget = item.widget() + if not widget: + continue + + layout.removeItem(item) + newItem: FormLayoutWidgetItem = FormLayoutWidgetItem(widget, layout, role) + layout.setItem(row, role, newItem) + self.addWidget(widget) + self.d.m_wrWidgetItemList.append(newItem) diff --git a/qt__pyqt__pyside__pyqode/layout_column_resizer/example.py b/qt__pyqt__pyside__pyqode/layout_column_resizer/example.py new file mode 100644 index 000000000..f22f01688 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/layout_column_resizer/example.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +import platform + +from datetime import datetime +from pathlib import Path + +from PyQt5.QtWidgets import ( + QDialog, + QWidget, + QFormLayout, + QLabel, + QDialogButtonBox, + QGroupBox, + QVBoxLayout, + QScrollArea, +) + +from column_resizer import ColumnResizer + + +DIR: Path = Path(__file__).resolve().parent + +PROGRAM_NAME: str = DIR.name + + +class About(QDialog): + def __init__(self, title: str, use_column_resizer: bool) -> None: + super().__init__() + + self.setWindowTitle(title) + + self._started: datetime = datetime.now() + + gb_python = QGroupBox("Python:") + gb_python_layout = QFormLayout(gb_python) + gb_python_layout.addRow( + "Version:", + QLabel(sys.version), + ) + gb_python_layout.addRow( + "Implementation:", + QLabel(platform.python_implementation()), + ) + gb_python_layout.addRow("Executable:", QLabel(sys.executable)) + + button_box = QDialogButtonBox(QDialogButtonBox.Ok) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + + fields_layout = QFormLayout() + fields_layout.addRow( + "Version:", + QLabel("1.0.0"), + ) + fields_layout.addRow( + "Directory:", + QLabel(str(DIR)), + ) + fields_layout.addRow( + "Argv:", + QLabel(" ".join(sys.argv[1:])), + ) + fields_layout.addRow(gb_python) + fields_layout.addRow( + "Platform:", + QLabel(platform.platform()), + ) + + fields_widget = QWidget() + fields_layout.setContentsMargins(0, 0, 0, 0) + fields_widget.setLayout(fields_layout) + + scroll_area = QScrollArea() + scroll_area.setWidget(fields_widget) + scroll_area.setWidgetResizable(True) + scroll_area.setFrameStyle(QScrollArea.NoFrame) + + main_layout = QVBoxLayout(self) + main_layout.addWidget(QLabel(f"

    {PROGRAM_NAME}

    ")) + main_layout.addWidget(scroll_area) + main_layout.addWidget(button_box) + + if use_column_resizer: + resizer = ColumnResizer(self) + resizer.addWidgetsFromLayout(fields_layout, 0) + resizer.addWidgetsFromLayout(gb_python_layout, 0) + + self.resize(500, 300) + + +if __name__ == "__main__": + from PyQt5.QtWidgets import QApplication + + app = QApplication(sys.argv) + + w1 = About(f"use_column_resizer=False", use_column_resizer=False) + w1.show() + + w2 = About(f"use_column_resizer=True", use_column_resizer=True) + w2.show() + w2.move(w1.x() + w1.width(), w1.y()) + + sys.exit(app.exec()) diff --git a/qt__pyqt__pyside__pyqode/layout_column_resizer/example_pyqt6.py b/qt__pyqt__pyside__pyqode/layout_column_resizer/example_pyqt6.py new file mode 100644 index 000000000..7c8997c32 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/layout_column_resizer/example_pyqt6.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +import sys +import platform + +from datetime import datetime +from pathlib import Path + +from PyQt6.QtWidgets import ( + QDialog, + QWidget, + QFormLayout, + QLabel, + QDialogButtonBox, + QGroupBox, + QVBoxLayout, + QScrollArea, +) + +from column_resizer_pyqt6 import ColumnResizer + + +DIR: Path = Path(__file__).resolve().parent + +PROGRAM_NAME: str = DIR.name + + +class About(QDialog): + def __init__(self, title: str, use_column_resizer: bool) -> None: + super().__init__() + + self.setWindowTitle(title) + + self._started: datetime = datetime.now() + + gb_python = QGroupBox("Python:") + gb_python_layout = QFormLayout(gb_python) + gb_python_layout.addRow( + "Version:", + QLabel(sys.version), + ) + gb_python_layout.addRow( + "Implementation:", + QLabel(platform.python_implementation()), + ) + gb_python_layout.addRow("Executable:", QLabel(sys.executable)) + + button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + + fields_layout = QFormLayout() + fields_layout.addRow( + "Version:", + QLabel("1.0.0"), + ) + fields_layout.addRow( + "Directory:", + QLabel(str(DIR)), + ) + fields_layout.addRow( + "Argv:", + QLabel(" ".join(sys.argv[1:])), + ) + fields_layout.addRow(gb_python) + fields_layout.addRow( + "Platform:", + QLabel(platform.platform()), + ) + + fields_widget = QWidget() + fields_layout.setContentsMargins(0, 0, 0, 0) + fields_widget.setLayout(fields_layout) + + scroll_area = QScrollArea() + scroll_area.setWidget(fields_widget) + scroll_area.setWidgetResizable(True) + scroll_area.setFrameStyle(QScrollArea.Shape.NoFrame) + + main_layout = QVBoxLayout(self) + main_layout.addWidget(QLabel(f"

    {PROGRAM_NAME}

    ")) + main_layout.addWidget(scroll_area) + main_layout.addWidget(button_box) + + if use_column_resizer: + resizer = ColumnResizer(self) + resizer.addWidgetsFromLayout(fields_layout, 0) + resizer.addWidgetsFromLayout(gb_python_layout, 0) + + self.resize(500, 300) + + +if __name__ == "__main__": + from PyQt6.QtWidgets import QApplication + + app = QApplication(sys.argv) + + w1 = About(f"use_column_resizer=False", use_column_resizer=False) + w1.show() + + w2 = About(f"use_column_resizer=True", use_column_resizer=True) + w2.show() + w2.move(w1.x() + w1.width(), w1.y()) + + sys.exit(app.exec()) diff --git a/qt__pyqt__pyside__pyqode/layout_column_resizer/screenshot.png b/qt__pyqt__pyside__pyqode/layout_column_resizer/screenshot.png new file mode 100644 index 000000000..b47a455c6 Binary files /dev/null and b/qt__pyqt__pyside__pyqode/layout_column_resizer/screenshot.png differ diff --git a/qt__pyqt__pyside__pyqode/lazy__qtwidgets_itemviews_fetchmore_example__QAbstractListModel.py b/qt__pyqt__pyside__pyqode/lazy__qtwidgets_itemviews_fetchmore_example__QAbstractListModel.py index 3c6d1d147..3a01b50c5 100644 --- a/qt__pyqt__pyside__pyqode/lazy__qtwidgets_itemviews_fetchmore_example__QAbstractListModel.py +++ b/qt__pyqt__pyside__pyqode/lazy__qtwidgets_itemviews_fetchmore_example__QAbstractListModel.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" # SOURCE: https://doc.qt.io/qt-5.12/qtwidgets-itemviews-fetchmore-example.html @@ -10,16 +10,30 @@ from pathlib import Path from PyQt5.QtWidgets import ( - QApplication, QMainWindow, QListView, QGridLayout, QLabel, QWidget, - QLineEdit, QTextBrowser, QSizePolicy + QApplication, + QMainWindow, + QListView, + QGridLayout, + QLabel, + QWidget, + QLineEdit, + QTextBrowser, + QSizePolicy, +) +from PyQt5.QtCore import ( + QAbstractListModel, + QModelIndex, + Qt, + pyqtSignal, + QVariant, + QLibraryInfo, ) -from PyQt5.QtCore import QAbstractListModel, QModelIndex, Qt, pyqtSignal, QVariant, QLibraryInfo class FileListModel(QAbstractListModel): numberPopulated = pyqtSignal(int) - def __init__(self, batch_size=50, parent=None): + def __init__(self, batch_size=50, parent=None) -> None: super().__init__(parent) self.fileList = [] @@ -51,13 +65,15 @@ def data(self, index: QModelIndex, role=Qt.DisplayRole) -> QVariant: def canFetchMore(self, parent: QModelIndex = None) -> bool: return self.fileCount < len(self.fileList) - def fetchMore(self, parent: QModelIndex = None): + def fetchMore(self, parent: QModelIndex = None) -> None: remainder = len(self.fileList) - self.fileCount itemsToFetch = min(self.batch_size, remainder) if itemsToFetch <= 0: return - self.beginInsertRows(QModelIndex(), self.fileCount, self.fileCount + itemsToFetch - 1) + self.beginInsertRows( + QModelIndex(), self.fileCount, self.fileCount + itemsToFetch - 1 + ) self.fileCount += itemsToFetch @@ -65,7 +81,7 @@ def fetchMore(self, parent: QModelIndex = None): self.numberPopulated.emit(itemsToFetch) - def setDirPath(self, path: str): + def setDirPath(self, path: str) -> None: self.beginResetModel() # Recursive @@ -76,7 +92,7 @@ def setDirPath(self, path: str): class MainWindow(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowTitle("Fetch More Example") @@ -110,11 +126,11 @@ def __init__(self): self.setCentralWidget(QWidget()) self.centralWidget().setLayout(layout) - def updateLog(self, number: int): + def updateLog(self, number: int) -> None: self.logViewer.append(f"{number} items added.") -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/load_url__async_thread__QNetworkAccessManager.py b/qt__pyqt__pyside__pyqode/load_url__async_thread__QNetworkAccessManager.py index cda726932..fbb1e2770 100644 --- a/qt__pyqt__pyside__pyqode/load_url__async_thread__QNetworkAccessManager.py +++ b/qt__pyqt__pyside__pyqode/load_url__async_thread__QNetworkAccessManager.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class MainWindow(Qt.QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.button = Qt.QPushButton('Load url!') + self.button = Qt.QPushButton("Load url!") self.button.clicked.connect(self.on_clicked) self.manager = Qt.QNetworkAccessManager(self) @@ -19,19 +19,19 @@ def __init__(self): self.setCentralWidget(self.button) - def on_reply_finished(self, reply): - self.setWindowTitle('After load: {}'.format(reply)) + def on_reply_finished(self, reply) -> None: + self.setWindowTitle(f"After load: {reply}") - def on_clicked(self): - url = 'https://github.com/gil9red/SimplePyScripts' + def on_clicked(self) -> None: + url = "https://github.com/gil9red/SimplePyScripts" - self.setWindowTitle('Before load') + self.setWindowTitle("Before load") # Отправляем запрос self.manager.get(Qt.QNetworkRequest(Qt.QUrl(url))) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/load_url__async_thread__QThread.py b/qt__pyqt__pyside__pyqode/load_url__async_thread__QThread.py index a2dd23952..a5b9efb71 100644 --- a/qt__pyqt__pyside__pyqode/load_url__async_thread__QThread.py +++ b/qt__pyqt__pyside__pyqode/load_url__async_thread__QThread.py @@ -1,43 +1,42 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import requests from PyQt5 import Qt class LoadUrlThread(Qt.QThread): load_finished = Qt.pyqtSignal(object) - def __init__(self, url): + def __init__(self, url) -> None: super().__init__() self.url = url - def run(self): - import requests + def run(self) -> None: rs = requests.get(self.url) - self.load_finished.emit(rs) class MainWindow(Qt.QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.button = Qt.QPushButton('Load url!') + self.button = Qt.QPushButton("Load url!") self.button.clicked.connect(self.on_clicked) self.setCentralWidget(self.button) - def on_finished_load_url(self, rs): - self.setWindowTitle('After load: {}'.format(rs)) + def on_finished_load_url(self, rs) -> None: + self.setWindowTitle(f"After load: {rs}") - def on_clicked(self): - url = 'https://github.com/gil9red/SimplePyScripts' + def on_clicked(self) -> None: + url = "https://github.com/gil9red/SimplePyScripts" - self.setWindowTitle('Before load') + self.setWindowTitle("Before load") self.thread = LoadUrlThread(url) self.thread.load_finished.connect(self.on_finished_load_url) @@ -45,7 +44,7 @@ def on_clicked(self): self.thread.start() -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/model_with_iterator__QAbstractListModel/_more_examples.py b/qt__pyqt__pyside__pyqode/model_with_iterator__QAbstractListModel/_more_examples.py index 415e8d00a..9535232e3 100644 --- a/qt__pyqt__pyside__pyqode/model_with_iterator__QAbstractListModel/_more_examples.py +++ b/qt__pyqt__pyside__pyqode/model_with_iterator__QAbstractListModel/_more_examples.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from string import printable @@ -20,22 +20,30 @@ def get_infinity_generator(): class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() main_layout = QHBoxLayout(self) - main_layout.addWidget(self._add_view_with_it('infinity_generator', get_infinity_generator())) - main_layout.addWidget(self._add_view_with_it('range(1_000_000)', range(1_000_000))) - main_layout.addWidget(self._add_view_with_it('list of pow2', [str(i ** i) for i in range(1_000)])) - main_layout.addWidget(self._add_view_with_it('str', printable * 100)) - main_layout.addWidget(self._add_view_with_it('dict', dict.fromkeys(dir(self)))) + main_layout.addWidget( + self._add_view_with_it("infinity_generator", get_infinity_generator()) + ) + main_layout.addWidget( + self._add_view_with_it("range(1_000_000)", range(1_000_000)) + ) + main_layout.addWidget( + self._add_view_with_it("list of pow2", [str(i**i) for i in range(1_000)]) + ) + main_layout.addWidget(self._add_view_with_it("str", printable * 100)) + main_layout.addWidget(self._add_view_with_it("dict", dict.fromkeys(dir(self)))) def _add_view_with_it(self, title: str, it: Iterator) -> QWidget: group_box = QGroupBox() - group_box.setTitle('1111') + group_box.setTitle("1111") model = IteratorListModel(it=it) - model.rowsInserted.connect(lambda: group_box.setTitle(f'[{title}] rows: {model.rowCount()}')) + model.rowsInserted.connect( + lambda: group_box.setTitle(f"[{title}] rows: {model.rowCount()}") + ) view = QListView() view.setModel(model) @@ -46,7 +54,7 @@ def _add_view_with_it(self, title: str, it: Iterator) -> QWidget: return group_box -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/model_with_iterator__QAbstractListModel/iterator_list_model.py b/qt__pyqt__pyside__pyqode/model_with_iterator__QAbstractListModel/iterator_list_model.py index ec65245aa..f09e6fbc0 100644 --- a/qt__pyqt__pyside__pyqode/model_with_iterator__QAbstractListModel/iterator_list_model.py +++ b/qt__pyqt__pyside__pyqode/model_with_iterator__QAbstractListModel/iterator_list_model.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from typing import Iterator @@ -11,7 +11,7 @@ class IteratorListModel(QAbstractListModel): - def __init__(self, it: Iterator, prefetch=100, parent=None): + def __init__(self, it: Iterator, prefetch=100, parent=None) -> None: super().__init__(parent) self._at_end = False @@ -22,7 +22,7 @@ def __init__(self, it: Iterator, prefetch=100, parent=None): def canFetchMore(self, parent: QModelIndex = None) -> bool: return not self._at_end - def fetchMore(self, parent: QModelIndex = None): + def fetchMore(self, parent: QModelIndex = None) -> None: if self._at_end: return @@ -58,11 +58,11 @@ def rowCount(self, parent: QModelIndex = None) -> int: return len(self._items) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) model = IteratorListModel(it=range(1_000_000)) - model.rowsInserted.connect(lambda: mw.setWindowTitle(f'Rows: {model.rowCount()}')) + model.rowsInserted.connect(lambda: mw.setWindowTitle(f"Rows: {model.rowCount()}")) mw = QListView() mw.setModel(model) diff --git a/qt__pyqt__pyside__pyqode/move_window_on_center__QDesktopWidget.py b/qt__pyqt__pyside__pyqode/move_window_on_center__QDesktopWidget.py index 7a7d02ca0..a92a23f4b 100644 --- a/qt__pyqt__pyside__pyqode/move_window_on_center__QDesktopWidget.py +++ b/qt__pyqt__pyside__pyqode/move_window_on_center__QDesktopWidget.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication, QWidget @@ -12,7 +12,8 @@ mw = QWidget() rect = mw.frameGeometry() -center = app.desktop().availableGeometry().center() # This is where QDesktopWidget is used +# This is where QDesktopWidget is used +center = app.desktop().availableGeometry().center() rect.moveCenter(center) mw.move(rect.topLeft()) diff --git a/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__create_child_process__with__Tkinter_and_Qt.py b/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__create_child_process__with__Tkinter_and_Qt.py index dfcf28be6..614e5b281 100644 --- a/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__create_child_process__with__Tkinter_and_Qt.py +++ b/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__create_child_process__with__Tkinter_and_Qt.py @@ -1,54 +1,56 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from multiprocessing import Process +import tkinter as tk +from PyQt5.Qt import QApplication, Qt, QLabel -def go_qt(name): - from PyQt5.Qt import QApplication, Qt, QLabel + +def go_qt(name) -> None: app = QApplication([]) mw = QLabel() mw.setAlignment(Qt.AlignCenter) mw.setMinimumSize(150, 50) - mw.setText('Hello, ' + name) + mw.setText("Hello, " + name) mw.show() app.exec() -def go_tk(name): - import tkinter as tk +def go_tk(name) -> None: app = tk.Tk() app.minsize(150, 50) - mw = tk.Label(app, text='Hello, ' + name) - mw.pack(fill='both', expand=True) + mw = tk.Label(app, text="Hello, " + name) + mw.pack(fill="both", expand=True) app.mainloop() -def create_qt(): - p = Process(target=go_qt, args=('Qt',)) +def create_qt() -> None: + p = Process(target=go_qt, args=("Qt",)) p.start() -def create_tk(): - p = Process(target=go_tk, args=('Tk',)) +def create_tk() -> None: + p = Process(target=go_tk, args=("Tk",)) p.start() -if __name__ == '__main__': - from PyQt5.Qt import QApplication, QPushButton, QWidget, QVBoxLayout +if __name__ == "__main__": + from PyQt5.Qt import QPushButton, QWidget, QVBoxLayout + app = QApplication([]) - button_qt = QPushButton('Create Qt') + button_qt = QPushButton("Create Qt") button_qt.clicked.connect(create_qt) - button_tk = QPushButton('Create Tk') + button_tk = QPushButton("Create Tk") button_tk.clicked.connect(create_tk) layout = QVBoxLayout() diff --git a/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process.py b/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process.py index 9d70cf0ae..e216d0e33 100644 --- a/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process.py +++ b/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process.py @@ -1,24 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -def go(name): - from PyQt5.Qt import QApplication, Qt, QLabel +from PyQt5.Qt import QApplication, Qt, QLabel + + +def go(name) -> None: app = QApplication([]) mw = QLabel() mw.setAlignment(Qt.AlignCenter) mw.setMinimumSize(150, 50) - mw.setText('Hello, ' + name) + mw.setText("Hello, " + name) mw.show() app.exec() -if __name__ == '__main__': +if __name__ == "__main__": from multiprocessing import Process - p = Process(target=go, args=('bob',)) + + p = Process(target=go, args=("bob",)) p.start() p.join() diff --git a/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process_list__Pool_map.py b/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process_list__Pool_map.py index 7a17434fa..c27cae3ae 100644 --- a/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process_list__Pool_map.py +++ b/qt__pyqt__pyside__pyqode/multiprocessing__QApplication__in_other_process_list__Pool_map.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -if __name__ == '__main__': +if __name__ == "__main__": from multiprocessing import Pool from multiprocessing__QApplication__in_other_process import go with Pool() as p: - p.map(go, ['Alice', 'Bob', 'World']) + p.map(go, ["Alice", "Bob", "World"]) diff --git a/qt__pyqt__pyside__pyqode/not_working__youtube_Maximum_durations_QWebView__PyQt4.py b/qt__pyqt__pyside__pyqode/not_working__youtube_Maximum_durations_QWebView__PyQt4.py new file mode 100644 index 000000000..412adc556 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/not_working__youtube_Maximum_durations_QWebView__PyQt4.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +""" +Maximum durations. + +""" + + +import sys + +from bs4 import BeautifulSoup + +from PyQt4.QtGui import * +from PyQt4.QtCore import * +from PyQt4.QtWebKit import * + + +QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, True) + +video_time_list = set() +video_link_list = set() + + +def get_total_seconds(time_str): + """ + Return seconds from strings: "hh:mm:ss", "mm:ss" and "ss". + + """ + + total_seconds = 0 + + time_part = list(map(int, time_str.split(":"))) + time_part_number = len(time_part) + + if time_part_number == 1: + total_seconds += time_part[0] + + elif time_part_number == 2: + total_seconds += time_part[0] * 60 + time_part[1] + + elif time_part_number == 3: + total_seconds += time_part[0] * 3600 + time_part[1] * 60 + time_part[2] + + return total_seconds + + +def load_finished_handler(ok) -> None: + if not ok: + return + + doc = view.page().mainFrame().documentElement() + + def get_video_time_list(): + html = doc.toOuterXml() + root = BeautifulSoup(html, "lxml") + + return [title.text for title in root.select(".video-time")] + + def get_video_link_list(): + html = doc.toOuterXml() + root = BeautifulSoup(html, "lxml") + + return {a["href"] for a in root.select(".yt-uix-tile-link")} + + # Get video time and append to list + global video_time_list + video_time_list.update(get_video_time_list()) + + # Get video link and append to list + global video_link_list + video_link_list.update(get_video_link_list()) + + # Every 5 seconds + timer = QTimer() + timer.setInterval(5000) + + def timeout_handler() -> None: + doc = view.page().mainFrame().documentElement() + button = doc.findFirst(".load-more-button") + + # If button "More" visible + is_load_more = not button.isNull() + if is_load_more: + # Get video time and append global list + global video_time_list + video_time_list.update(get_video_time_list()) + + # Get video link and append to list + global video_link_list + video_link_list.update(get_video_link_list()) + + else: + timer.stop() + return + + # Click on button "More" + print("More") + button.evaluateJavaScript("this.click()") + + timer.timeout.connect(timeout_handler) + timer.start() + + # Wait loading all video + while timer.isActive(): + QApplication.instance().processEvents() + + print() + print("Total:") + print(f" Videos: {len(video_link_list)}, {video_link_list}") + print(f" Durations: {len(video_time_list)}, {video_time_list}") + + # Find maximum duration + max_total_seconds = 0 + max_time_str = None + + for time_str in video_time_list: + total_seconds = get_total_seconds(time_str) + if total_seconds > max_total_seconds: + max_total_seconds = total_seconds + max_time_str = time_str + + print(f" Max durations: {max_time_str}") + + sys.exit() + + +if __name__ == "__main__": + app = QApplication([]) + + view = QWebView() + # view.show() + + view.loadFinished.connect(load_finished_handler) + + url = QUrl("https://www.youtube.com/channel/UCWvMAvbk23gFzZ3BRUfACRA/videos") + view.load(url) + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/not_working__youtube_summary_durations_all_video_QWebView__PyQt4.py b/qt__pyqt__pyside__pyqode/not_working__youtube_summary_durations_all_video_QWebView__PyQt4.py new file mode 100644 index 000000000..8302eb998 --- /dev/null +++ b/qt__pyqt__pyside__pyqode/not_working__youtube_summary_durations_all_video_QWebView__PyQt4.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +__author__ = "ipetrash" + + +""" +Summary durations all video. + +""" + + +import sys + +from bs4 import BeautifulSoup + +from PyQt4.QtGui import * +from PyQt4.QtCore import * +from PyQt4.QtWebKit import * + + +QWebSettings.globalSettings().setAttribute(QWebSettings.DeveloperExtrasEnabled, True) + +url_video_by_time_dict = dict() + + +def get_total_seconds(time_str): + """ + Return seconds from strings: "hh:mm:ss", "mm:ss" and "ss". + + """ + + total_seconds = 0 + + time_part = list(map(int, time_str.split(":"))) + time_part_number = len(time_part) + + if time_part_number == 1: + total_seconds += time_part[0] + + elif time_part_number == 2: + total_seconds += time_part[0] * 60 + time_part[1] + + elif time_part_number == 3: + total_seconds += time_part[0] * 3600 + time_part[1] * 60 + time_part[2] + + return total_seconds + + +def load_finished_handler(ok) -> None: + if not ok: + return + + doc = view.page().mainFrame().documentElement() + + def update_url_video_by_time_dict() -> None: + global url_video_by_time_dict + + html = doc.toOuterXml() + root = BeautifulSoup(html, "lxml") + + for item in root.select(".channels-content-item"): + url = item.select_one(".yt-uix-tile-link")["href"] + time_str = item.select_one(".video-time").text + + url_video_by_time_dict[url] = time_str + + # Fill dict + update_url_video_by_time_dict() + + # Every 5 seconds + timer = QTimer() + timer.setInterval(5000) + + def timeout_handler() -> None: + doc = view.page().mainFrame().documentElement() + button = doc.findFirst(".load-more-button") + + # If button "More" visible + is_load_more = not button.isNull() + if is_load_more: + # Fill dict + update_url_video_by_time_dict() + + else: + timer.stop() + return + + # Click on button "More" + print("More") + button.evaluateJavaScript("this.click()") + + timer.timeout.connect(timeout_handler) + timer.start() + + # Wait loading all video + while timer.isActive(): + QApplication.instance().processEvents() + + print() + print("Total:") + print(f" Videos: {len(url_video_by_time_dict)}") + print(f" Durations: {list(url_video_by_time_dict.values())}") + + # Summary duration + sum_seconds = 0 + + for time_str in url_video_by_time_dict.values(): + sum_seconds += get_total_seconds(time_str) + + mm, ss = divmod(sum_seconds, 60) + hh, mm = divmod(mm, 60) + sum_time_str = "%d:%02d:%02d" % (hh, mm, ss) + + print(f" Summary duration (secs): {sum_seconds}") + print(f" Summary duration: {sum_time_str}") + + sys.exit() + + +if __name__ == "__main__": + app = QApplication([]) + + view = QWebView() + # view.show() + + view.loadFinished.connect(load_finished_handler) + + url = QUrl("https://www.youtube.com/channel/UCWvMAvbk23gFzZ3BRUfACRA/videos") + view.load(url) + + app.exec() diff --git a/qt__pyqt__pyside__pyqode/play_mp3__pyqt5__qmediaplayer/play.py b/qt__pyqt__pyside__pyqode/play_mp3__pyqt5__qmediaplayer/play.py index 85a92914b..518bc7668 100644 --- a/qt__pyqt__pyside__pyqode/play_mp3__pyqt5__qmediaplayer/play.py +++ b/qt__pyqt__pyside__pyqode/play_mp3__pyqt5__qmediaplayer/play.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt -def _on_media_status_changed(status): +def _on_media_status_changed(status) -> None: if status == Qt.QMediaPlayer.EndOfMedia: Qt.QCoreApplication.instance().quit() @@ -15,7 +15,7 @@ def _on_media_status_changed(status): app = Qt.QCoreApplication([]) player = Qt.QMediaPlayer() -file_name = Qt.QUrl.fromLocalFile('example.mp3') +file_name = Qt.QUrl.fromLocalFile("example.mp3") player.setMedia(Qt.QMediaContent(file_name)) player.mediaStatusChanged.connect(_on_media_status_changed) player.play() diff --git a/qt__pyqt__pyside__pyqode/player_youtube_on_custom_page__QWebViewEngine.py b/qt__pyqt__pyside__pyqode/player_youtube_on_custom_page__QWebViewEngine.py index 11f1a81be..8578f4927 100644 --- a/qt__pyqt__pyside__pyqode/player_youtube_on_custom_page__QWebViewEngine.py +++ b/qt__pyqt__pyside__pyqode/player_youtube_on_custom_page__QWebViewEngine.py @@ -1,25 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication from PyQt5.QtWebEngineWidgets import QWebEngineView -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) view = QWebEngineView() view.show() - view.setHtml("""\ + view.setHtml( + """\ - """) + """ + ) app.exec() diff --git a/qt__pyqt__pyside__pyqode/printer__QTextEdit__QPrinter__QPrinterDialog/main.py b/qt__pyqt__pyside__pyqode/printer__QTextEdit__QPrinter__QPrinterDialog/main.py index 8dd5fded4..104fd428e 100644 --- a/qt__pyqt__pyside__pyqode/printer__QTextEdit__QPrinter__QPrinterDialog/main.py +++ b/qt__pyqt__pyside__pyqode/printer__QTextEdit__QPrinter__QPrinterDialog/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt @@ -14,7 +14,7 @@ """ -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) printer = Qt.QPrinter() diff --git a/qt__pyqt__pyside__pyqode/printer_to_PDF__QTextEdit__QPrinter__QPrinterDialog/main.py b/qt__pyqt__pyside__pyqode/printer_to_PDF__QTextEdit__QPrinter__QPrinterDialog/main.py index ed4a0bc22..449ae3993 100644 --- a/qt__pyqt__pyside__pyqode/printer_to_PDF__QTextEdit__QPrinter__QPrinterDialog/main.py +++ b/qt__pyqt__pyside__pyqode/printer_to_PDF__QTextEdit__QPrinter__QPrinterDialog/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt @@ -14,7 +14,7 @@ """ -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) printer = Qt.QPrinter() @@ -23,7 +23,7 @@ te.setHtml(html) te.show() - printer.setOutputFileName('result.pdf') + printer.setOutputFileName("result.pdf") te.print(printer) app.exec() diff --git a/qt__pyqt__pyside__pyqode/pyq5__QTreeView_QFileSystemModel_and_viewer_as_QTextEdit__on_double_click.py b/qt__pyqt__pyside__pyqode/pyq5__QTreeView_QFileSystemModel_and_viewer_as_QTextEdit__on_double_click.py index d7193ee4c..89dd73810 100644 --- a/qt__pyqt__pyside__pyqode/pyq5__QTreeView_QFileSystemModel_and_viewer_as_QTextEdit__on_double_click.py +++ b/qt__pyqt__pyside__pyqode/pyq5__QTreeView_QFileSystemModel_and_viewer_as_QTextEdit__on_double_click.py @@ -1,17 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" + -from PyQt5.Qt import QApplication, QMainWindow, QSplitter, QTreeView, QTextEdit, QFileSystemModel, QDir import os +from PyQt5.Qt import ( + QApplication, + QMainWindow, + QSplitter, + QTreeView, + QTextEdit, + QFileSystemModel, + QDir, +) class MainWindow(QMainWindow): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) - self.setWindowTitle('Direct tree') + self.setWindowTitle("Direct tree") self.model = QFileSystemModel() self.model.setRootPath(QDir.rootPath()) @@ -34,10 +43,10 @@ def __init__(self, parent=None): self.setCentralWidget(splitter) - def _on_double_clicked(self, index): + def _on_double_clicked(self, index) -> None: file_name = self.model.filePath(index) - with open(file_name, encoding='utf-8') as f: + with open(file_name, encoding="utf-8") as f: text = f.read() self.textEdit.setPlainText(text) diff --git a/qt__pyqt__pyside__pyqode/pyq5__simple_balls__with_part_transparent_body.py b/qt__pyqt__pyside__pyqode/pyq5__simple_balls__with_part_transparent_body.py index e9392d508..c9d857d1e 100644 --- a/qt__pyqt__pyside__pyqode/pyq5__simple_balls__with_part_transparent_body.py +++ b/qt__pyqt__pyside__pyqode/pyq5__simple_balls__with_part_transparent_body.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from random import randint -from typing import List -from PyQt5.Qt import * +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton +from PyQt5.QtGui import QPainter, QColor, QPen +from PyQt5.QtCore import QTimer, Qt # SOURCE: https://github.com/gil9red/SimplePyScripts/blob/1d6226ea5545e49b533e71c92d77856a7b2e171d/pygame__examples/simple_balls.py @@ -29,8 +30,15 @@ def update(self): self.y += self.v_y def draw(self, painter: QPainter): - r, g, b = self.color - color = QColor(r, g, b) + if isinstance(self.color, tuple): + if len(self.color) == 4: + r, g, b, a = self.color + color = QColor(r, g, b, a) + else: + r, g, b = self.color + color = QColor(r, g, b) + else: + color = self.color painter.save() painter.setPen(Qt.black) @@ -59,8 +67,21 @@ def right(self): return self.x + self.r +def get_random_vector() -> tuple[int, int]: + pos = 0, 0 + # Если pos равен (0, 0), пересчитываем значения, т.к. шарик должен двигаться + while pos == (0, 0): + pos = randint(-3, 3), randint(-3, 3) + + return pos + + +def get_random_color() -> tuple[int, int, int]: + return randint(0, 255), randint(0, 255), randint(0, 255) + + class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setWindowFlags(Qt.FramelessWindowHint) @@ -69,7 +90,7 @@ def __init__(self): self._old_pos = None self.frame_color = Qt.darkCyan - self.balls: List[Ball] = [] + self.balls: list[Ball] = [] timeout = 1000 // 60 @@ -84,22 +105,22 @@ def __init__(self): self.setLayout(layout) - def mousePressEvent(self, event): + def mousePressEvent(self, event) -> None: if event.button() == Qt.LeftButton: self._old_pos = event.pos() - def mouseReleaseEvent(self, event): + def mouseReleaseEvent(self, event) -> None: if event.button() == Qt.LeftButton: self._old_pos = None - def mouseMoveEvent(self, event): + def mouseMoveEvent(self, event) -> None: if not self._old_pos: return delta = event.pos() - self._old_pos self.move(self.pos() + delta) - def tick(self): + def tick(self) -> None: for ball in self.balls: ball.update() @@ -114,7 +135,7 @@ def tick(self): # Вызов перерисовки self.update() - def paintEvent(self, event): + def paintEvent(self, event) -> None: painter = QPainter(self) painter.setBrush(QColor(0, 0, 0, 1)) @@ -124,18 +145,7 @@ def paintEvent(self, event): for ball in self.balls: ball.draw(painter) - def append_random_ball(self): - def get_random_vector(): - pos = 0, 0 - # Если pos равен (0, 0), пересчитываем значения, т.к. шарик должен двигаться - while pos == (0, 0): - pos = randint(-3, 3), randint(-3, 3) - - return pos - - def get_random_color(): - return randint(0, 255), randint(0, 255), randint(0, 255) - + def append_random_ball(self) -> None: x = self.width() // 2 + randint(-self.width() // 4, self.width() // 4) y = self.height() // 2 + randint(-self.height() // 4, self.height() // 4) r = randint(10, 20) @@ -146,10 +156,10 @@ def get_random_color(): self.balls.append(ball) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) - WIDTH = 600 # ширина экрана + WIDTH = 600 # ширина экрана HEIGHT = 600 # высота экрана BALL_NUMBER = 1000 diff --git a/qt__pyqt__pyside__pyqode/pyqode_example/main.py b/qt__pyqt__pyside__pyqode/pyqode_example/main.py index a98fd2ea1..d08cc1266 100644 --- a/qt__pyqt__pyside__pyqode/pyqode_example/main.py +++ b/qt__pyqt__pyside__pyqode/pyqode_example/main.py @@ -2,15 +2,16 @@ # -*- coding: utf-8 -*- -if __name__ == '__main__': +if __name__ == "__main__": import sys - from PySide.QtGui import QApplication, QMainWindow - app = QApplication(sys.argv) + from PySide.QtGui import QApplication, QMainWindow from pyqode.core import api from pyqode.core import modes + app = QApplication(sys.argv) + editor = api.CodeEdit() # Добавление модов: подсветка кода, подсветка текущей строки и @@ -19,10 +20,10 @@ editor.modes.append(modes.CaretLineHighlighterMode()) editor.modes.append(modes.IndenterMode()) - editor.setPlainText(open(__file__, encoding='utf-8').read(), None, None) + editor.setPlainText(open(__file__, encoding="utf-8").read(), None, None) mw = QMainWindow() - mw.setWindowTitle('pyqode') + mw.setWindowTitle("pyqode") mw.setCentralWidget(editor) mw.show() diff --git a/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout.py b/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout.py index b661bb814..38a464657 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout.py +++ b/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout.py @@ -1,36 +1,38 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import QtWidgets as qtw from PyQt5.QtCore import QRect, QTimer -def show_tooltip(parent, widget): - qtw.QToolTip.showText(parent.mapToGlobal(widget.pos()), widget.toolTip(), widget, QRect()) +def show_tooltip(parent, widget) -> None: + qtw.QToolTip.showText( + parent.mapToGlobal(widget.pos()), widget.toolTip(), widget, QRect() + ) -if __name__ == '__main__': +if __name__ == "__main__": app = qtw.QApplication([]) line_edit = qtw.QLineEdit() - line_edit.setToolTip('This my LINE EDIT!') + line_edit.setToolTip("This my LINE EDIT!") - button = qtw.QPushButton('My button!') - button.setToolTip('Simple button...') + button = qtw.QPushButton("My button!") + button.setToolTip("Simple button...") text_edit = qtw.QTextEdit() - text_edit.setToolTip('TextEdit!') + text_edit.setToolTip("TextEdit!") layout = qtw.QFormLayout() - layout.addRow('Line edit:', line_edit) - layout.addRow('Button:', button) - layout.addRow('Text edit:', text_edit) + layout.addRow("Line edit:", line_edit) + layout.addRow("Button:", button) + layout.addRow("Text edit:", text_edit) w = qtw.QWidget() - w.setWindowTitle('Tooltip example') + w.setWindowTitle("Tooltip example") w.setLayout(layout) w.show() diff --git a/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout__using_QEvent.py b/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout__using_QEvent.py index bbe097d44..d2219b5ea 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout__using_QEvent.py +++ b/qt__pyqt__pyside__pyqode/pyqt5_QToolTip_with_programmatically_timeout__using_QEvent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import QtWidgets as qtw @@ -9,29 +9,32 @@ from PyQt5.QtGui import QHelpEvent -def show_tooltip(parent, widget): - app.notify(widget, QHelpEvent(QHelpEvent.ToolTip, widget.pos(), parent.mapToGlobal(widget.pos()))) +def show_tooltip(parent, widget) -> None: + app.notify( + widget, + QHelpEvent(QHelpEvent.ToolTip, widget.pos(), parent.mapToGlobal(widget.pos())), + ) -if __name__ == '__main__': +if __name__ == "__main__": app = qtw.QApplication([]) line_edit = qtw.QLineEdit() - line_edit.setToolTip('This my LINE EDIT!') + line_edit.setToolTip("This my LINE EDIT!") - button = qtw.QPushButton('My button!') - button.setToolTip('Simple button...') + button = qtw.QPushButton("My button!") + button.setToolTip("Simple button...") text_edit = qtw.QTextEdit() - text_edit.setToolTip('TextEdit!') + text_edit.setToolTip("TextEdit!") layout = qtw.QFormLayout() - layout.addRow('Line edit:', line_edit) - layout.addRow('Button:', button) - layout.addRow('Text edit:', text_edit) + layout.addRow("Line edit:", line_edit) + layout.addRow("Button:", button) + layout.addRow("Text edit:", text_edit) w = qtw.QWidget() - w.setWindowTitle('Tooltip example') + w.setWindowTitle("Tooltip example") w.setLayout(layout) w.show() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QComboBox.py b/qt__pyqt__pyside__pyqode/pyqt5__QComboBox.py index d58201587..4dc2d35f8 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QComboBox.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QComboBox.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import * class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() self.tb_result = QTextBrowser() self.cb_pets = QComboBox() self.cb_pets.currentIndexChanged.connect(self._on_pet_changed) - self.cb_pets.addItem('Собаки', userData='dogs') - self.cb_pets.addItem('Коты', userData='cats') + self.cb_pets.addItem("Собаки", userData="dogs") + self.cb_pets.addItem("Коты", userData="cats") layout = QVBoxLayout() layout.addWidget(self.cb_pets) @@ -24,7 +24,7 @@ def __init__(self): self.setLayout(layout) - def _on_pet_changed(self, index): + def _on_pet_changed(self, index) -> None: # print(index) # 0 # print(self.cb_pets.itemText(index)) # Собаки # print(self.cb_pets.itemData(index)) # dogs @@ -34,17 +34,17 @@ def _on_pet_changed(self, index): # print(self.cb_pets.currentData()) # dogs data = self.cb_pets.itemData(index) - if data == 'cats': + if data == "cats": text = "Вы любите кошек" - elif data == 'dogs': + elif data == "dogs": text = "Вы любите собак" else: - text = '' + text = "" self.tb_result.setHtml(text) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QFileSystemModel__append_selected_files_and_dirs.py b/qt__pyqt__pyside__pyqode/pyqt5__QFileSystemModel__append_selected_files_and_dirs.py index c9525960e..4919e88a7 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QFileSystemModel__append_selected_files_and_dirs.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QFileSystemModel__append_selected_files_and_dirs.py @@ -1,18 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import ( - QWidget, QFileSystemModel, QTreeView, QListWidget, QPushButton, QSplitter, - QVBoxLayout, QApplication + QWidget, + QFileSystemModel, + QTreeView, + QListWidget, + QPushButton, + QSplitter, + QVBoxLayout, + QApplication, ) from PyQt5.QtCore import QDir, Qt class Widget(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() path = QDir.rootPath() @@ -23,11 +29,13 @@ def __init__(self): self.tree_view = QTreeView() self.tree_view.setModel(self.model) self.tree_view.setSelectionMode(QTreeView.ExtendedSelection) - self.tree_view.selectionModel().selectionChanged.connect(self._on_selection_changed) + self.tree_view.selectionModel().selectionChanged.connect( + self._on_selection_changed + ) self.list_files = QListWidget() - self.button_add = QPushButton('Добавить!') + self.button_add = QPushButton("Добавить!") self.button_add.setEnabled(False) self.button_add.clicked.connect(self._on_add) @@ -39,17 +47,17 @@ def __init__(self): main_layout.addWidget(splitter) main_layout.addWidget(self.button_add) - def _on_selection_changed(self, selected, deselected): + def _on_selection_changed(self, selected, deselected) -> None: has = self.tree_view.selectionModel().hasSelection() self.button_add.setEnabled(has) - def _on_add(self): + def _on_add(self) -> None: for row in self.tree_view.selectionModel().selectedRows(): path = self.model.filePath(row) self.list_files.addItem(path) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QItemDelegate.py b/qt__pyqt__pyside__pyqode/pyqt5__QItemDelegate.py index 602fe569f..3b269007d 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QItemDelegate.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QItemDelegate.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from datetime import datetime @@ -9,7 +9,7 @@ class MessagesWindow(QtWidgets.QTableView): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self.setModel(ModelMessages()) @@ -18,30 +18,49 @@ def __init__(self, parent=None): class ModelMessages(QtGui.QStandardItemModel): - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) - self.appendRow([QtGui.QStandardItem('2018-11-25 11:15:06.0001'), QtGui.QStandardItem('111')]) - self.appendRow([QtGui.QStandardItem('2018-11-25 11:15:06.0001'), QtGui.QStandardItem('111')]) - self.appendRow([QtGui.QStandardItem('2018-11-25 11:15:06.0001'), QtGui.QStandardItem('111')]) + self.appendRow( + [ + QtGui.QStandardItem("2018-11-25 11:15:06.0001"), + QtGui.QStandardItem("111"), + ] + ) + self.appendRow( + [ + QtGui.QStandardItem("2018-11-25 11:15:06.0001"), + QtGui.QStandardItem("111"), + ] + ) + self.appendRow( + [ + QtGui.QStandardItem("2018-11-25 11:15:06.0001"), + QtGui.QStandardItem("111"), + ] + ) class ItemDateFormat(QtWidgets.QItemDelegate): - def paint(self, painter, opt: QtWidgets.QStyleOptionViewItem, index): + def paint(self, painter, opt: QtWidgets.QStyleOptionViewItem, index) -> None: self.drawBackground(painter, opt, index) self.drawFocus(painter, opt, opt.rect) try: - opt.text = '{0:%d %b %H:%M}'.format(datetime.strptime(index.model().data(index), '%Y-%m-%d %H:%M:%S.%f')) + value = index.model().data(index) + dt = datetime.strptime(value, '%Y-%m-%d %H:%M:%S.%f') + opt.text = f"{dt:%d %b %H:%M}" except TypeError: pass opt.font.setItalic(True) opt.backgroundBrush = QtCore.Qt.yellow - QtWidgets.QApplication.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, opt, painter) + QtWidgets.QApplication.style().drawControl( + QtWidgets.QStyle.CE_ItemViewItem, opt, painter + ) -if __name__ == '__main__': +if __name__ == "__main__": app = QtWidgets.QApplication([]) w = MessagesWindow() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QLineEdit_with_fixed_cursor_position.py b/qt__pyqt__pyside__pyqode/pyqt5__QLineEdit_with_fixed_cursor_position.py index a96a0cf0a..919573d60 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QLineEdit_with_fixed_cursor_position.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QLineEdit_with_fixed_cursor_position.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter.py b/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter.py index 3ccf32644..0d8831cda 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" -from PyQt5.Qt import * +from PyQt5.QtCore import Qt, QRegularExpression +from PyQt5.QtGui import QSyntaxHighlighter, QTextCharFormat, QFont +from PyQt5.QtWidgets import QApplication, QPlainTextEdit, QTextEdit class MyHighlighter(QSyntaxHighlighter): - def highlightBlock(self, text): + def highlightBlock(self, text) -> None: char_format = QTextCharFormat() char_format.setFontWeight(QFont.Bold) char_format.setForeground(Qt.darkMagenta) @@ -20,14 +22,23 @@ def highlightBlock(self, text): self.setFormat(match.capturedStart(), match.capturedLength(), char_format) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) - mw = QTextEdit() - mw.setText("x = sin(1)\nb = cos(sin(PI));") - - highlighter = MyHighlighter(mw.document()) - - mw.show() + text_edit = QTextEdit() + text_edit.setWindowTitle("QTextEdit") + text_edit.setText("x = sin(1)\nb = cos(sin(PI));") + highlighter_text_edit = MyHighlighter(text_edit.document()) + text_edit.show() + + plain_text_edit = QPlainTextEdit() + plain_text_edit.setWindowTitle("QPlainTextEdit") + plain_text_edit.setPlainText(text_edit.toPlainText()) + highlighter_plain_text_edit = MyHighlighter(plain_text_edit.document()) + plain_text_edit.show() + + pos = text_edit.pos() + pos.setX(pos.x() + text_edit.width() + 5) + plain_text_edit.move(pos) app.exec() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter__with_many_QTextCharFormat.py b/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter__with_many_QTextCharFormat.py index 7d1a9283f..0c3db8a08 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter__with_many_QTextCharFormat.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QSyntaxHighlighter__with_many_QTextCharFormat.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.Qt import * class MyHighlighter(QSyntaxHighlighter): - def __init__(self, parent): + def __init__(self, parent) -> None: super().__init__(parent) self.regexp_by_format = dict() @@ -16,24 +16,26 @@ def __init__(self, parent): char_format = QTextCharFormat() char_format.setFontWeight(QFont.Bold) char_format.setForeground(Qt.darkMagenta) - self.regexp_by_format[r'\bsin\b'] = char_format + self.regexp_by_format[r"\bsin\b"] = char_format char_format = QTextCharFormat() char_format.setFontWeight(QFont.Bold) char_format.setFontItalic(True) char_format.setForeground(Qt.darkCyan) - self.regexp_by_format[r'\bcos\b'] = char_format + self.regexp_by_format[r"\bcos\b"] = char_format - def highlightBlock(self, text): + def highlightBlock(self, text) -> None: for regexp, char_format in self.regexp_by_format.items(): expression = QRegularExpression(regexp) it = expression.globalMatch(text) while it.hasNext(): match = it.next() - self.setFormat(match.capturedStart(), match.capturedLength(), char_format) + self.setFormat( + match.capturedStart(), match.capturedLength(), char_format + ) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = QTextEdit() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__select_all_row_and_column__by_cell_click.py b/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__select_all_row_and_column__by_cell_click.py index 1cfb3a9bc..79cf2fd54 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__select_all_row_and_column__by_cell_click.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__select_all_row_and_column__by_cell_click.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QMainWindow, QTableWidget, QTableWidgetItem, QApplication @@ -9,17 +9,17 @@ class Widget(QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.setWindowTitle('Table') + self.setWindowTitle("Table") self.table_widget = QTableWidget() self.table_widget.cellClicked.connect(self._on_cell_clicked) self.setCentralWidget(self.table_widget) - def _on_cell_clicked(self, row, col): + def _on_cell_clicked(self, row, col) -> None: selection_model = self.table_widget.selectionModel() selection_model.clear() @@ -31,18 +31,18 @@ def _on_cell_clicked(self, row, col): selection_model.select(index, QItemSelectionModel.Select) - def fill(self): + def fill(self) -> None: self.table_widget.clear() - labels = ['ID', 'NAME', 'PRICE'] + labels = ["ID", "NAME", "PRICE"] self.table_widget.setColumnCount(len(labels)) self.table_widget.setHorizontalHeaderLabels(labels) for id_, name, price in [ - ['1', 'name_1', 'price_1'], - ['2', 'name_2', 'price_2'], - ['3', 'name_3', 'price_3'], + ["1", "name_1", "price_1"], + ["2", "name_2", "price_2"], + ["3", "name_3", "price_3"], ]: row = self.table_widget.rowCount() self.table_widget.setRowCount(row + 1) @@ -52,7 +52,7 @@ def fill(self): self.table_widget.setItem(row, 2, QTableWidgetItem(price)) -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) w = Widget() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__show_dialog_on_double_click_cell.py b/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__show_dialog_on_double_click_cell.py index a3d4cbfc5..9c67d101c 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__show_dialog_on_double_click_cell.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QTableWidget__show_dialog_on_double_click_cell.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5 import Qt class MainWindow(Qt.QMainWindow): - def __init__(self): + def __init__(self) -> None: super().__init__() self.table = Qt.QTableWidget() @@ -16,7 +16,7 @@ def __init__(self): self.table.setColumnCount(2) self.table.itemDoubleClicked.connect(self.on_cell_item_clicked) - rows = ['Vasya', 'Petya', 'Masha'] + rows = ["Vasya", "Petya", "Masha"] for i, value in enumerate(rows): self.table.setItem(i, 0, Qt.QTableWidgetItem(str(i + 1))) @@ -29,15 +29,17 @@ def __init__(self): self.setCentralWidget(main_widget) - def on_cell_item_clicked(self, item): + def on_cell_item_clicked(self, item) -> None: print(item) - new_text, ok = Qt.QInputDialog.getText(self, 'Change Name', 'Change Name', text=item.text()) + new_text, ok = Qt.QInputDialog.getText( + self, "Change Name", "Change Name", text=item.text() + ) if ok: item.setText(new_text) -if __name__ == '__main__': +if __name__ == "__main__": app = Qt.QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QThread_with_QProgressDialog_PleaseWait.py b/qt__pyqt__pyside__pyqode/pyqt5__QThread_with_QProgressDialog_PleaseWait.py index b203c538e..4d8d76894 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QThread_with_QProgressDialog_PleaseWait.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QThread_with_QProgressDialog_PleaseWait.py @@ -1,29 +1,30 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" +import time from PyQt5.Qt import * class RunFuncThread(QThread): run_finished = pyqtSignal(object) - def __init__(self, func): + def __init__(self, func) -> None: super().__init__() self.func = func - def run(self): + def run(self) -> None: self.run_finished.emit(self.func()) class MainWindow(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() - self.pb_go = QPushButton('Go') + self.pb_go = QPushButton("Go") self.pb_go.clicked.connect(self.go) self.te_log = QPlainTextEdit() @@ -34,14 +35,13 @@ def __init__(self): self.setLayout(layout) - def _on_run_finished(self, value): + def _on_run_finished(self, value) -> None: self.te_log.setPlainText(str(value)) - def go(self): + def go(self) -> None: progress_dialog = QProgressDialog(self) def foo(): - import time items = [] for i in range(10): @@ -57,13 +57,13 @@ def foo(): thread.run_finished.connect(progress_dialog.close) thread.start() - progress_dialog.setWindowTitle('Please wait...') + progress_dialog.setWindowTitle("Please wait...") progress_dialog.setLabelText(progress_dialog.windowTitle()) progress_dialog.setRange(0, 0) progress_dialog.exec() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) mw = MainWindow() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QTreeView_QFileSystemModel.py b/qt__pyqt__pyside__pyqode/pyqt5__QTreeView_QFileSystemModel.py index 5e882a3e2..047c7fe30 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QTreeView_QFileSystemModel.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QTreeView_QFileSystemModel.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QTreeView, QFileSystemModel, QApplication from PyQt5.QtCore import QDir -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) model = QFileSystemModel() diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_async__multiprocessing__process.py b/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_async__multiprocessing__process.py index 1c9787b21..8013eed2f 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_async__multiprocessing__process.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_async__multiprocessing__process.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication @@ -10,7 +10,7 @@ class Client(QWebEnginePage): - def __init__(self, urls): + def __init__(self, urls) -> None: self.app = QApplication([]) super().__init__() @@ -22,10 +22,10 @@ def __init__(self, urls): self.load(QUrl(url)) self.app.exec_() - def _on_load_finished(self): + def _on_load_finished(self) -> None: self.toHtml(self.callable) - def callable(self, html_str): + def callable(self, html_str) -> None: self.response_list.append(html_str) self.app.quit() @@ -35,22 +35,23 @@ def go(urls): return client.response_list -if __name__ == '__main__': +if __name__ == "__main__": urls = [ [ - 'http://doc.qt.io/Qt-5/qwebenginepage.html', - 'https://yandex.ru/', + "http://doc.qt.io/Qt-5/qwebenginepage.html", + "https://yandex.ru/", ], [ - 'http://doc.qt.io/Qt-5/qwebenginepage.html', - 'https://www.google.ru/', + "http://doc.qt.io/Qt-5/qwebenginepage.html", + "https://www.google.ru/", ], [ - 'https://www.google.ru/', - ] + "https://www.google.ru/", + ], ] from multiprocessing import Pool + with Pool() as p: results = p.map(go, urls) print(len(results)) @@ -61,7 +62,7 @@ def go(urls): print(len(result)) for html in result: - with open('result_{}.html'.format(number), 'w', encoding='utf-8') as f: + with open(f"result_{number}.html", "w", encoding="utf-8") as f: f.write(html) number += 1 diff --git a/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_sync.py b/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_sync.py index 4cc5bbd9d..9a6d08c47 100644 --- a/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_sync.py +++ b/qt__pyqt__pyside__pyqode/pyqt5__QWebEnginePage__Client_sync.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -__author__ = 'ipetrash' +__author__ = "ipetrash" from PyQt5.QtWidgets import QApplication @@ -10,7 +10,7 @@ class Client(QWebEnginePage): - def __init__(self, urls): + def __init__(self, urls) -> None: self.app = QApplication([]) super().__init__() @@ -22,19 +22,20 @@ def __init__(self, urls): self.load(QUrl(url)) self.app.exec_() - def _on_load_finished(self): + def _on_load_finished(self) -> None: self.toHtml(self.callable) - def callable(self, html_str): + def callable(self, html_str) -> None: self.response_list.append(html_str) self.app.quit() -if __name__ == '__main__': +if __name__ == "__main__": urls = [ - 'http://doc.qt.io/Qt-5/qwebenginepage.html', - 'https://www.google.ru/', - 'https://yandex.ru/', + "http://doc.qt.io/Qt-5/qwebenginepage.html", + "https://www.google.ru/", + "https://yandex.ru/", ] client = Client(urls) - print(len(client.response_list), client.response_list) # 3 ['